mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-11-04 09:57:55 +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
 | 
			
		||||
 | 
			
		||||
* 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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -96,6 +96,7 @@ setup(
 | 
			
		|||
        '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',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ an appropriate set of serializer fields for each case.
 | 
			
		|||
import datetime
 | 
			
		||||
import decimal
 | 
			
		||||
import json  # noqa
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
import tempfile
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -169,33 +170,32 @@ class TestRegularFieldMappings(TestCase):
 | 
			
		|||
                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"""
 | 
			
		||||
            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):
 | 
			
		||||
| 
						 | 
				
			
			@ -203,19 +203,19 @@ class TestRegularFieldMappings(TestCase):
 | 
			
		|||
                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 TestFieldSource(TestCase):
 | 
			
		|||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 @@ class TestUniquenessValidation(TestCase):
 | 
			
		|||
    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 @@ class TestUniquenessTogetherValidation(TestCase):
 | 
			
		|||
 | 
			
		||||
    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):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -282,13 +288,13 @@ class TestUniquenessTogetherValidation(TestCase):
 | 
			
		|||
                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 TestUniquenessTogetherValidation(TestCase):
 | 
			
		|||
                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):
 | 
			
		||||
| 
						 | 
				
			
			@ -411,13 +417,13 @@ class TestUniquenessTogetherValidation(TestCase):
 | 
			
		|||
                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 @@ class TestUniqueConstraintValidation(TestCase):
 | 
			
		|||
        # 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):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -569,15 +575,18 @@ class TestUniqueConstraintValidation(TestCase):
 | 
			
		|||
        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])}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										5
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user