Add Django 5.0 support (#9233)

* Update tox.ini

* Update tests for Django 5.0

* Update documentation

* Update setup.py
This commit is contained in:
Rodrigo 2024-02-28 07:52:22 -03:00 committed by GitHub
parent a45432b54d
commit d4016d8ec1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 97 additions and 85 deletions

View File

@ -56,7 +56,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python 3.6+ * Python 3.6+
* Django 4.2, 4.1, 4.0, 3.2, 3.1, 3.0 * Django 5.0, 4.2, 4.1, 4.0, 3.2, 3.1, 3.0
We **highly recommend** and only officially support the latest patch release of We **highly recommend** and only officially support the latest patch release of
each Python and Django series. each Python and Django series.

View File

@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following: REST framework requires the following:
* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11) * Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11)
* Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2) * Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0)
We **highly recommend** and only officially support the latest patch release of We **highly recommend** and only officially support the latest patch release of
each Python and Django series. each Python and Django series.

View File

@ -96,6 +96,7 @@ setup(
'Framework :: Django :: 4.0', 'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1', 'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2', 'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',

View File

@ -1538,7 +1538,8 @@ class TestNoOutputFormatDateTimeField(FieldValues):
field = serializers.DateTimeField(format=None) field = serializers.DateTimeField(format=None)
class TestNaiveDateTimeField(FieldValues): @override_settings(TIME_ZONE='UTC', USE_TZ=False)
class TestNaiveDateTimeField(FieldValues, TestCase):
""" """
Valid and invalid values for `DateTimeField` with naive datetimes. Valid and invalid values for `DateTimeField` with naive datetimes.
""" """

View File

@ -8,6 +8,7 @@ an appropriate set of serializer fields for each case.
import datetime import datetime
import decimal import decimal
import json # noqa import json # noqa
import re
import sys import sys
import tempfile import tempfile
@ -169,33 +170,32 @@ class TestRegularFieldMappings(TestCase):
model = RegularFieldsModel model = RegularFieldsModel
fields = '__all__' fields = '__all__'
expected = dedent(""" expected = dedent(r"""
TestSerializer(): TestSerializer\(\):
auto_field = IntegerField(read_only=True) auto_field = IntegerField\(read_only=True\)
big_integer_field = IntegerField() big_integer_field = IntegerField\(.*\)
boolean_field = BooleanField(default=False, required=False) boolean_field = BooleanField\(default=False, required=False\)
char_field = CharField(max_length=100) char_field = CharField\(max_length=100\)
comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>]) comma_separated_integer_field = CharField\(max_length=100, validators=\[<django.core.validators.RegexValidator object>\]\)
date_field = DateField() date_field = DateField\(\)
datetime_field = DateTimeField() datetime_field = DateTimeField\(\)
decimal_field = DecimalField(decimal_places=1, max_digits=3) decimal_field = DecimalField\(decimal_places=1, max_digits=3\)
email_field = EmailField(max_length=100) email_field = EmailField\(max_length=100\)
float_field = FloatField() float_field = FloatField\(\)
integer_field = IntegerField() integer_field = IntegerField\(.*\)
null_boolean_field = BooleanField(allow_null=True, default=False, required=False) null_boolean_field = BooleanField\(allow_null=True, default=False, required=False\)
positive_integer_field = IntegerField() positive_integer_field = IntegerField\(.*\)
positive_small_integer_field = IntegerField() positive_small_integer_field = IntegerField\(.*\)
slug_field = SlugField(allow_unicode=False, max_length=100) slug_field = SlugField\(allow_unicode=False, max_length=100\)
small_integer_field = IntegerField() small_integer_field = IntegerField\(.*\)
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'}) text_field = CharField\(max_length=100, style={'base_template': 'textarea.html'}\)
file_field = FileField(max_length=100) file_field = FileField\(max_length=100\)
time_field = TimeField() time_field = TimeField\(\)
url_field = URLField(max_length=100) url_field = URLField\(max_length=100\)
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>) custom_field = ModelField\(model_field=<tests.test_model_serializer.CustomField: custom_field>\)
file_path_field = FilePathField(path=%r) file_path_field = FilePathField\(path=%r\)
""" % tempfile.gettempdir()) """ % tempfile.gettempdir())
assert re.search(expected, repr(TestSerializer())) is not None
self.assertEqual(repr(TestSerializer()), expected)
def test_field_options(self): def test_field_options(self):
class TestSerializer(serializers.ModelSerializer): class TestSerializer(serializers.ModelSerializer):
@ -203,19 +203,19 @@ class TestRegularFieldMappings(TestCase):
model = FieldOptionsModel model = FieldOptionsModel
fields = '__all__' fields = '__all__'
expected = dedent(""" expected = dedent(r"""
TestSerializer(): TestSerializer\(\):
id = IntegerField(label='ID', read_only=True) id = IntegerField\(label='ID', read_only=True\)
value_limit_field = IntegerField(max_value=10, min_value=1) value_limit_field = IntegerField\(max_value=10, min_value=1\)
length_limit_field = CharField(max_length=12, min_length=3) length_limit_field = CharField\(max_length=12, min_length=3\)
blank_field = CharField(allow_blank=True, max_length=10, required=False) blank_field = CharField\(allow_blank=True, max_length=10, required=False\)
null_field = IntegerField(allow_null=True, required=False) null_field = IntegerField\(allow_null=True,.*required=False\)
default_field = IntegerField(default=0, required=False) default_field = IntegerField\(default=0,.*required=False\)
descriptive_field = IntegerField(help_text='Some help text', label='A label') descriptive_field = IntegerField\(help_text='Some help text', label='A label'.*\)
choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) text_choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
""") """)
self.assertEqual(repr(TestSerializer()), expected) assert re.search(expected, repr(TestSerializer())) is not None
def test_nullable_boolean_field_choices(self): def test_nullable_boolean_field_choices(self):
class NullableBooleanChoicesModel(models.Model): class NullableBooleanChoicesModel(models.Model):
@ -1334,12 +1334,12 @@ class TestFieldSource(TestCase):
} }
} }
expected = dedent(""" expected = dedent(r"""
TestSerializer(): TestSerializer\(\):
number_field = IntegerField(source='integer_field') number_field = IntegerField\(.*source='integer_field'\)
""") """)
self.maxDiff = None self.maxDiff = None
self.assertEqual(repr(TestSerializer()), expected) assert re.search(expected, repr(TestSerializer())) is not None
class Issue6110TestModel(models.Model): class Issue6110TestModel(models.Model):

