Skip to content

Commit c8ff5f3

Browse files
committed
Merge branch 'master' into labels
2 parents a7491d6 + 2f3f9d6 commit c8ff5f3

13 files changed

+112
-42
lines changed

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ In ``urls.py``:
153153
)
154154
155155
urlpatterns = [
156-
path('swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
156+
path('swagger.<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
157157
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
158158
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
159159
...

src/drf_yasg/app_settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
'DEFAULT_API_URL': None,
4242

4343
'USE_SESSION_AUTH': True,
44+
'USE_COMPAT_RENDERERS': getattr(settings, 'SWAGGER_USE_COMPAT_RENDERERS', True),
4445
'CSRF_COOKIE_NAME': settings.CSRF_COOKIE_NAME,
4546
'CSRF_HEADER_NAME': settings.CSRF_HEADER_NAME,
4647
'SECURITY_DEFINITIONS': {

src/drf_yasg/inspectors/field.py

+37-32
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from contextlib import suppress
77
from collections import OrderedDict
88
from decimal import Decimal
9-
from inspect import signature as inspect_signature
109

1110
import typing
1211
from django.core import validators
@@ -29,6 +28,14 @@
2928
import pkg_resources
3029
drf_version = pkg_resources.get_distribution("djangorestframework").version
3130

31+
try:
32+
from types import NoneType, UnionType
33+
34+
UNION_TYPES = (typing.Union, UnionType)
35+
except ImportError: # Python < 3.10
36+
NoneType = type(None)
37+
UNION_TYPES = (typing.Union,)
38+
3239
logger = logging.getLogger(__name__)
3340

3441

@@ -480,15 +487,6 @@ def decimal_return_type():
480487
return openapi.TYPE_STRING if rest_framework_settings.COERCE_DECIMAL_TO_STRING else openapi.TYPE_NUMBER
481488

482489

483-
def get_origin_type(hint_class):
484-
return getattr(hint_class, '__origin__', None) or hint_class
485-
486-
487-
def hint_class_issubclass(hint_class, check_class):
488-
origin_type = get_origin_type(hint_class)
489-
return inspect.isclass(origin_type) and issubclass(origin_type, check_class)
490-
491-
492490
hinting_type_info = [
493491
(bool, (openapi.TYPE_BOOLEAN, None)),
494492
(int, (openapi.TYPE_INTEGER, None)),
@@ -505,11 +503,15 @@ def hint_class_issubclass(hint_class, check_class):
505503
if hasattr(typing, 'get_args'):
506504
# python >=3.8
507505
typing_get_args = typing.get_args
506+
typing_get_origin = typing.get_origin
508507
else:
509508
# python <3.8
510509
def typing_get_args(tp):
511510
return getattr(tp, '__args__', ())
512511

512+
def typing_get_origin(tp):
513+
return getattr(tp, '__origin__', None)
514+
513515

514516
def inspect_collection_hint_class(hint_class):
515517
args = typing_get_args(hint_class)
@@ -525,12 +527,6 @@ def inspect_collection_hint_class(hint_class):
525527
hinting_type_info.append(((typing.Sequence, typing.AbstractSet), inspect_collection_hint_class))
526528

527529

528-
def _get_union_types(hint_class):
529-
origin_type = get_origin_type(hint_class)
530-
if origin_type is typing.Union:
531-
return hint_class.__args__
532-
533-
534530
def get_basic_type_info_from_hint(hint_class):
535531
"""Given a class (eg from a SerializerMethodField's return type hint,
536532
return its basic type information - ``type``, ``format``, ``pattern``,
@@ -540,21 +536,28 @@ def get_basic_type_info_from_hint(hint_class):
540536
:return: the extracted attributes as a dictionary, or ``None`` if the field type is not known
541537
:rtype: OrderedDict
542538
"""
543-
union_types = _get_union_types(hint_class)
544539

545-
if union_types:
540+
if typing_get_origin(hint_class) in UNION_TYPES:
546541
# Optional is implemented as Union[T, None]
547-
if len(union_types) == 2 and isinstance(None, union_types[1]):
548-
result = get_basic_type_info_from_hint(union_types[0])
542+
filtered_types = [t for t in typing_get_args(hint_class) if t is not NoneType]
543+
if len(filtered_types) == 1:
544+
result = get_basic_type_info_from_hint(filtered_types[0])
549545
if result:
550546
result['x-nullable'] = True
551547

552548
return result
553549

554550
return None
555551

552+
# resolve the origin class if the class is generic
553+
resolved_class = typing_get_origin(hint_class) or hint_class
554+
555+
# bail out early
556+
if not inspect.isclass(resolved_class):
557+
return None
558+
556559
for check_class, info in hinting_type_info:
557-
if hint_class_issubclass(hint_class, check_class):
560+
if issubclass(resolved_class, check_class):
558561
if callable(info):
559562
return info(hint_class)
560563

@@ -617,17 +620,19 @@ def field_to_swagger_object(self, field, swagger_object_type, use_references, **
617620
return self.probe_field_inspectors(serializer, swagger_object_type, use_references, read_only=True)
618621
else:
619622
# look for Python 3.5+ style type hinting of the return value
620-
hint_class = inspect_signature(method).return_annotation
621-
622-
if not inspect.isclass(hint_class) and hasattr(hint_class, '__args__'):
623-
hint_class = hint_class.__args__[0]
624-
if inspect.isclass(hint_class) and not issubclass(hint_class, inspect._empty):
625-
type_info = get_basic_type_info_from_hint(hint_class)
626-
627-
if type_info is not None:
628-
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type,
629-
use_references, **kwargs)
630-
return SwaggerType(**type_info)
623+
hint_class = typing.get_type_hints(method).get('return')
624+
625+
# annotations such as typing.Optional have an __instancecheck__
626+
# hook and will not look like classes, but `issubclass` needs
627+
# a class as its first argument, so only in that case abort
628+
if inspect.isclass(hint_class) and issubclass(hint_class, inspect._empty):
629+
return NotHandled
630+
631+
type_info = get_basic_type_info_from_hint(hint_class)
632+
if type_info is not None:
633+
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type,
634+
use_references, **kwargs)
635+
return SwaggerType(**type_info)
631636

632637
return NotHandled
633638

src/drf_yasg/renderers.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ class OpenAPIRenderer(_SpecRenderer):
4646
class SwaggerJSONRenderer(_SpecRenderer):
4747
"""Renders the schema as a JSON document with the generic ``application/json`` mime type."""
4848
media_type = 'application/json'
49-
format = '.json'
49+
format = 'json'
5050
codec_class = OpenAPICodecJson
5151

5252

5353
class SwaggerYAMLRenderer(_SpecRenderer):
5454
"""Renders the schema as a YAML document."""
5555
media_type = 'application/yaml'
56-
format = '.yaml'
56+
format = 'yaml'
5757
codec_class = OpenAPICodecYaml
5858

5959

src/drf_yasg/views.py

+16
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from .renderers import (
1414
ReDocOldRenderer,
1515
ReDocRenderer,
16+
SwaggerJSONRenderer,
1617
SwaggerUIRenderer,
18+
SwaggerYAMLRenderer,
1719
_SpecRenderer,
1820
)
1921

@@ -77,6 +79,20 @@ def get_schema_view(info=None, url=None, patterns=None, urlconf=None, public=Fal
7779
validators = validators or []
7880
_spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS)
7981

82+
# optionally copy renderers with the validators that are configured above
83+
if swagger_settings.USE_COMPAT_RENDERERS:
84+
warnings.warn(
85+
"SwaggerJSONRenderer & SwaggerYAMLRenderer's `format` has changed to not include a `.` prefix, "
86+
"please silence this warning by setting `SWAGGER_USE_COMPAT_RENDERERS = False` "
87+
"in your Django settings and ensure your application works "
88+
"(check your URLCONF and swagger/redoc URLs).",
89+
DeprecationWarning)
90+
_spec_renderers += tuple(
91+
type(cls.__name__, (cls,), {'format': '.' + cls.format})
92+
for cls in _spec_renderers
93+
if issubclass(cls, (SwaggerJSONRenderer, SwaggerYAMLRenderer))
94+
)
95+
8096
class SchemaView(APIView):
8197
_ignore_model_permissions = True
8298
schema = None # exclude from schema

testproj/testproj/settings/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
}
152152

153153
REDOC_SETTINGS = {
154-
'SPEC_URL': ('schema-json', {'format': '.json'}),
154+
'SPEC_URL': ('schema-json', {'format': 'json'}),
155155
}
156156

157157
# Internationalization

testproj/testproj/urls.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ def root_redirect(request):
5454
]
5555

5656
urlpatterns = [
57-
re_path(r'^swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=0),
57+
re_path(r'^swagger\.(?P<format>json|yaml)$', SchemaView.without_ui(cache_timeout=0),
5858
name='schema-json'),
5959
path('swagger/', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
6060
path('redoc/', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
6161
path('redoc-old/', SchemaView.with_ui('redoc-old', cache_timeout=0), name='schema-redoc-old'),
6262

63-
re_path(r'^cached/swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=None),
63+
re_path(r'^cached/swagger\.(?P<format>json|yaml)$', SchemaView.without_ui(cache_timeout=None),
6464
name='cschema-json'),
6565
path('cached/swagger/', SchemaView.with_ui('swagger', cache_timeout=None), name='cschema-swagger-ui'),
6666
path('cached/redoc/', SchemaView.with_ui('redoc', cache_timeout=None), name='cschema-redoc'),

tests/test_get_basic_type_info_from_hint.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@
1515
]
1616

1717

18+
python310_union_tests = []
19+
if sys.version_info >= (3, 10):
20+
# # New PEP 604 union syntax in Python 3.10+
21+
python310_union_tests = [
22+
(bool | None, {'type': openapi.TYPE_BOOLEAN, 'format': None, 'x-nullable': True}),
23+
(list[int] | None, {
24+
'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_INTEGER), 'x-nullable': True
25+
}),
26+
# Following cases are not 100% correct, but it should work somehow and not crash.
27+
(int | float, None),
28+
]
29+
30+
1831
@pytest.mark.parametrize('hint_class, expected_swagger_type_info', [
1932
(int, {'type': openapi.TYPE_INTEGER, 'format': None}),
2033
(str, {'type': openapi.TYPE_STRING, 'format': None}),
@@ -41,7 +54,7 @@
4154
(type('SomeType', (object,), {}), None),
4255
(None, None),
4356
(6, None),
44-
] + python39_generics_tests)
57+
] + python39_generics_tests + python310_union_tests)
4558
def test_get_basic_type_info_from_hint(hint_class, expected_swagger_type_info):
4659
type_info = get_basic_type_info_from_hint(hint_class)
4760
assert type_info == expected_swagger_type_info

tests/test_schema_generator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def retrieve(self, request, pk=None):
345345
)
346346
swagger = generator.get_schema(None, True)
347347
property_schema = swagger["definitions"]["OptionalMethod"]["properties"]["x"]
348-
assert property_schema == openapi.Schema(title='X', type=expected_type, readOnly=True)
348+
assert property_schema == openapi.Schema(title='X', type=expected_type, readOnly=True, x_nullable=True)
349349

350350

351351
EXPECTED_DESCRIPTION = """\

tests/test_schema_views.py

+24
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,30 @@ def test_redoc(client, validate_schema):
5353
_validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads)
5454

5555

56+
@pytest.mark.urls('urlconfs.legacy_renderer')
57+
@pytest.mark.parametrize('format', ('.json', '.yaml'))
58+
def test_swagger_ui_legacy_renderer(settings, client, validate_schema, format):
59+
settings.SWAGGER_SETTINGS = {
60+
**settings.SWAGGER_SETTINGS,
61+
'SPEC_URL': ('schema-json', {'format': format}),
62+
}
63+
64+
_validate_ui_schema_view(client, '/swagger/', 'swagger-ui-dist/swagger-ui-bundle.js')
65+
_validate_text_schema_view(client, validate_schema, '/swagger/?format=openapi', json.loads)
66+
67+
68+
@pytest.mark.urls('urlconfs.legacy_renderer')
69+
@pytest.mark.parametrize('format', ('.json', '.yaml'))
70+
def test_redoc_legacy_renderer(settings, client, validate_schema, format):
71+
settings.REDOC_SETTINGS = {
72+
**settings.REDOC_SETTINGS,
73+
'SPEC_URL': ('schema-json', {'format': format}),
74+
}
75+
76+
_validate_ui_schema_view(client, '/redoc/', 'redoc/redoc.min.js')
77+
_validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads)
78+
79+
5680
def test_caching(client, validate_schema):
5781
prev_schema = None
5882

tests/urlconfs/legacy_renderer.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.urls import path, re_path
2+
3+
from testproj.urls import required_urlpatterns, SchemaView
4+
5+
urlpatterns = [
6+
re_path(r'^swagger(?P<format>\.json|\.yaml)$', SchemaView.without_ui(cache_timeout=0),
7+
name='schema-json'),
8+
path('swagger/', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
9+
path('redoc/', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
10+
path('redoc-old/', SchemaView.with_ui('redoc-old', cache_timeout=0), name='schema-redoc-old'),
11+
] + required_urlpatterns

tests/urlconfs/ns_versioning.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class VersionedSchemaView(SchemaView):
1313

1414

1515
schema_patterns = [
16-
re_path(r'swagger(?P<format>.json|.yaml)$', VersionedSchemaView.without_ui(), name='ns-schema')
16+
re_path(r'swagger\.(?P<format>json|yaml)$', VersionedSchemaView.without_ui(), name='ns-schema')
1717
]
1818

1919

tests/urlconfs/url_versioning.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ class VersionedSchemaView(SchemaView):
4949
urlpatterns = required_urlpatterns + [
5050
re_path(VERSION_PREFIX_URL + r"snippets/$", SnippetList.as_view()),
5151
re_path(VERSION_PREFIX_URL + r"snippets_excluded/$", ExcludedSnippets.as_view()),
52-
re_path(VERSION_PREFIX_URL + r'swagger(?P<format>.json|.yaml)$', VersionedSchemaView.without_ui(),
52+
re_path(VERSION_PREFIX_URL + r'swagger\.(?P<format>json|yaml)$', VersionedSchemaView.without_ui(),
5353
name='vschema-json'),
5454
]

0 commit comments

Comments
 (0)