From d4016d8ec11e1783614142512de14eff0798b464 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Wed, 28 Feb 2024 07:52:22 -0300 Subject: [PATCH] Add Django 5.0 support (#9233) * Update tox.ini * Update tests for Django 5.0 * Update documentation * Update setup.py --- README.md | 2 +- docs/index.md | 2 +- setup.py | 1 + tests/test_fields.py | 3 +- tests/test_model_serializer.py | 84 ++++++++++++++++----------------- tests/test_validators.py | 85 +++++++++++++++++++--------------- tox.ini | 5 +- 7 files changed, 97 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 078ac07116..56933a4c8e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/index.md b/docs/index.md index a7f1444a32..07d2331076 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. diff --git a/setup.py b/setup.py index 682c6c491d..40898b6c15 100755 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tests/test_fields.py b/tests/test_fields.py index 7006d473c2..2b6fc56ac0 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -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. """ diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 20d0319fcb..5b6551a986 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -8,6 +8,7 @@ import datetime import decimal import json # noqa +import re import sys import tempfile @@ -169,33 +170,32 @@ 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=[]) - 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=) - file_path_field = FilePathField(path=%r) + expected = dedent(r""" + 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=\[\]\) + 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=\) + 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): @@ -203,19 +203,19 @@ 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): @@ -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): diff --git a/tests/test_validators.py b/tests/test_validators.py index 49b0db63a2..c38dc11345 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -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 @@ -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': 'new-email@example.com'} @@ -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 = [] + validators = \[\] """) - assert repr(serializer) == expected + assert re.search(expected, repr(serializer)) is not None def test_is_not_unique_together(self): """ @@ -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): """ @@ -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 = [] + validators = \[\] """) - 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): @@ -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 @@ -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=[]) + 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=\[\]\) class Meta: - validators = [, ]>, fields=('race_name', 'position'))>] + validators = \[, \]>, fields=\('race_name', 'position'\)\)>\] """) - assert repr(serializer) == expected + assert re.search(expected, repr(serializer)) is not None def test_unique_together_field(self): """ @@ -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])} diff --git a/tox.ini b/tox.ini index 38682615f8..ffcbd6729d 100644 --- a/tox.ini +++ b/tox.ini @@ -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 @@ -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