Skip to content

Add Django 5.0 support #9233

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

Merged
merged 4 commits into from
Feb 28, 2024
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.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements

* 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
each Python and Django series.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following:

* 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
each Python and Django series.
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def get_version(package):
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
Expand Down
3 changes: 2 additions & 1 deletion tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1538,7 +1538,8 @@ class TestNoOutputFormatDateTimeField(FieldValues):
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.
"""
Expand Down
84 changes: 42 additions & 42 deletions tests/test_model_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import datetime
import decimal
import json # noqa
import re
import sys
import tempfile

Expand Down Expand Up @@ -169,53 +170,52 @@ class Meta:
model = RegularFieldsModel
fields = '__all__'

expected = dedent("""
TestSerializer():
auto_field = IntegerField(read_only=True)
big_integer_field = IntegerField()
boolean_field = BooleanField(default=False, required=False)
char_field = CharField(max_length=100)
comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>])
date_field = DateField()
datetime_field = DateTimeField()
decimal_field = DecimalField(decimal_places=1, max_digits=3)
email_field = EmailField(max_length=100)
float_field = FloatField()
integer_field = IntegerField()
null_boolean_field = BooleanField(allow_null=True, default=False, required=False)
positive_integer_field = IntegerField()
positive_small_integer_field = IntegerField()
slug_field = SlugField(allow_unicode=False, max_length=100)
small_integer_field = IntegerField()
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
file_field = FileField(max_length=100)
time_field = TimeField()
url_field = URLField(max_length=100)
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
file_path_field = FilePathField(path=%r)
expected = dedent(r"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usage of regex instead of old string format seems interesting to me. can you share more insight about it? pros and cons regarding this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The need arises from the integer validator generated from Django 5 integer fields. That validator breaks the assertion against the repr. In other tests, there is a conditional on Django version, an "if" to add the extra string to match. I find regex to be more flexible against changes that are not the object of the test - i.e. the correct functionality asserted from the repr in which the integer validator does not play a role. This would also prevent cluttering if, in future versions of Django, similar changes arrive (preventing the discussion for each version).

The cons I find would be the escaping cluttering in the strings because otherwise, it is pretty straight with the current testing suite ("the dedent of strings for testing the repr" way) and not harder to read.

TestSerializer\(\):
auto_field = IntegerField\(read_only=True\)
big_integer_field = IntegerField\(.*\)
boolean_field = BooleanField\(default=False, required=False\)
char_field = CharField\(max_length=100\)
comma_separated_integer_field = CharField\(max_length=100, validators=\[<django.core.validators.RegexValidator object>\]\)
date_field = DateField\(\)
datetime_field = DateTimeField\(\)
decimal_field = DecimalField\(decimal_places=1, max_digits=3\)
email_field = EmailField\(max_length=100\)
float_field = FloatField\(\)
integer_field = IntegerField\(.*\)
null_boolean_field = BooleanField\(allow_null=True, default=False, required=False\)
positive_integer_field = IntegerField\(.*\)
positive_small_integer_field = IntegerField\(.*\)
slug_field = SlugField\(allow_unicode=False, max_length=100\)
small_integer_field = IntegerField\(.*\)
text_field = CharField\(max_length=100, style={'base_template': 'textarea.html'}\)
file_field = FileField\(max_length=100\)
time_field = TimeField\(\)
url_field = URLField\(max_length=100\)
custom_field = ModelField\(model_field=<tests.test_model_serializer.CustomField: custom_field>\)
file_path_field = FilePathField\(path=%r\)
""" % tempfile.gettempdir())

self.assertEqual(repr(TestSerializer()), expected)
assert re.search(expected, repr(TestSerializer())) is not None

def test_field_options(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = FieldOptionsModel
fields = '__all__'

expected = dedent("""
TestSerializer():
id = IntegerField(label='ID', read_only=True)
value_limit_field = IntegerField(max_value=10, min_value=1)
length_limit_field = CharField(max_length=12, min_length=3)
blank_field = CharField(allow_blank=True, max_length=10, required=False)
null_field = IntegerField(allow_null=True, required=False)
default_field = IntegerField(default=0, required=False)
descriptive_field = IntegerField(help_text='Some help text', label='A label')
choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))
text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))
expected = dedent(r"""
TestSerializer\(\):
id = IntegerField\(label='ID', read_only=True\)
value_limit_field = IntegerField\(max_value=10, min_value=1\)
length_limit_field = CharField\(max_length=12, min_length=3\)
blank_field = CharField\(allow_blank=True, max_length=10, required=False\)
null_field = IntegerField\(allow_null=True,.*required=False\)
default_field = IntegerField\(default=0,.*required=False\)
descriptive_field = IntegerField\(help_text='Some help text', label='A label'.*\)
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):
class NullableBooleanChoicesModel(models.Model):
Expand Down Expand Up @@ -1334,12 +1334,12 @@ class Meta:
}
}

expected = dedent("""
TestSerializer():
number_field = IntegerField(source='integer_field')
expected = dedent(r"""
TestSerializer\(\):
number_field = IntegerField\(.*source='integer_field'\)
""")
self.maxDiff = None
self.assertEqual(repr(TestSerializer()), expected)
assert re.search(expected, repr(TestSerializer())) is not None


class Issue6110TestModel(models.Model):
Expand Down
85 changes: 47 additions & 38 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import datetime
import re
from unittest.mock import MagicMock, patch

import pytest
from django import VERSION as django_version
from django.db import DataError, models
from django.test import TestCase

Expand Down Expand Up @@ -112,11 +114,15 @@ def test_updated_instance_excluded(self):
def test_doesnt_pollute_model(self):
instance = AnotherUniquenessModel.objects.create(code='100')
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
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):
data = {'username': 'Existing', 'email': '[email protected]'}
Expand Down Expand Up @@ -193,15 +199,15 @@ def setUp(self):

