Skip to content

Commit d3ecf6e

Browse files
committed
Improved webob support
1 parent c19d432 commit d3ecf6e

File tree

6 files changed

+83
-26
lines changed

6 files changed

+83
-26
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,5 @@ integrations = [
340340
"litestar>=2 ; python_version>=\"3.10\" and python_version<\"4.0\"",
341341
"uvicorn>=0.11.6",
342342
"daphne>=4.0.0,<5.0",
343+
"WebOb>=1.8",
343344
]

src/graphql_server/webob/views.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
from __future__ import annotations
22

33
import warnings
4-
from typing import TYPE_CHECKING, Any, Mapping, Optional, Union, cast
5-
from typing_extensions import TypeGuard
6-
7-
from webob import Request, Response
4+
from collections.abc import Mapping
5+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
86

97
from graphql_server.http import GraphQLRequestData
108
from graphql_server.http.exceptions import HTTPException
119
from graphql_server.http.sync_base_view import SyncBaseHTTPView, SyncHTTPRequestAdapter
12-
from graphql_server.http.typevars import Context, RootValue
1310
from graphql_server.http.types import HTTPMethod, QueryParams
11+
from graphql_server.http.typevars import Context, RootValue
12+
from webob import Request, Response
1413

1514
if TYPE_CHECKING:
1615
from graphql.type import GraphQLSchema
16+
1717
from graphql_server.http import GraphQLHTTPResponse
1818
from graphql_server.http.ides import GraphQL_IDE
1919

src/tests/http/clients/webob.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,22 @@
66
import json
77
import urllib.parse
88
from io import BytesIO
9+
from json import dumps
910
from typing import Any, Optional, Union
1011
from typing_extensions import Literal
1112

1213
from graphql import ExecutionResult
13-
from webob import Request, Response
14+
from urllib3 import encode_multipart_formdata
1415

1516
from graphql_server.http import GraphQLHTTPResponse
1617
from graphql_server.http.ides import GraphQL_IDE
1718
from graphql_server.webob import GraphQLView as BaseGraphQLView
1819
from tests.http.context import get_context
1920
from tests.views.schema import Query, schema
21+
from webob import Request, Response
2022

21-
from .base import JSON, HttpClient, Response as ClientResponse, ResultOverrideFunction
23+
from .base import JSON, HttpClient, ResultOverrideFunction
24+
from .base import Response as ClientResponse
2225

2326

2427
class GraphQLView(BaseGraphQLView[dict[str, object], object]):
@@ -82,18 +85,16 @@ async def _graphql_request(
8285

8386
url = "/graphql"
8487

85-
if body and files:
86-
body.update({name: (file, name) for name, file in files.items()})
88+
headers = self._get_headers(method=method, headers=headers, files=files)
8789

8890
if method == "get":
8991
body_encoded = urllib.parse.urlencode(body or {})
9092
url = f"{url}?{body_encoded}"
91-
else:
92-
if body:
93-
data = body if files else json.dumps(body)
94-
kwargs["body"] = data
95-
96-
headers = self._get_headers(method=method, headers=headers, files=files)
93+
elif body:
94+
if files:
95+
header_pairs, body = create_multipart_request_body(body, files)
96+
headers = dict(header_pairs)
97+
kwargs["body"] = body
9798

9899
return await self.request(url, method, headers=headers, **kwargs)
99100

@@ -104,9 +105,11 @@ def _do_request(
104105
headers: Optional[dict[str, str]] = None,
105106
**kwargs: Any,
106107
) -> ClientResponse:
107-
body = kwargs.get("body", None)
108+
body = kwargs.pop("body", None)
109+
if isinstance(body, dict):
110+
body = json.dumps(body).encode("utf-8")
108111
req = Request.blank(
109-
url, method=method.upper(), headers=headers or {}, body=body
112+
url, method=method.upper(), headers=headers or {}, body=body, **kwargs
110113
)
111114
resp = self.view.dispatch_request(req)
112115
return ClientResponse(
@@ -139,5 +142,26 @@ async def post(
139142
json: Optional[JSON] = None,
140143
headers: Optional[dict[str, str]] = None,
141144
) -> ClientResponse:
142-
body = json if json is not None else data
145+
body = dumps(json).encode("utf-8") if json is not None else data
143146
return await self.request(url, "post", headers=headers, body=body)
147+
148+
149+
def create_multipart_request_body(
150+
body: dict[str, object], files: dict[str, BytesIO]
151+
) -> tuple[list[tuple[str, str]], bytes]:
152+
fields = {
153+
"operations": body["operations"],
154+
"map": body["map"],
155+
}
156+
157+
for filename, data in files.items():
158+
fields[filename] = (filename, data.read().decode(), "text/plain")
159+
160+
request_body, content_type_header = encode_multipart_formdata(fields)
161+
162+
headers = [
163+
("Content-Type", content_type_header),
164+
("Content-Length", f"{len(request_body)}"),
165+
]
166+
167+
return headers, request_body

src/tests/http/conftest.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,10 @@ def _get_http_client_classes() -> Generator[Any, None, None]:
2828
[pytest.mark.channels, pytest.mark.django_db],
2929
),
3030
]:
31-
try:
32-
client_class = getattr(
33-
importlib.import_module(f".{module}", package="tests.http.clients"),
34-
client,
35-
)
36-
except ImportError:
37-
client_class = None
31+
client_class = getattr(
32+
importlib.import_module(f".{module}", package="tests.http.clients"),
33+
client,
34+
)
3835

3936
yield pytest.param(
4037
client_class,

src/tests/http/test_multipart_subscription.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ def http_client(http_client_class: type[HttpClient]) -> HttpClient:
3131
reason="SyncChannelsHttpClient doesn't support multipart subscriptions"
3232
)
3333

34+
with contextlib.suppress(ImportError):
35+
from .clients.webob import WebobHttpClient
36+
37+
if http_client_class is WebobHttpClient:
38+
pytest.skip(
39+
reason="WebobHttpClient doesn't support multipart subscriptions"
40+
)
41+
3442
with contextlib.suppress(ImportError):
3543
from .clients.async_flask import AsyncFlaskHttpClient
3644
from .clients.flask import FlaskHttpClient

uv.lock

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)