Skip to content

Commit cae52b1

Browse files
committed
Improve test support across python versions
1 parent 3dd579e commit cae52b1

File tree

4 files changed

+130
-32
lines changed

4 files changed

+130
-32
lines changed

ninja/signature/details.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -221,21 +221,26 @@ def _get_param_type(self, name: str, arg: inspect.Parameter) -> FuncParam:
221221
annotation = arg.annotation
222222
default = arg.default
223223

224-
# TODO: Remove version check once support for <=3.8 is dropped.
225-
# Annotated[] is only available in 3.9+ per
226-
# https://docs.python.org/3/library/typing.html#typing.Annotated
227-
if get_origin(annotation) is Annotated and version_info >= (3, 9):
224+
if get_origin(annotation) is Annotated:
228225
args = get_args(annotation)
229226
if isinstance(args[-1], Param):
230227
prev_default = default
231228
if len(args) == 2:
232229
annotation, default = args
233230
else:
234-
# NOTE: Annotated[args[:-1]] seems to have the same runtime
235-
# behavior as Annotated[*args[:-1]], but the latter is
236-
# invalid in Python < 3.11 because star expressions
237-
# were not allowed in index expressions.
238-
annotation, default = Annotated[args[:-1]], args[-1]
231+
# TODO: Remove version check once support for <=3.8 is dropped.
232+
# Annotated[] is only available at runtime in 3.9+ per
233+
# https://docs.python.org/3/library/typing.html#typing.Annotated
234+
if version_info >= (3, 9):
235+
# NOTE: Annotated[args[:-1]] seems to have the same runtime
236+
# behavior as Annotated[*args[:-1]], but the latter is
237+
# invalid in Python < 3.11 because star expressions
238+
# were not allowed in index expressions.
239+
annotation, default = Annotated[args[:-1]], args[-1]
240+
else:
241+
raise NotImplementedError(
242+
"This definition requires Python version 3.9+"
243+
)
239244
if prev_default != self.signature.empty:
240245
default.default = prev_default
241246

tests/main.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from sys import version_info
12
from typing import List, Optional
23
from uuid import UUID
34

45
import pydantic
6+
import pytest
57
from django.urls import register_converter
68
from typing_extensions import Annotated
79

@@ -55,12 +57,29 @@ def custom_validator(value: int) -> int:
5557
}),
5658
]
5759

58-
59-
@router.get("/path/param_ex/{item_id}")
60-
def get_path_param_ex_id(
61-
request, item_id: PathEx[CustomValidatedInt, P(description="path_ex description")]
62-
):
63-
return item_id
60+
# TODO: Remove this condition once support for <= 3.8 is dropped
61+
if version_info >= (3, 9):
62+
63+
@router.get("/path/param_ex/{item_id}")
64+
def get_path_param_ex_id(
65+
request,
66+
item_id: PathEx[CustomValidatedInt, P(description="path_ex description")],
67+
):
68+
return item_id
69+
70+
else:
71+
72+
def test_annotated_path_ex_unsupported():
73+
with pytest.raises(NotImplementedError, match="3.9+"):
74+
75+
@router.get("/path/param_ex/{item_id}")
76+
def get_path_param_ex_id(
77+
request,
78+
item_id: PathEx[
79+
CustomValidatedInt, P(description="path_ex description")
80+
],
81+
):
82+
return item_id
6483

6584

6685
@router.get("/path/param/{item_id}")

tests/test_openapi_schema.py

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import sys
2+
from sys import version_info
23
from typing import Any, List, Union
34
from unittest.mock import Mock
4-
from typing_extensions import Annotated
55

66
import pydantic
77
import pytest
88
from django.contrib.admin.views.decorators import staff_member_required
99
from django.test import Client, override_settings
10+
from typing_extensions import Annotated
1011

