Skip to content

Commit a2f3edc

Browse files
committed
Merge branch 'release/0.3.1'
2 parents 220850f + 60311a6 commit a2f3edc

File tree

7 files changed

+126
-21
lines changed

7 files changed

+126
-21
lines changed

.flake8

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ per-file-ignores =
122122
; Found wrong metadata variable
123123
WPS410,
124124

125+
swagger.py:
126+
; Too many local variables
127+
WPS210,
128+
125129
exclude =
126130
./.git,
127131
./venv,

.python-version

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
3.11.4
2+
3.10.12
3+
3.9.17
4+
3.8.17

aiohttp_deps/swagger.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import inspect
22
from collections import defaultdict
33
from logging import getLogger
4-
from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar, get_type_hints
4+
from typing import (
5+
Any,
6+
Awaitable,
7+
Callable,
8+
Dict,
9+
Optional,
10+
Tuple,
11+
TypeVar,
12+
get_type_hints,
13+
)
514

615
import pydantic
716
from aiohttp import web
@@ -13,6 +22,7 @@
1322

1423
_T = TypeVar("_T") # noqa: WPS111
1524

25+
REF_TEMPLATE = "#/components/schemas/{model}"
1626
SCHEMA_KEY = "openapi_schema"
1727
SWAGGER_HTML_TEMPALTE = """
1828
<html lang="en">
@@ -75,12 +85,13 @@ def dummy(_var: annotation.annotation) -> None: # type: ignore
7585
return var == Optional[var]
7686

7787

78-
def _add_route_def( # noqa: C901, WPS210
88+
def _add_route_def( # noqa: C901, WPS210, WPS211
7989
openapi_schema: Dict[str, Any],
8090
route: web.ResourceRoute,
8191
method: str,
8292
graph: DependencyGraph,
8393
extra_openapi: Dict[str, Any],
94+
extra_openapi_schemas: Dict[str, Any],
8495
) -> None:
8596
route_info: Dict[str, Any] = {
8697
"description": inspect.getdoc(graph.target),
@@ -90,7 +101,10 @@ def _add_route_def( # noqa: C901, WPS210
90101
if route.resource is None: # pragma: no cover
91102
return
92103

93-
params: Dict[tuple[str, str], Any] = {}
104+
if extra_openapi_schemas:
105+
openapi_schema["components"]["schemas"].update(extra_openapi_schemas)
106+
107+
params: Dict[Tuple[str, str], Any] = {}
94108

95109
def _insert_in_params(data: Dict[str, Any]) -> None:
96110
element = params.get((data["name"], data["in"]))
@@ -114,9 +128,9 @@ def _insert_in_params(data: Dict[str, Any]) -> None:
114128
):
115129
input_schema = pydantic.TypeAdapter(
116130
dependency.signature.annotation,
117-
).json_schema()
131+
).json_schema(ref_template=REF_TEMPLATE)
118132
openapi_schema["components"]["schemas"].update(
119-
input_schema.pop("definitions", {}),
133+
input_schema.pop("$defs", {}),
120134
)
121135
route_info["requestBody"] = {
122136
"content": {content_type: {"schema": input_schema}},
@@ -216,13 +230,19 @@ async def event_handler(app: web.Application) -> None:
216230
"__extra_openapi__",
217231
{},
218232
)
233+
extra_schemas = getattr(
234+
route._handler.original_handler,
235+
"__extra_openapi_schemas__",
236+
{},
237+
)
219238
try:
220239
_add_route_def(
221240
openapi_schema,
222241
route, # type: ignore
223242
route.method,
224243
route._handler.graph,
225244
extra_openapi=extra_openapi,
245+
extra_openapi_schemas=extra_schemas,
226246
)
227247
except Exception as exc: # pragma: no cover
228248
logger.warn(
@@ -234,20 +254,23 @@ async def event_handler(app: web.Application) -> None:
234254
elif isinstance(route._handler, InjectableViewHandler):
235255
for key, graph in route._handler.graph_map.items():
236256
extra_openapi = getattr(
237-
getattr(
238-
route._handler.original_handler,
239-
key,
240-
),
257+
getattr(route._handler.original_handler, key),
241258
"__extra_openapi__",
242259
{},
243260
)
261+
extra_schemas = getattr(
262+
getattr(route._handler.original_handler, key),
263+
"__extra_openapi_schemas__",
264+
{},
265+
)
244266
try:
245267
_add_route_def(
246268
openapi_schema,
247269
route, # type: ignore
248270
key,
249271
graph,
250272
extra_openapi=extra_openapi,
273+
extra_openapi_schemas=extra_schemas,
251274
)
252275
except Exception as exc: # pragma: no cover
253276
logger.warn(
@@ -315,16 +338,20 @@ def openapi_response(
315338

316339
def decorator(func: _T) -> _T:
317340
openapi = getattr(func, "__extra_openapi__", {})
341+
openapi_schemas = getattr(func, "__extra_openapi_schemas__", {})
318342
adapter: "pydantic.TypeAdapter[Any]" = pydantic.TypeAdapter(model)
319343
responses = openapi.get("responses", {})
320344
status_response = responses.get(status, {})
321345
if not status_response:
322346
status_response["description"] = description
323347
status_response["content"] = status_response.get("content", {})
324-
status_response["content"][content_type] = {"schema": adapter.json_schema()}
348+
response_schema = adapter.json_schema(ref_template=REF_TEMPLATE)
349+
openapi_schemas.update(response_schema.pop("$defs", {}))
350+
status_response["content"][content_type] = {"schema": response_schema}
325351
responses[status] = status_response
326352
openapi["responses"] = responses
327353
func.__extra_openapi__ = openapi # type: ignore
354+
func.__extra_openapi_schemas__ = openapi_schemas # type: ignore
328355
return func
329356

330357
return decorator

poetry.lock

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

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "aiohttp-deps"
33
description = "Dependency injection for AioHTTP"
44
authors = ["Taskiq team <taskiq@no-reply.com>"]
55
maintainers = ["Taskiq team <taskiq@no-reply.com>"]
6-
version = "0.3.0"
6+
version = "0.3.1"
77
readme = "README.md"
88
license = "LICENSE"
99
classifiers = [

tests/test_swagger.py

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections import deque
2-
from typing import Any, Dict, Optional
2+
from typing import Any, Dict, Generic, Optional, TypeVar
33

44
import pytest
55
from aiohttp import web
@@ -20,6 +20,21 @@
2020
from tests.conftest import ClientGenerator
2121

2222

23+
def follow_ref(ref: str, data: Dict[str, Any]) -> Dict[str, Any]:
24+
"""Function for following openapi references."""
25+
components = deque(ref.split("/"))
26+
current_model = None
27+
while components:
28+
component = components.popleft()
29+
if component.strip() == "#":
30+
current_model = data
31+
continue
32+
current_model = current_model.get(component)
33+
if current_model is None:
34+
return {}
35+
return current_model
36+
37+
2338
def get_schema_by_ref(full_schema: Dict[str, Any], ref: str):
2439
ref_path = deque(ref.split("/"))
2540
current_schema = full_schema
@@ -141,19 +156,26 @@ async def my_handler(body=Depends(Json())):
141156
assert resp.status == 200
142157
resp_json = await resp.json()
143158
handler_info = resp_json["paths"]["/a"]["get"]
144-
print(handler_info)
145159
assert handler_info["requestBody"]["content"]["application/json"] == {}
146160

147161

148162
@pytest.mark.anyio
149-
async def test_json_untyped(
163+
async def test_json_generic(
150164
my_app: web.Application,
151165
aiohttp_client: ClientGenerator,
152166
):
153167
OPENAPI_URL = "/my_api_def.json"
154168
my_app.on_startup.append(setup_swagger(schema_url=OPENAPI_URL))
155169

156-
async def my_handler(body=Depends(Json())):
170+
T = TypeVar("T")
171+
172+
class First(BaseModel):
173+
name: str
174+
175+
class Second(BaseModel, Generic[T]):
176+
data: T
177+
178+
async def my_handler(body: Second[First] = Depends(Json())):
157179
"""Nothing."""
158180

159181
my_app.router.add_get("/a", my_handler)
@@ -163,7 +185,10 @@ async def my_handler(body=Depends(Json())):
163185
assert resp.status == 200
164186
resp_json = await resp.json()
165187
handler_info = resp_json["paths"]["/a"]["get"]
166-
assert {} == handler_info["requestBody"]["content"]["application/json"]
188+
schema = handler_info["requestBody"]["content"]["application/json"]["schema"]
189+
first_ref = schema["properties"]["data"]["$ref"]
190+
first_obj = follow_ref(first_ref, resp_json)
191+
assert "name" in first_obj["properties"]
167192

168193

169194
@pytest.mark.anyio
@@ -438,7 +463,6 @@ async def my_handler():
438463
resp_json = await resp.json()
439464

440465
handler_info = resp_json["paths"]["/a"]["get"]
441-
print(handler_info)
442466
assert handler_info["responses"] == {"200": {}}
443467

444468

@@ -495,7 +519,6 @@ async def my_handler(
495519
assert resp.status == 200
496520
resp_json = await resp.json()
497521
params = resp_json["paths"]["/a"]["get"]["parameters"]
498-
print(params)
499522
assert len(params) == 1
500523
assert params[0]["name"] == "Head"
501524
assert params[0]["required"]
@@ -562,3 +585,34 @@ async def my_handler():
562585
assert "200" in route_info["responses"]
563586
assert "application/json" in route_info["responses"]["200"]["content"]
564587
assert "application/xml" in route_info["responses"]["200"]["content"]
588+
589+
590+
@pytest.mark.anyio
591+
async def test_custom_responses_generics(
592+
my_app: web.Application,
593+
aiohttp_client: ClientGenerator,
594+
) -> None:
595+
OPENAPI_URL = "/my_api_def.json"
596+
my_app.on_startup.append(setup_swagger(schema_url=OPENAPI_URL))
597+
598+
T = TypeVar("T")
599+
600+
class First(BaseModel):
601+
name: str
602+
603+
class Second(BaseModel, Generic[T]):
604+
data: T
605+
606+
@openapi_response(200, Second[First])
607+
async def my_handler():
608+
"""Nothing."""
609+
610+
my_app.router.add_get("/a", my_handler)
611+
client = await aiohttp_client(my_app)
612+
response = await client.get(OPENAPI_URL)
613+
resp_json = await response.json()
614+
first_ref = resp_json["paths"]["/a"]["get"]["responses"]["200"]["content"][
615+
"application/json"
616+
]["schema"]["properties"]["data"]["$ref"]
617+
first_obj = follow_ref(first_ref, resp_json)
618+
assert "name" in first_obj["properties"]

tox.ini

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[tox]
2+
isolated_build = true
3+
env_list =
4+
py311
5+
py310
6+
py39
7+
py38
8+
9+
[testenv]
10+
skip_install = true
11+
allowlist_externals = poetry
12+
commands_pre =
13+
poetry install
14+
commands =
15+
pre-commit run --all-files
16+
poetry run pytest -vv

0 commit comments

Comments
 (0)