def test_repr(self):
serializer = UniquenessTogetherSerializer()
expected = dedent("""
UniquenessTogetherSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100, required=True)
position = IntegerField(required=True)
expected = dedent(r"""
UniquenessTogetherSerializer\(\):
id = IntegerField\(label='ID', read_only=True\)
race_name = CharField\(max_length=100, required=True\)
position = IntegerField\(.*required=True\)
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):
"""
Expand Down Expand Up @@ -282,13 +288,13 @@ class Meta:
read_only_fields = ('race_name',)

serializer = ReadOnlyFieldSerializer()
expected = dedent("""
ReadOnlyFieldSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(read_only=True)
position = IntegerField(required=True)
expected = dedent(r"""
ReadOnlyFieldSerializer\(\):
id = IntegerField\(label='ID', read_only=True\)
race_name = CharField\(read_only=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):
"""
Expand Down Expand Up @@ -366,14 +372,14 @@ class Meta:
fields = ['name', 'position']

serializer = TestSerializer()
expected = dedent("""
TestSerializer():
name = CharField(source='race_name')
position = IntegerField()
expected = dedent(r"""
TestSerializer\(\):
name = CharField\(source='race_name'\)
position = IntegerField\(.*\)
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):
class TestSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -411,13 +417,13 @@ class Meta:
validators = []

serializer = NoValidatorsSerializer()
expected = dedent("""
NoValidatorsSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100)
position = IntegerField()
expected = dedent(r"""
NoValidatorsSerializer\(\):
id = IntegerField\(label='ID', read_only=True.*\)
race_name = CharField\(max_length=100\)
position = IntegerField\(.*\)
""")
assert repr(serializer) == expected
assert re.search(expected, repr(serializer)) is not None

def test_ignore_validation_for_null_fields(self):
# None values that are on fields which are part of the uniqueness
Expand Down Expand Up @@ -540,16 +546,16 @@ def test_repr(self):
# the order of validators isn't deterministic so delete
# fancy_conditions field that has two of them
del serializer.fields['fancy_conditions']
expected = dedent("""
UniqueConstraintSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100, required=True)
position = IntegerField(required=True)
global_id = IntegerField(validators=[<UniqueValidator(queryset=UniqueConstraintModel.objects.all())>])
expected = dedent(r"""
UniqueConstraintSerializer\(\):
id = IntegerField\(label='ID', read_only=True\)
race_name = CharField\(max_length=100, required=True\)
position = IntegerField\(.*required=True\)
global_id = IntegerField\(.*validators=\[<UniqueValidator\(queryset=UniqueConstraintModel.objects.all\(\)\)>\]\)
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):
"""
Expand All @@ -569,15 +575,18 @@ def test_single_field_uniq_validators(self):
UniqueConstraint with single field must be transformed into
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()
assert len(serializer.validators) == 1
validators = serializer.fields['global_id'].validators
assert len(validators) == 1
assert len(validators) == 1 + extra_validators_qty
assert validators[0].queryset == UniqueConstraintModel.objects

validators = serializer.fields['fancy_conditions'].validators
assert len(validators) == 2
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators}
assert len(validators) == 2 + extra_validators_qty
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])}


Expand Down
5 changes: 3 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ envlist =
{py36,py37,py38,py39}-django31
{py36,py37,py38,py39,py310}-django32
{py38,py39,py310}-{django40,django41,django42,djangomain}
{py311}-{django41,django42,djangomain}
{py312}-{django42,djangomain}
{py311}-{django41,django42,django50,djangomain}
{py312}-{django42,djanggo50,djangomain}
base
dist
docs
Expand All @@ -23,6 +23,7 @@ deps =
django40: Django>=4.0,<4.1
django41: Django>=4.1,<4.2
django42: Django>=4.2,<5.0
django50: Django>=5.0,<5.1
djangomain: https://github.com/django/django/archive/main.tar.gz
-rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt
Expand Down