mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 01:26:53 +03:00
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:
parent
a45432b54d
commit
d4016d8ec1
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -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',
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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])}
|
||||||
|
|
||||||
|
|
||||||
|
|
5
tox.ini
5
tox.ini
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user