Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add django 5 to test suite and adapt to changes #1126 #1127

Merged
merged 3 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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