Skip to content

Commit

Permalink
Merge pull request #1127 from tfranzel/django5
Browse files Browse the repository at this point in the history
add django 5 to test suite and adapt to changes #1126
  • Loading branch information
tfranzel committed Dec 10, 2023
2 parents f31238e + 8ecee1b commit 0ec4d23
Show file tree
Hide file tree
Showing 17 changed files with 106 additions and 52 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion drf_spectacular/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
Expand Down
9 changes: 9 additions & 0 deletions requirements/linting.txt
Original file line number Diff line number Diff line change
@@ -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
8 changes: 1 addition & 7 deletions requirements/testing.txt
Original file line number Diff line number Diff line change
@@ -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
pytest-cov>=2.8.1
12 changes: 12 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 []
5 changes: 3 additions & 2 deletions tests/contrib/test_rest_framework_gis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)


Expand Down
10 changes: 6 additions & 4 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
2 changes: 1 addition & 1 deletion tests/test_extend_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 8 additions & 3 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion tests/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
15 changes: 9 additions & 6 deletions tests/test_postprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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')])
Expand Down
8 changes: 4 additions & 4 deletions tests/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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', [
Expand Down Expand Up @@ -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
}

Expand Down
12 changes: 8 additions & 4 deletions tests/test_split.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
29 changes: 17 additions & 12 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -159,4 +161,7 @@ ignore_missing_imports = True
ignore_missing_imports = True

[mypy-pydantic.*]
ignore_missing_imports = True
ignore_missing_imports = True

[mypy-exceptiongroup.*]
ignore_missing_imports = True

0 comments on commit 0ec4d23

Please sign in to comment.