1112
from ninja import (
1213
Body,
@@ -42,11 +43,13 @@ class TypeB(Schema):
4243

4344
AnnotatedStr = Annotated[
4445
str,
45-
pydantic.WithJsonSchema({
46-
"type": "string",
47-
"format": "custom-format",
48-
"example": "example_string",
49-
}),
46+
pydantic.WithJsonSchema(
47+
{
48+
"type": "string",
49+
"format": "custom-format",
50+
"example": "example_string",
51+
}
52+
),
5053
]
5154

5255

@@ -86,19 +89,43 @@ def method_body_schema(request, data: Payload):
8689
return dict(i=data.i, f=data.f)
8790

8891

89-
@api.get("/test-path/{int:i}/{f}/{path_ex}", response=Response)
92+
@api.get("/test-path/{int:i}/{f}", response=Response)
9093
def method_path(
9194
request,
9295
i: int,
9396
f: float,
94-
path_ex: PathEx[
95-
AnnotatedStr,
96-
P(description="path_ex description"),
97-
],
9897
):
9998
return dict(i=i, f=f)
10099

101100

101+
# This definition is only possible in Python 3.9+
102+
# TODO: Drop this condition once support for <= 3.8 is dropped
103+
if version_info >= (3, 9):
104+
105+
@api.get("/test-pathex/{path_ex}", response=AnnotatedStr)
106+
def method_pathex(
107+
request,
108+
path_ex: PathEx[
109+
AnnotatedStr,
110+
P(description="path_ex description"),
111+
],
112+
):
113+
return path_ex
114+
115+
else:
116+
with pytest.raises(NotImplementedError, match="3.9+"):
117+
118+
@api.get("/test-pathex/{path_ex}", response=AnnotatedStr)
119+
def method_pathex(
120+
request,
121+
path_ex: PathEx[
122+
AnnotatedStr,
123+
P(description="path_ex description"),
124+
],
125+
):
126+
return path_ex
127+
128+
102129
@api.post("/test-form", response=Response)
103130
def method_form(request, data: Payload = Form(...)):
104131
return dict(i=data.i, f=data.f)
@@ -433,7 +460,7 @@ def test_schema_body_schema(schema):
433460

434461

435462
def test_schema_path(schema):
436-
method_list = schema["paths"]["/api/test-path/{i}/{f}/{path_ex}"]["get"]
463+
method_list = schema["paths"]["/api/test-path/{i}/{f}"]["get"]
437464

438465
assert "requestBody" not in method_list
439466

@@ -450,6 +477,30 @@ def test_schema_path(schema):
450477
"schema": {"title": "F", "type": "number"},
451478
"required": True,
452479
},
480+
]
481+
482+
assert method_list["responses"] == {
483+
200: {
484+
"content": {
485+
"application/json": {
486+
"schema": {"$ref": "#/components/schemas/Response"},
487+
},
488+
},
489+
"description": "OK",
490+
}
491+
}
492+
493+
494+
@pytest.mark.skipif(
495+
version_info < (3, 9),
496+
reason="requires py3.9+ for Annotated[] at the route definition site",
497+
)
498+
def test_schema_pathex(schema):
499+
method_list = schema["paths"]["/api/test-pathex/{path_ex}"]["get"]
500+
501+
assert "requestBody" not in method_list
502+
503+
assert method_list["parameters"] == [
453504
{
454505
"in": "path",
455506
"name": "path_ex",
@@ -470,7 +521,12 @@ def test_schema_path(schema):
470521
200: {
471522
"content": {
472523
"application/json": {
473-
"schema": {"$ref": "#/components/schemas/Response"},
524+
"schema": {
525+
"example": "example_string",
526+
"format": "custom-format",
527+
"title": "Response",
528+
"type": "string",
529+
},
474530
},
475531
},
476532
"description": "OK",
@@ -650,7 +706,7 @@ def test_schema_title_description(schema):
650706
"schema": {
651707
"properties": {
652708
"file": {
653-
"description": "file " "param " "desc",
709+
"description": "file param desc",
654710
"format": "binary",
655711
"title": "File",
656712
"type": "string",

tests/test_path.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from sys import version_info
2+
13
import pytest
24
from main import router
35

@@ -221,9 +223,6 @@ def test_text_get():
221223
("/path/bool/true", 200, True),
222224
("/path/bool/False", 200, False),
223225
("/path/bool/false", 200, False),
224-
("/path/param_ex/True", 422, response_not_valid_int),
225-
("/path/param_ex/0", 422, response_not_valid_custom),
226-
("/path/param_ex/42", 200, 42),
227226
("/path/param/foo", 200, "foo"),
228227
("/path/param-required/foo", 200, "foo"),
229228
("/path/param-minlength/foo", 200, "foo"),
@@ -288,6 +287,25 @@ def test_get_path(path, expected_status, expected_response):
288287
assert response.json() == expected_response
289288

290289

290+
@pytest.mark.skipif(
291+
version_info < (3, 9),
292+
reason="requires py3.9+ for Annotated[] at the route definition site",
293+
)
294+
@pytest.mark.parametrize(
295+
"path,expected_status,expected_response",
296+
[
297+
("/path/param_ex/True", 422, response_not_valid_int),
298+
("/path/param_ex/0", 422, response_not_valid_custom),
299+
("/path/param_ex/42", 200, 42),
300+
],
301+
)
302+
def test_get_pathex(path, expected_status, expected_response):
303+
response = client.get(path)
304+
print(path, response.json())
305+
assert response.status_code == expected_status
306+
assert response.json() == expected_response
307+
308+
291309
@pytest.mark.parametrize(
292310
"path,expected_status,expected_response",
293311
[

0 commit comments

Comments
 (0)