Skip to content

Commit 44fe051

Browse files
authored
LRU cache for decorator signature (#174)
* Address signature on lru_cache * Add extra tests for openapi
1 parent 9fd0b44 commit 44fe051

3 files changed

Lines changed: 177 additions & 17 deletions

File tree

lilya/contrib/openapi/decorator.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import inspect
22
from collections.abc import Callable, Sequence
3-
from functools import wraps
3+
from functools import lru_cache, wraps
44
from typing import Annotated, Any
55

66
from typing_extensions import Doc
@@ -13,6 +13,11 @@
1313
SUCCESSFUL_RESPONSE = "Successful response"
1414

1515

16+
@lru_cache
17+
def get_signature(func: Callable[..., Any]) -> inspect.Signature:
18+
return inspect.Signature.from_callable(func)
19+
20+
1621
def openapi(
1722
summary: Annotated[
1823
str | None,
@@ -127,22 +132,11 @@ def openapi(
127132
"""
128133

129134
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
130-
if inspect.iscoroutinefunction(func):
131-
132-
@wraps(func)
133-
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
134-
handler = BaseHandler()
135-
signature = inspect.Signature.from_callable(func)
136-
return handler.handle_response(func, other_signature=signature)
137-
138-
wrapper = async_wrapper
139-
else:
140-
141-
@wraps(func)
142-
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
143-
return func(*args, **kwargs)
144-
145-
wrapper = sync_wrapper
135+
@wraps(func)
136+
def wrapper(*args: Any, **kwargs: Any) -> Any:
137+
handler = BaseHandler()
138+
signature = get_signature(func)
139+
return handler.handle_response(func, other_signature=signature)
146140

147141
def response_models(responses: dict[int, OpenAPIResponse] | None = None) -> Any:
148142
responses: dict[int, ResponseParam] = {} if responses is None else responses # type: ignore

lilya/testclient/helpers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def create_client(
3434
cookies: httpx._types.CookieTypes | None = None,
3535
before_request: Sequence[Callable[..., Any]] | None = None,
3636
after_request: Sequence[Callable[..., Any]] | None = None,
37+
enable_openapi: bool = True,
38+
openapi_config: Any | None = None,
3739
**kwargs: Any,
3840
) -> TestClient:
3941
"""
@@ -64,6 +66,8 @@ def create_client(
6466
include_in_schema=include_in_schema,
6567
before_request=before_request,
6668
after_request=after_request,
69+
enable_openapi=enable_openapi,
70+
openapi_config=openapi_config,
6771
**kwargs,
6872
),
6973
base_url=base_url,

tests/openapi/test_responses.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
from pydantic import BaseModel
2+
3+
from lilya import __version__
4+
from lilya.contrib.openapi.datastructures import OpenAPIResponse
5+
from lilya.contrib.openapi.decorator import openapi
6+
from lilya.routing import Path
7+
from lilya.testclient import create_client
8+
9+
10+
class ErrorResponse(BaseModel):
11+
detail: str
12+
message: str
13+
14+
15+
class User(BaseModel):
16+
name: str
17+
age: int
18+
19+
20+
@openapi(
21+
summary="A test",
22+
description="A test",
23+
responses={
24+
400: OpenAPIResponse(model=ErrorResponse, description="Bad Request"),
25+
201: OpenAPIResponse(model=User, description="Ok"),
26+
},
27+
)
28+
async def create(user: User):
29+
return user
30+
31+
32+
def test_responses_decorator(test_client_factory):
33+
with create_client(
34+
routes=[
35+
Path("/create", handler=create, methods=["POST"]),
36+
]
37+
) as client:
38+
response = client.get("/openapi.json")
39+
40+
assert response.status_code == 200
41+
42+
assert response.json() == {
43+
"openapi": "3.1.0",
44+
"info": {
45+
"title": "Lilya",
46+
"version": __version__,
47+
"summary": "Lilya application",
48+
"description": "Yet another framework/toolkit that delivers.",
49+
"contact": {
50+
"name": "Lilya",
51+
"url": "https://lilya.dev",
52+
"email": "admin@myapp.com",
53+
},
54+
},
55+
"paths": {
56+
"/create": {
57+
"post": {
58+
"operationId": None,
59+
"summary": "A test",
60+
"description": "A test",
61+
"requestBody": {
62+
"content": {
63+
"application/json": {
64+
"schema": {
65+
"properties": {
66+
"name": {"title": "Name", "type": "string"},
67+
"age": {"title": "Age", "type": "integer"},
68+
},
69+
"required": ["name", "age"],
70+
"title": "User",
71+
"type": "object",
72+
}
73+
}
74+
}
75+
},
76+
"responses": {
77+
"400": {
78+
"description": "Bad Request",
79+
"content": {
80+
"application/json": {
81+
"schema": {"$ref": "#/components/schemas/ErrorResponse"}
82+
}
83+
},
84+
},
85+
"201": {
86+
"description": "Ok",
87+
"content": {
88+
"application/json": {
89+
"schema": {"$ref": "#/components/schemas/User"}
90+
}
91+
},
92+
},
93+
},
94+
}
95+
}
96+
},
97+
"components": {
98+
"schemas": {
99+
"ErrorResponse": {
100+
"properties": {
101+
"detail": {"title": "Detail", "type": "string"},
102+
"message": {"title": "Message", "type": "string"},
103+
},
104+
"required": ["detail", "message"],
105+
"title": "ErrorResponse",
106+
"type": "object",
107+
},
108+
"User": {
109+
"properties": {
110+
"name": {"title": "Name", "type": "string"},
111+
"age": {"title": "Age", "type": "integer"},
112+
},
113+
"required": ["name", "age"],
114+
"title": "User",
115+
"type": "object",
116+
},
117+
}
118+
},
119+
"servers": [{"url": "/"}],
120+
}
121+
122+
123+
@openapi()
124+
async def create_again(user: User):
125+
return user
126+
127+
128+
def test_responses_decorator_simple(test_client_factory):
129+
with create_client(
130+
routes=[
131+
Path("/item", handler=create_again, methods=["POST"]),
132+
]
133+
) as client:
134+
response = client.get("/openapi.json")
135+
136+
assert response.status_code == 200
137+
138+
assert response.json() == {
139+
"openapi": "3.1.0",
140+
"info": {
141+
"title": "Lilya",
142+
"version": __version__,
143+
"summary": "Lilya application",
144+
"description": "Yet another framework/toolkit that delivers.",
145+
"contact": {
146+
"name": "Lilya",
147+
"url": "https://lilya.dev",
148+
"email": "admin@myapp.com",
149+
},
150+
},
151+
"paths": {
152+
"/item": {
153+
"post": {
154+
"operationId": None,
155+
"requestBody": {"content": {"application/json": {"schema": {}}}},
156+
"responses": {"200": {"description": "Successful response"}},
157+
}
158+
}
159+
},
160+
"components": {"schemas": {}},
161+
"servers": [{"url": "/"}],
162+
}

0 commit comments

Comments
 (0)