diff --git a/README.rst b/README.rst index d3bc88c9..c25967fb 100644 --- a/README.rst +++ b/README.rst @@ -61,7 +61,7 @@ Requirements ------------ - Python >= 3.6 -- Django (2.2, 3.2, 4.0, 4.1, 4.2) +- Django (2.2, 3.2, 4.0, 4.1, 4.2, 5.0) - Django REST Framework (3.10.3, 3.11, 3.12, 3.13, 3.14) Installation diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index 5fbbe677..3a31252b 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -1344,11 +1344,12 @@ class TmpView(views.APIView): ) view.kwargs = {} # prepare AutoSchema with "init" values as if get_operation() was called - view.schema.registry = registry - view.schema.path = path - view.schema.path_regex = path - view.schema.path_prefix = '' - view.schema.method = method.upper() + schema: Any = view.schema + schema.registry = registry + schema.path = path + schema.path_regex = path + schema.path_prefix = '' + schema.method = method.upper() return view diff --git a/drf_spectacular/views.py b/drf_spectacular/views.py index 95f0f975..15c49b3e 100644 --- a/drf_spectacular/views.py +++ b/drf_spectacular/views.py @@ -46,7 +46,7 @@ class SpectacularAPIView(APIView): - YAML: application/vnd.oai.openapi - JSON: application/vnd.oai.openapi+json - """) + """) # type: ignore renderer_classes = [ OpenApiYamlRenderer, OpenApiYamlRenderer2, OpenApiJsonRenderer, OpenApiJsonRenderer2 ] diff --git a/requirements/linting.txt b/requirements/linting.txt new file mode 100644 index 00000000..12ea858a --- /dev/null +++ b/requirements/linting.txt @@ -0,0 +1,9 @@ +pytest # required for mypy to succeed +flake8 +isort==5.12.0 # 5.13 somehow breaks django-stubs plugin +mypy==1.7.1 +django-stubs==4.2.3 +djangorestframework-stubs==3.14.2 + +Django==4.2.7 +djangorestframework==3.14.0 \ No newline at end of file diff --git a/requirements/testing.txt b/requirements/testing.txt index 53fc0ce0..7454c418 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,9 +1,3 @@ pytest>=5.3.5 pytest-django>=3.8.0 -pytest-cov>=2.8.1 -flake8>=3.7.9 -mypy>=0.770 -django-stubs>=1.8.0,<1.10.0 -djangorestframework-stubs>=1.1.0 -types-PyYAML>=0.1.6 -isort>=5.0.4 \ No newline at end of file +pytest-cov>=2.8.1 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index 89030ff1..75b2496a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -113,3 +113,15 @@ def is_gis_installed(): return False else: return True + + +def strip_int64_details(schema): + """ remove new min/max/format for django 5 with sqlite db for comparison’s sake """ + + if schema.get('format') == 'int64' and 'minimum' in schema and 'maximum' in schema: + return { + k: v for k, v in schema.items() + if k not in ('format', 'minimum', 'maximum') + } + else: + return schema diff --git a/tests/conftest.py b/tests/conftest.py index c46e4892..3a919bf0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,10 @@ import os +import re from importlib import import_module import django import pytest +from django import __version__ as DJANGO_VERSION from django.core import management from tests import is_gis_installed @@ -191,3 +193,19 @@ def module_available(module_str): return False else: return True + + +@pytest.fixture() +def django_transforms(): + def integer_field_sqlite(s): + return re.sub( + r' *maximum: 9223372036854775807\n *minimum: (-9223372036854775808|0)\n *format: int64\n', + '', + s, + flags=re.M + ) + + if DJANGO_VERSION >= '5': + return [integer_field_sqlite] + else: + return [] diff --git a/tests/contrib/test_rest_framework_gis.py b/tests/contrib/test_rest_framework_gis.py index 5704ead5..5fa1582b 100644 --- a/tests/contrib/test_rest_framework_gis.py +++ b/tests/contrib/test_rest_framework_gis.py @@ -13,7 +13,7 @@ @pytest.mark.system_requirement_fulfilled(is_gis_installed()) @pytest.mark.skipif(DRF_VERSION < '3.12', reason='DRF pagination schema broken') @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', {}) -def test_rest_framework_gis(no_warnings, clear_caches): +def test_rest_framework_gis(no_warnings, clear_caches, django_transforms): from django.contrib.gis.db.models import ( GeometryCollectionField, GeometryField, LineStringField, MultiLineStringField, MultiPointField, MultiPolygonField, PointField, PolygonField, @@ -86,7 +86,8 @@ class PlainViewset(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.Ge assert_schema( generate_schema(None, patterns=router.urls), - 'tests/contrib/test_rest_framework_gis.yml' + 'tests/contrib/test_rest_framework_gis.yml', + transforms=django_transforms, ) diff --git a/tests/test_basic.py b/tests/test_basic.py index ce07fc33..7f194c66 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -75,16 +75,18 @@ def create(self, request, *args, **kwargs): return super().create(request, *args, **kwargs) # pragma: no cover -def test_basic(no_warnings): +def test_basic(no_warnings, django_transforms): assert_schema( generate_schema('albums', AlbumModelViewset), - 'tests/test_basic.yml' + 'tests/test_basic.yml', + transforms=django_transforms, ) @mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0') -def test_basic_oas_3_1(no_warnings): +def test_basic_oas_3_1(no_warnings, django_transforms): assert_schema( generate_schema('albums', AlbumModelViewset), - 'tests/test_basic_oas_3_1.yml' + 'tests/test_basic_oas_3_1.yml', + transforms=django_transforms, ) diff --git a/tests/test_extend_schema.py b/tests/test_extend_schema.py index 213e2571..a9c64fe0 100644 --- a/tests/test_extend_schema.py +++ b/tests/test_extend_schema.py @@ -58,7 +58,7 @@ class QuerySerializer(serializers.Serializer): ) order_by = serializers.MultipleChoiceField( choices=['a', 'b', 'c'], - default=['a'], + default=['a'], # type: ignore ) tag = serializers.CharField(required=False) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index d32f3bd3..a12f0469 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -126,7 +126,7 @@ def get(self, request): @api_view() -def x_view_function(): +def x_view_function(request): """ underspecified library view """ return Response(1.234) # pragma: no cover diff --git a/tests/test_fields.py b/tests/test_fields.py index dd6bac8f..033a09ae 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -296,19 +296,21 @@ class AuxModelViewset(viewsets.ReadOnlyModelViewSet): @pytest.mark.urls(__name__) -def test_fields(no_warnings): +def test_fields(no_warnings, django_transforms): assert_schema( SchemaGenerator().get_schema(request=None, public=True), - 'tests/test_fields.yml' + 'tests/test_fields.yml', + transforms=django_transforms, ) @pytest.mark.urls(__name__) @mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0') -def test_fields_oas_3_1(no_warnings): +def test_fields_oas_3_1(no_warnings, django_transforms): assert_schema( SchemaGenerator().get_schema(request=None, public=True), 'tests/test_fields_oas_3_1.yml', + transforms=django_transforms, ) @@ -371,4 +373,7 @@ def test_model_setup_is_valid(): else: expected['field_file'] = f'http://testserver/{m.field_file.name}' + if DJANGO_VERSION >= '5': + expected['field_datetime'] = '2021-09-09T10:15:26.049862-05:00' + assert_equal(json.loads(response.content), expected) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 1265609b..0864dd9c 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -32,7 +32,7 @@ class Meta: class XViewset(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): __doc__ = _(""" More lengthy explanation of the view - """) + """) # type: ignore serializer_class = XSerializer queryset = I18nModel.objects.none() diff --git a/tests/test_postprocessing.py b/tests/test_postprocessing.py index 0435c558..f671a3b2 100644 --- a/tests/test_postprocessing.py +++ b/tests/test_postprocessing.py @@ -72,11 +72,13 @@ class BlankNullLanguageStrEnum(str, Enum): NULL = None -class BlankNullLanguageChoices(TextChoices): - EN = 'en' - BLANK = '' - # These will still be included since the values get cast to strings so 'None' != None - NULL = None +if '3' < DJANGO_VERSION < '5': + # Django 5 added a sanity check that prohibits None + class BlankNullLanguageChoices(TextChoices): + EN = 'en' + BLANK = '' + # These will still be included since the values get cast to strings so 'None' != None + NULL = None class ASerializer(serializers.Serializer): @@ -272,7 +274,8 @@ def test_enum_override_variations_with_blank_and_null(no_warnings): ('BlankNullLanguageEnum', [('en', 'EN')]), ('BlankNullLanguageStrEnum', [('en', 'EN'), ('None', 'NULL')]) ] - if DJANGO_VERSION > '3': + if '3' < DJANGO_VERSION < '5': + # Django 5 added a sanity check that prohibits None enum_override_variations += [ ('BlankNullLanguageChoices', [('en', 'En'), ('None', 'Null')]), ('BlankNullLanguageChoices.choices', [('en', 'En'), ('None', 'Null')]) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index b3a68617..9979b119 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -32,7 +32,7 @@ OpenApiExample, OpenApiParameter, OpenApiRequest, OpenApiResponse, extend_schema, extend_schema_field, extend_schema_serializer, extend_schema_view, inline_serializer, ) -from tests import generate_schema, get_request_schema, get_response_schema +from tests import generate_schema, get_request_schema, get_response_schema, strip_int64_details from tests.models import SimpleModel, SimpleSerializer @@ -2026,7 +2026,7 @@ class RouteNestedViewset(viewsets.ModelViewSet): assert operation['parameters'][0]['name'] == 'client_pk' assert operation['parameters'][0]['schema'] == {'format': 'uuid', 'type': 'string'} assert operation['parameters'][2]['name'] == 'maildrop_pk' - assert operation['parameters'][2]['schema'] == {'type': 'integer'} + assert operation['parameters'][2]['schema']['type'] == 'integer' @pytest.mark.parametrize('value', [ @@ -2322,10 +2322,10 @@ class XViewset(viewsets.ModelViewSet): queryset = M8Model.objects.all() schema = generate_schema('x', XViewset) - assert schema['components']['schemas']['X']['properties']['field'] == { + assert strip_int64_details(schema['components']['schemas']['X']['properties']['field']) == { 'type': 'integer', 'default': 3 } - assert schema['components']['schemas']['X']['properties']['field_smf'] == { + assert strip_int64_details(schema['components']['schemas']['X']['properties']['field_smf']) == { 'type': 'integer', 'readOnly': True, 'default': 4 } diff --git a/tests/test_split.py b/tests/test_split.py index a5c45ad4..33ade7f3 100644 --- a/tests/test_split.py +++ b/tests/test_split.py @@ -37,18 +37,22 @@ class XViewset(mixins.UpdateModelMixin, viewsets.GenericViewSet): @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', False) -def test_nested_partial_on_split_request_false(no_warnings): +def test_nested_partial_on_split_request_false(no_warnings, django_transforms): # without split request, PatchedY and Y have the same properties (minus required). # PATCH only modifies outermost serializer, nested serializers must stay unaffected. assert_schema( - generate_schema('x', XViewset), 'tests/test_split_request_false.yml' + generate_schema('x', XViewset), + 'tests/test_split_request_false.yml', + transforms=django_transforms ) @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) -def test_nested_partial_on_split_request_true(no_warnings): +def test_nested_partial_on_split_request_true(no_warnings, django_transforms): # with split request, behaves like above, however response schemas are always unpatched. # nested request serializers are only affected by their manual partial flag and not due to PATCH. assert_schema( - generate_schema('x', XViewset), 'tests/test_split_request_true.yml' + generate_schema('x', XViewset), + 'tests/test_split_request_true.yml', + transforms=django_transforms, ) diff --git a/tox.ini b/tox.ini index 358fb193..7286a549 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,15 @@ [tox] envlist = - py39-lint,py39-docs, + py311-lint,py311-docs, {py36,py37,py38}-django{2.2}-drf{3.10,3.11}, {py37,py38,py39}-django{3.2}-drf{3.11,3.12}, {py38,py39,py310}-django{4.0,4.1}-drf{3.13,3.14}, - {py311}-django{4.1, 4.2}-drf{3.14}, - {py312}-django{4.2}-drf{3.14}, - py310-django4.2-drfmaster - py310-djangomaster-drf3.14 - py310-drfmaster-djangomaster - py310-drfmaster-djangomaster-allowcontribfail + {py311}-django{4.1, 4.2, 5.0}-drf{3.14}, + {py312}-django{4.2, 5.0}-drf{3.14}, + py311-django5.0-drfmaster + py311-djangomaster-drf3.14 + py311-drfmaster-djangomaster + py311-drfmaster-djangomaster-allowcontribfail skip_missing_interpreters = true [testenv] @@ -24,6 +24,7 @@ deps = django4.0: Django>=4.0,<4.1 django4.1: Django>=4.1,<4.2 django4.2: Django>=4.2,<4.3 + django5.0: Django>=5.0,<5.1 drf3.10: djangorestframework>=3.10,<3.11 drf3.11: djangorestframework>=3.11,<3.12 @@ -37,15 +38,16 @@ deps = -r requirements/testing.txt -r requirements/optionals.txt -[testenv:py310-drfmaster-djangomaster-allowcontribfail] +[testenv:py311-drfmaster-djangomaster-allowcontribfail] commands = python runtests.py {posargs:--fast --cov=drf_spectacular --cov=tests --cov-report=xml --allow-contrib-fail} -[testenv:py39-lint] +[testenv:py311-lint] commands = python runtests.py --lintonly deps = - -r requirements/testing.txt + -r requirements/base.txt + -r requirements/linting.txt -[testenv:py39-docs] +[testenv:py311-docs] commands = sphinx-build -WEa -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html deps = -r requirements/docs.txt @@ -159,4 +161,7 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-pydantic.*] -ignore_missing_imports = True \ No newline at end of file +ignore_missing_imports = True + +[mypy-exceptiongroup.*] +ignore_missing_imports = True