diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..2d9e2c5ee --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Action updates into a single larger pull request + schedule: + interval: weekly diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 756b6d24b..c3c587cbf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -62,9 +62,9 @@ jobs: name: Test documentation links runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 36d356493..892235175 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -11,14 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.10" - - uses: pre-commit/action@v3.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} + - uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c37da7449..8939dd3db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,3 +25,9 @@ repos: exclude: ^(?!docs).*$ additional_dependencies: - black==23.1.0 +- repo: https://github.com/codespell-project/codespell + # Configuration for codespell is in .codespellrc + rev: v2.2.6 + hooks: + - id: codespell + exclude: locale|kickstarter-announcement.md|coreapi-0.1.1.js diff --git a/README.md b/README.md index 078ac0711..56933a4c8 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/api-guide/schemas.md b/docs/api-guide/schemas.md index 7af98dbf5..c387af972 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -56,10 +56,11 @@ The following sections explain more. ### Install dependencies - pip install pyyaml uritemplate + pip install pyyaml uritemplate inflection * `pyyaml` is used to generate schema into YAML-based OpenAPI format. * `uritemplate` is used internally to get parameters in path. +* `inflection` is used to pluralize operations more appropriately in the list endpoints. ### Generating a static schema with the `generateschema` management command diff --git a/docs/community/3.15-announcement.md b/docs/community/3.15-announcement.md new file mode 100644 index 000000000..5bcff6969 --- /dev/null +++ b/docs/community/3.15-announcement.md @@ -0,0 +1,58 @@ + + +# Django REST framework 3.15 + +At the Internet, on March 15th, 2024, with 176 commits by 138 authors, we are happy to announce the release of Django REST framework 3.15. + +## Django 5.0 and Python 3.12 support + +The latest release now fully supports Django 5.0 and Python 3.12. + +The current minimum versions of Django still is 3.0 and Python 3.6. + +## Primary Support of UniqueConstraint + +`ModelSerializer` generates validators for [UniqueConstraint](https://docs.djangoproject.com/en/4.0/ref/models/constraints/#uniqueconstraint) (both UniqueValidator and UniqueTogetherValidator) + +## ValidationErrors improvements + +The `ValidationError` has been aligned with Django's, currently supporting the same style (signature) and nesting. + +## SimpleRouter non-regex matching support + +By default the URLs created by `SimpleRouter` use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router. + +## ZoneInfo as the primary source of timezone data + +Dependency on pytz has been removed and deprecation warnings have been added, Django will provide ZoneInfo instances as long as USE_DEPRECATED_PYTZ is not enabled. More info on the migration can be found [in this guide](https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html). + +## Align `SearchFilter` behaviour to `django.contrib.admin` search + +Searches now may contain _quoted phrases_ with spaces, each phrase is considered as a single search term, and it will raise a validation error if any null-character is provided in search. See the [Filtering API guide](../api-guide/filtering.md) for more information. + +## Default values propagation + +Model fields' default values are now propagated to serializer fields, for more information see the [Serializer fields API guide](../api-guide/fields.md#default). + +## Other fixes and improvements + +There are a number of fixes and minor improvements in this release, ranging from documentation, internal infrastructure (typing, testing, requirements, deprecation, etc.), security and overall behaviour. + +See the [release notes](release-notes.md) page for a complete listing. diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 71f29e5c0..07220ef0f 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -34,6 +34,90 @@ You can determine your currently installed version using `pip show`: --- +## 3.15.x series + +### 3.15.0 + +Date: 15th March 2024 + +* Django 5.0 and Python 3.12 support [[#9157](https://github.com/encode/django-rest-framework/pull/9157)] +* Use POST method instead of GET to perform logout in browsable API [[9208](https://github.com/encode/django-rest-framework/pull/9208)] +* Added jQuery 3.7.1 support & dropped previous version [[#9094](https://github.com/encode/django-rest-framework/pull/9094)] +* Use str as default path converter [[#9066](https://github.com/encode/django-rest-framework/pull/9066)] +* Document support for http.HTTPMethod in the @action decorator added in Python 3.11 [[#9067](https://github.com/encode/django-rest-framework/pull/9067)] +* Update exceptions.md [[#9071](https://github.com/encode/django-rest-framework/pull/9071)] +* Partial serializer should not have required fields [[#7563](https://github.com/encode/django-rest-framework/pull/7563)] +* Propagate 'default' from model field to serializer field. [[#9030](https://github.com/encode/django-rest-framework/pull/9030)] +* Allow to override child.run_validation call in ListSerializer [[#8035](https://github.com/encode/django-rest-framework/pull/8035)] +* Align SearchFilter behaviour to django.contrib.admin search [[#9017](https://github.com/encode/django-rest-framework/pull/9017)] +* Class name added to unknown field error [[#9019](https://github.com/encode/django-rest-framework/pull/9019)] +* Fix: Pagination response schemas. [[#9049](https://github.com/encode/django-rest-framework/pull/9049)] +* Fix choices in ChoiceField to support IntEnum [[#8955](https://github.com/encode/django-rest-framework/pull/8955)] +* Fix `SearchFilter` rendering search field with invalid value [[#9023](https://github.com/encode/django-rest-framework/pull/9023)] +* Fix OpenAPI Schema yaml rendering for `timedelta` [[#9007](https://github.com/encode/django-rest-framework/pull/9007)] +* Fix `NamespaceVersioning` ignoring `DEFAULT_VERSION` on non-None namespaces [[#7278](https://github.com/encode/django-rest-framework/pull/7278)] +* Added Deprecation Warnings for CoreAPI [[#7519](https://github.com/encode/django-rest-framework/pull/7519)] +* Removed usage of `field.choices` that triggered full table load [[#8950](https://github.com/encode/django-rest-framework/pull/8950)] +* Permit mixed casing of string values for `BooleanField` validation [[#8970](https://github.com/encode/django-rest-framework/pull/8970)] +* Fixes `BrowsableAPIRenderer` for usage with `ListSerializer`. [[#7530](https://github.com/encode/django-rest-framework/pull/7530)] +* Change semantic of `OR` of two permission classes [[#7522](https://github.com/encode/django-rest-framework/pull/7522)] +* Remove dependency on `pytz` [[#8984](https://github.com/encode/django-rest-framework/pull/8984)] +* Make set_value a method within `Serializer` [[#8001](https://github.com/encode/django-rest-framework/pull/8001)] +* Fix URLPathVersioning reverse fallback [[#7247](https://github.com/encode/django-rest-framework/pull/7247)] +* Warn about Decimal type in min_value and max_value arguments of DecimalField [[#8972](https://github.com/encode/django-rest-framework/pull/8972)] +* Fix mapping for choice values [[#8968](https://github.com/encode/django-rest-framework/pull/8968)] +* Refactor read function to use context manager for file handling [[#8967](https://github.com/encode/django-rest-framework/pull/8967)] +* Fix: fallback on CursorPagination ordering if unset on the view [[#8954](https://github.com/encode/django-rest-framework/pull/8954)] +* Replaced `OrderedDict` with `dict` [[#8964](https://github.com/encode/django-rest-framework/pull/8964)] +* Refactor get_field_info method to include max_digits and decimal_places attributes in SimpleMetadata class [[#8943](https://github.com/encode/django-rest-framework/pull/8943)] +* Implement `__eq__` for validators [[#8925](https://github.com/encode/django-rest-framework/pull/8925)] +* Ensure CursorPagination respects nulls in the ordering field [[#8912](https://github.com/encode/django-rest-framework/pull/8912)] +* Use ZoneInfo as primary source of timezone data [[#8924](https://github.com/encode/django-rest-framework/pull/8924)] +* Add username search field for TokenAdmin (#8927) [[#8934](https://github.com/encode/django-rest-framework/pull/8934)] +* Handle Nested Relation in SlugRelatedField when many=False [[#8922](https://github.com/encode/django-rest-framework/pull/8922)] +* Bump version of jQuery to 3.6.4 & updated ref links [[#8909](https://github.com/encode/django-rest-framework/pull/8909)] +* Support UniqueConstraint [[#7438](https://github.com/encode/django-rest-framework/pull/7438)] +* Allow Request, Response, Field, and GenericAPIView to be subscriptable. This allows the classes to be made generic for type checking. [[#8825](https://github.com/encode/django-rest-framework/pull/8825)] +* Feat: Add some changes to ValidationError to support django style validation errors [[#8863](https://github.com/encode/django-rest-framework/pull/8863)] +* Fix Respect `can_read_model` permission in DjangoModelPermissions [[#8009](https://github.com/encode/django-rest-framework/pull/8009)] +* Add SimplePathRouter [[#6789](https://github.com/encode/django-rest-framework/pull/6789)] +* Re-prefetch related objects after updating [[#8043](https://github.com/encode/django-rest-framework/pull/8043)] +* Fix FilePathField required argument [[#8805](https://github.com/encode/django-rest-framework/pull/8805)] +* Raise ImproperlyConfigured exception if `basename` is not unique [[#8438](https://github.com/encode/django-rest-framework/pull/8438)] +* Use PrimaryKeyRelatedField pkfield in openapi [[#8315](https://github.com/encode/django-rest-framework/pull/8315)] +* replace partition with split in BasicAuthentication [[#8790](https://github.com/encode/django-rest-framework/pull/8790)] +* Fix BooleanField's allow_null behavior [[#8614](https://github.com/encode/django-rest-framework/pull/8614)] +* Handle Django's ValidationErrors in ListField [[#6423](https://github.com/encode/django-rest-framework/pull/6423)] +* Remove a bit of inline CSS. Add CSP nonce where it might be required and is available [[#8783](https://github.com/encode/django-rest-framework/pull/8783)] +* Use autocomplete widget for user selection in Token admin [[#8534](https://github.com/encode/django-rest-framework/pull/8534)] +* Make browsable API compatible with strong CSP [[#8784](https://github.com/encode/django-rest-framework/pull/8784)] +* Avoid inline script execution for injecting CSRF token [[#7016](https://github.com/encode/django-rest-framework/pull/7016)] +* Mitigate global dependency on inflection [[#8017](https://github.com/encode/django-rest-framework/pull/8017)] [[#8781](https://github.com/encode/django-rest-framework/pull/8781)] +* Register Django urls [[#8778](https://github.com/encode/django-rest-framework/pull/8778)] +* Implemented Verbose Name Translation for TokenProxy [[#8713](https://github.com/encode/django-rest-framework/pull/8713)] +* Properly handle OverflowError in DurationField deserialization [[#8042](https://github.com/encode/django-rest-framework/pull/8042)] +* Fix OpenAPI operation name plural appropriately [[#8017](https://github.com/encode/django-rest-framework/pull/8017)] +* Represent SafeString as plain string on schema rendering [[#8429](https://github.com/encode/django-rest-framework/pull/8429)] +* Fix #8771 - Checking for authentication even if `_ignore_model_permissions = True` [[#8772](https://github.com/encode/django-rest-framework/pull/8772)] +* Fix 404 when page query parameter is empty string [[#8578](https://github.com/encode/django-rest-framework/pull/8578)] +* Fixes instance check in ListSerializer.to_representation [[#8726](https://github.com/encode/django-rest-framework/pull/8726)] [[#8727](https://github.com/encode/django-rest-framework/pull/8727)] +* FloatField will crash if the input is a number that is too big [[#8725](https://github.com/encode/django-rest-framework/pull/8725)] +* Add missing DurationField to SimpleMetada label_lookup [[#8702](https://github.com/encode/django-rest-framework/pull/8702)] +* Add support for Python 3.11 [[#8752](https://github.com/encode/django-rest-framework/pull/8752)] +* Make request consistently available in pagination classes [[#8764](https://github.com/encode/django-rest-framework/pull/9764)] +* Possibility to remove trailing zeros on DecimalFields representation [[#6514](https://github.com/encode/django-rest-framework/pull/6514)] +* Add a method for getting serializer field name (OpenAPI) [[#7493](https://github.com/encode/django-rest-framework/pull/7493)] +* Add `__eq__` method for `OperandHolder` class [[#8710](https://github.com/encode/django-rest-framework/pull/8710)] +* Avoid importing `django.test` package when not testing [[#8699](https://github.com/encode/django-rest-framework/pull/8699)] +* Preserve exception messages for wrapped Django exceptions [[#8051](https://github.com/encode/django-rest-framework/pull/8051)] +* Include `examples` and `format` to OpenAPI schema of CursorPagination [[#8687](https://github.com/encode/django-rest-framework/pull/8687)] [[#8686](https://github.com/encode/django-rest-framework/pull/8686)] +* Fix infinite recursion with deepcopy on Request [[#8684](https://github.com/encode/django-rest-framework/pull/8684)] +* Refactor: Replace try/except with contextlib.suppress() [[#8676](https://github.com/encode/django-rest-framework/pull/8676)] +* Minor fix to SerializeMethodField docstring [[#8629](https://github.com/encode/django-rest-framework/pull/8629)] +* Minor refactor: Unnecessary use of list() function [[#8672](https://github.com/encode/django-rest-framework/pull/8672)] +* Unnecessary list comprehension [[#8670](https://github.com/encode/django-rest-framework/pull/8670)] +* Use correct class to indicate present deprecation [[#8665](https://github.com/encode/django-rest-framework/pull/8665)] + ## 3.14.x series ### 3.14.0 @@ -946,7 +1030,7 @@ See the [release announcement][3.6-release]. * description.py codes and tests removal. ([#4153][gh4153]) * Wrap guardian.VERSION in tuple. ([#4149][gh4149]) * Refine validator for fields with kwargs. ([#4146][gh4146]) -* Fix None values representation in childs of ListField, DictField. ([#4118][gh4118]) +* Fix None values representation in children of ListField, DictField. ([#4118][gh4118]) * Resolve TimeField representation for midnight value. ([#4107][gh4107]) * Set proper status code in AdminRenderer for the redirection after POST/DELETE requests. ([#4106][gh4106]) * TimeField render returns None instead of 00:00:00. ([#4105][gh4105]) diff --git a/docs/index.md b/docs/index.md index a7f1444a3..07d233107 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/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 9a9da4043..7b46a44e6 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -132,8 +132,6 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`... path('', include(router.urls)), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) ] - - urlpatterns += router.urls Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class. diff --git a/mkdocs.yml b/mkdocs.yml index dcef68987..79831fe95 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,6 +65,7 @@ nav: - 'Contributing to REST framework': 'community/contributing.md' - 'Project management': 'community/project-management.md' - 'Release Notes': 'community/release-notes.md' + - '3.15 Announcement': 'community/3.15-announcement.md' - '3.14 Announcement': 'community/3.14-announcement.md' - '3.13 Announcement': 'community/3.13-announcement.md' - '3.12 Announcement': 'community/3.12-announcement.md' diff --git a/pyproject.toml b/pyproject.toml index 538867dce..f6f47031c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ classifiers = [ "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/rest_framework/__init__.py b/rest_framework/__init__.py index b9e3f9817..45ff90980 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -10,7 +10,7 @@ ______ _____ _____ _____ __ import django __title__ = 'Django REST framework' -__version__ = '3.14.0' +__version__ = '3.15.0' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2023 Encode OSS Ltd' diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 472b8ad24..afc06b6cb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -46,6 +46,12 @@ try: except ImportError: yaml = None +# inflection is optional +try: + import inflection +except ImportError: + inflection = None + # requests is optional try: diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index c154494e2..38031e646 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -14,7 +14,7 @@ from django.utils.encoding import force_str from rest_framework import ( RemovedInDRF315Warning, exceptions, renderers, serializers ) -from rest_framework.compat import uritemplate +from rest_framework.compat import inflection, uritemplate from rest_framework.fields import _UnvalidatedField, empty from rest_framework.settings import api_settings @@ -247,9 +247,8 @@ class AutoSchema(ViewInspector): name = name[:-len(action)] if action == 'list': - from inflection import pluralize - - name = pluralize(name) + assert inflection, '`inflection` must be installed for OpenAPI schema support.' + name = inflection.pluralize(name) return name diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index eee18ddd4..b1b7b6477 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -603,12 +603,6 @@ class ListSerializer(BaseSerializer): self.min_length = kwargs.pop('min_length', None) assert self.child is not None, '`child` is a required argument.' assert not inspect.isclass(self.child), '`child` has not been instantiated.' - - instance = kwargs.get('instance', []) - data = kwargs.get('data', []) - if instance and data: - assert len(data) == len(instance), 'Data and instance should have same length' - super().__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) @@ -694,13 +688,7 @@ class ListSerializer(BaseSerializer): ret = [] errors = [] - for idx, item in enumerate(data): - if ( - hasattr(self, 'instance') - and self.instance - and len(self.instance) > idx - ): - self.child.instance = self.instance[idx] + for item in data: try: validated = self.run_child_validation(item) except ValidationError as exc: diff --git a/rest_framework/static/rest_framework/js/ajax-form.js b/rest_framework/static/rest_framework/js/ajax-form.js index 1483305ff..dda5454c2 100644 --- a/rest_framework/static/rest_framework/js/ajax-form.js +++ b/rest_framework/static/rest_framework/js/ajax-form.js @@ -3,6 +3,12 @@ function replaceDocument(docString) { doc.write(docString); doc.close(); + + if (window.djdt) { + // If Django Debug Toolbar is available, reinitialize it so that + // it can show updated panels from new `docString`. + window.addEventListener("load", djdt.init); + } } function doAjaxSubmit(e) { diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py index 6f2efee16..b24cc3c75 100644 --- a/rest_framework/utils/representation.py +++ b/rest_framework/utils/representation.py @@ -27,8 +27,8 @@ def smart_repr(value): if isinstance(value, models.Manager): return manager_repr(value) - if isinstance(value, Promise) and value._delegate_text: - value = force_str(value) + if isinstance(value, Promise): + value = force_str(value, strings_only=True) value = repr(value) diff --git a/rest_framework/views.py b/rest_framework/views.py index 4c30029fd..411c1ee38 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -421,7 +421,7 @@ class APIView(View): """ # Make the error obvious if a proper response is not returned assert isinstance(response, HttpResponseBase), ( - 'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` ' + 'Expected a `Response`, `HttpResponse` or `StreamingHttpResponse` ' 'to be returned from the view, but received a `%s`' % type(response) ) diff --git a/setup.cfg b/setup.cfg index 874b27acd..09d845ad9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,3 +26,8 @@ include = rest_framework/*,tests/* exclude_lines = pragma: no cover raise NotImplementedError + +[codespell] +# Ref: https://github.com/codespell-project/codespell#using-a-config-file +skip = */kickstarter-announcement.md,*.js,*.map,*.po +ignore-words-list = fo,malcom,ser diff --git a/tests/test_fields.py b/tests/test_fields.py index 7006d473c..2b6fc56ac 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 20d0319fc..5b6551a98 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -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=[]) - 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 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): diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 39d9238ef..10fa8afb9 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -2,7 +2,6 @@ import inspect import pickle import re import sys -import unittest from collections import ChainMap from collections.abc import Mapping @@ -784,63 +783,3 @@ class TestSetValueMethod: ret = {'a': 1} self.s.set_value(ret, ['x', 'y'], 2) assert ret == {'a': 1, 'x': {'y': 2}} - - -class MyClass(models.Model): - name = models.CharField(max_length=100) - value = models.CharField(max_length=100, blank=True) - - app_label = "test" - - @property - def is_valid(self): - return self.name == 'valid' - - -class MyClassSerializer(serializers.ModelSerializer): - class Meta: - model = MyClass - fields = ('id', 'name', 'value') - - def validate_value(self, value): - if value and not self.instance.is_valid: - raise serializers.ValidationError( - 'Status cannot be set for invalid instance') - return value - - -class TestMultipleObjectsValidation(unittest.TestCase): - def setUp(self): - self.objs = [ - MyClass(name='valid'), - MyClass(name='invalid'), - MyClass(name='other'), - ] - - def test_multiple_objects_are_validated_separately(self): - - serializer = MyClassSerializer( - data=[{'value': 'set', 'id': instance.id} for instance in - self.objs], - instance=self.objs, - many=True, - partial=True, - ) - - assert not serializer.is_valid() - assert serializer.errors == [ - {}, - {'value': ['Status cannot be set for invalid instance']}, - {'value': ['Status cannot be set for invalid instance']} - ] - - def test_exception_raised_when_data_and_instance_length_different(self): - - with self.assertRaises(AssertionError): - MyClassSerializer( - data=[{'value': 'set', 'id': instance.id} for instance in - self.objs], - instance=self.objs[:-1], - many=True, - partial=True, - ) diff --git a/tests/test_validators.py b/tests/test_validators.py index 49b0db63a..c38dc1134 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 @@ 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 = [] + 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 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 = [] + 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 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=[]) + 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 @@ 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])} diff --git a/tox.ini b/tox.ini index d4157860d..1fdb7fffb 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 @@ -24,6 +24,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