View File

@ -1,7 +1,9 @@
import datetime import datetime
import re
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from django import VERSION as django_version
from django.db import DataError, models from django.db import DataError, models
from django.test import TestCase from django.test import TestCase
@ -112,11 +114,15 @@ class TestUniquenessValidation(TestCase):
def test_doesnt_pollute_model(self): def test_doesnt_pollute_model(self):
instance = AnotherUniquenessModel.objects.create(code='100') instance = AnotherUniquenessModel.objects.create(code='100')
serializer = AnotherUniquenessSerializer(instance) serializer = AnotherUniquenessSerializer(instance)
assert AnotherUniquenessModel._meta.get_field('code').validators == [] assert all(
["Unique" not in repr(v) for v in AnotherUniquenessModel._meta.get_field('code').validators]
)
# Accessing data shouldn't effect validators on the model # Accessing data shouldn't effect validators on the model
serializer.data serializer.data
assert AnotherUniquenessModel._meta.get_field('code').validators == [] assert all(
["Unique" not in repr(v) for v in AnotherUniquenessModel._meta.get_field('code').validators]
)
def test_related_model_is_unique(self): def test_related_model_is_unique(self):
data = {'username': 'Existing', 'email': 'new-email@example.com'} data = {'username': 'Existing', 'email': 'new-email@example.com'}
@ -193,15 +199,15 @@ class TestUniquenessTogetherValidation(TestCase):
def test_repr(self): def test_repr(self):
serializer = UniquenessTogetherSerializer() serializer = UniquenessTogetherSerializer()
expected = dedent(""" expected = dedent(r"""
UniquenessTogetherSerializer(): UniquenessTogetherSerializer\(\):
id = IntegerField(label='ID', read_only=True) id = IntegerField\(label='ID', read_only=True\)
race_name = CharField(max_length=100, required=True) race_name = CharField\(max_length=100, required=True\)
position = IntegerField(required=True) position = IntegerField\(.*required=True\)
class Meta: class Meta:
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>] validators = \[<UniqueTogetherValidator\(queryset=UniquenessTogetherModel.objects.all\(\), fields=\('race_name', 'position'\)\)>\]
""") """)
assert repr(serializer) == expected assert re.search(expected, repr(serializer)) is not None
def test_is_not_unique_together(self): def test_is_not_unique_together(self):
""" """
@ -282,13 +288,13 @@ class TestUniquenessTogetherValidation(TestCase):
read_only_fields = ('race_name',) read_only_fields = ('race_name',)
serializer = ReadOnlyFieldSerializer() serializer = ReadOnlyFieldSerializer()
expected = dedent(""" expected = dedent(r"""
ReadOnlyFieldSerializer(): ReadOnlyFieldSerializer\(\):
id = IntegerField(label='ID', read_only=True) id = IntegerField\(label='ID', read_only=True\)
race_name = CharField(read_only=True) race_name = CharField\(read_only=True\)
position = IntegerField(required=True) position = IntegerField\(.*required=True\)
""") """)
assert repr(serializer) == expected assert re.search(expected, repr(serializer)) is not None
def test_read_only_fields_with_default(self): def test_read_only_fields_with_default(self):
""" """
@ -366,14 +372,14 @@ class TestUniquenessTogetherValidation(TestCase):
fields = ['name', 'position'] fields = ['name', 'position']
serializer = TestSerializer() serializer = TestSerializer()
expected = dedent(""" expected = dedent(r"""
TestSerializer(): TestSerializer\(\):
name = CharField(source='race_name') name = CharField\(source='race_name'\)
position = IntegerField() position = IntegerField\(.*\)
class Meta: class Meta:
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('name', 'position'))>] validators = \[<UniqueTogetherValidator\(queryset=UniquenessTogetherModel.objects.all\(\), fields=\('name', 'position'\)\)>\]
""") """)
assert repr(serializer) == expected assert re.search(expected, repr(serializer)) is not None
def test_default_validator_with_multiple_fields_with_same_source(self): def test_default_validator_with_multiple_fields_with_same_source(self):
class TestSerializer(serializers.ModelSerializer): class TestSerializer(serializers.ModelSerializer):
@ -411,13 +417,13 @@ class TestUniquenessTogetherValidation(TestCase):
validators = [] validators = []
serializer = NoValidatorsSerializer() serializer = NoValidatorsSerializer()
expected = dedent(""" expected = dedent(r"""
NoValidatorsSerializer(): NoValidatorsSerializer\(\):
id = IntegerField(label='ID', read_only=True) id = IntegerField\(label='ID', read_only=True.*\)
race_name = CharField(max_length=100) race_name = CharField\(max_length=100\)
position = IntegerField() position = IntegerField\(.*\)
""") """)
assert repr(serializer) == expected assert re.search(expected, repr(serializer)) is not None
def test_ignore_validation_for_null_fields(self): def test_ignore_validation_for_null_fields(self):
# None values that are on fields which are part of the uniqueness # None values that are on fields which are part of the uniqueness
@ -540,16 +546,16 @@ class TestUniqueConstraintValidation(TestCase):
# the order of validators isn't deterministic so delete # the order of validators isn't deterministic so delete
# fancy_conditions field that has two of them # fancy_conditions field that has two of them
del serializer.fields['fancy_conditions'] del serializer.fields['fancy_conditions']
expected = dedent(""" expected = dedent(r"""
UniqueConstraintSerializer(): UniqueConstraintSerializer\(\):
id = IntegerField(label='ID', read_only=True) id = IntegerField\(label='ID', read_only=True\)
race_name = CharField(max_length=100, required=True) race_name = CharField\(max_length=100, required=True\)
position = IntegerField(required=True) position = IntegerField\(.*required=True\)
global_id = IntegerField(validators=[<UniqueValidator(queryset=UniqueConstraintModel.objects.all())>]) global_id = IntegerField\(.*validators=\[<UniqueValidator\(queryset=UniqueConstraintModel.objects.all\(\)\)>\]\)
class Meta: class Meta:
validators = [<UniqueTogetherValidator(queryset=<QuerySet [<UniqueConstraintModel: UniqueConstraintModel object (1)>, <UniqueConstraintModel: UniqueConstraintModel object (2)>]>, fields=('race_name', 'position'))>] validators = \[<UniqueTogetherValidator\(queryset=<QuerySet \[<UniqueConstraintModel: UniqueConstraintModel object \(1\)>, <UniqueConstraintModel: UniqueConstraintModel object \(2\)>\]>, fields=\('race_name', 'position'\)\)>\]
""") """)
assert repr(serializer) == expected assert re.search(expected, repr(serializer)) is not None
def test_unique_together_field(self): def test_unique_together_field(self):
""" """
@ -569,15 +575,18 @@ class TestUniqueConstraintValidation(TestCase):
UniqueConstraint with single field must be transformed into UniqueConstraint with single field must be transformed into
field's UniqueValidator field's UniqueValidator
""" """
# Django 5 includes Max and Min values validators for IntergerField
extra_validators_qty = 2 if django_version[0] >= 5 else 0
#
serializer = UniqueConstraintSerializer() serializer = UniqueConstraintSerializer()
assert len(serializer.validators) == 1 assert len(serializer.validators) == 1
validators = serializer.fields['global_id'].validators validators = serializer.fields['global_id'].validators
assert len(validators) == 1 assert len(validators) == 1 + extra_validators_qty
assert validators[0].queryset == UniqueConstraintModel.objects assert validators[0].queryset == UniqueConstraintModel.objects
validators = serializer.fields['fancy_conditions'].validators validators = serializer.fields['fancy_conditions'].validators
assert len(validators) == 2 assert len(validators) == 2 + extra_validators_qty
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators} ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
assert ids_in_qs == {frozenset([1]), frozenset([3])} assert ids_in_qs == {frozenset([1]), frozenset([3])}

View File

@ -4,8 +4,8 @@ envlist =
{py36,py37,py38,py39}-django31 {py36,py37,py38,py39}-django31
{py36,py37,py38,py39,py310}-django32 {py36,py37,py38,py39,py310}-django32
{py38,py39,py310}-{django40,django41,django42,djangomain} {py38,py39,py310}-{django40,django41,django42,djangomain}
{py311}-{django41,django42,djangomain} {py311}-{django41,django42,django50,djangomain}
{py312}-{django42,djangomain} {py312}-{django42,djanggo50,djangomain}
base base
dist dist
docs docs
@ -23,6 +23,7 @@ deps =
django40: Django>=4.0,<4.1 django40: Django>=4.0,<4.1
django41: Django>=4.1,<4.2 django41: Django>=4.1,<4.2
django42: Django>=4.2,<5.0 django42: Django>=4.2,<5.0
django50: Django>=5.0,<5.1
djangomain: https://github.com/django/django/archive/main.tar.gz djangomain: https://github.com/django/django/archive/main.tar.gz
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt -rrequirements/requirements-optionals.txt