Merge branch 'master' into migrate_setuppy_to_pryoject.toml

This commit is contained in:
Mathieu Dupuy 2024-03-20 17:15:43 +01:00
commit 2770f5e17d
No known key found for this signature in database
GPG Key ID: 08C1D4F32506B23A
25 changed files with 293 additions and 179 deletions

13
.github/dependabot.yml vendored Normal file
View File

@ -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

View File

@ -25,7 +25,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: 'pip' cache: 'pip'
@ -62,9 +62,9 @@ jobs:
name: Test documentation links name: Test documentation links
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v5
with: with:
python-version: '3.9' python-version: '3.9'

View File

@ -11,14 +11,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-python@v4 - uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.10"
- uses: pre-commit/action@v3.0.0 - uses: pre-commit/action@v3.0.1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -25,3 +25,9 @@ repos:
exclude: ^(?!docs).*$ exclude: ^(?!docs).*$
additional_dependencies: additional_dependencies:
- black==23.1.0 - 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

View File

@ -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.

View File

@ -56,10 +56,11 @@ The following sections explain more.
### Install dependencies ### Install dependencies
pip install pyyaml uritemplate pip install pyyaml uritemplate inflection
* `pyyaml` is used to generate schema into YAML-based OpenAPI format. * `pyyaml` is used to generate schema into YAML-based OpenAPI format.
* `uritemplate` is used internally to get parameters in path. * `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 ### Generating a static schema with the `generateschema` management command

View File

@ -0,0 +1,58 @@
<style>
.promo li a {
float: left;
width: 130px;
height: 20px;
text-align: center;
margin: 10px 30px;
padding: 150px 0 0 0;
background-position: 0 50%;
background-size: 130px auto;
background-repeat: no-repeat;
font-size: 120%;
color: black;
}
.promo li {
list-style: none;
}
</style>
# 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.

View File

@ -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.x series
### 3.14.0 ### 3.14.0
@ -946,7 +1030,7 @@ See the [release announcement][3.6-release].
* description.py codes and tests removal. ([#4153][gh4153]) * description.py codes and tests removal. ([#4153][gh4153])
* Wrap guardian.VERSION in tuple. ([#4149][gh4149]) * Wrap guardian.VERSION in tuple. ([#4149][gh4149])
* Refine validator for fields with <source=> kwargs. ([#4146][gh4146]) * Refine validator for fields with <source=> 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]) * Resolve TimeField representation for midnight value. ([#4107][gh4107])
* Set proper status code in AdminRenderer for the redirection after POST/DELETE requests. ([#4106][gh4106]) * 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]) * TimeField render returns None instead of 00:00:00. ([#4105][gh4105])

View File

@ -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.

View File

@ -132,8 +132,6 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
path('', include(router.urls)), path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) 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. 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.

View File

@ -65,6 +65,7 @@ nav:
- 'Contributing to REST framework': 'community/contributing.md' - 'Contributing to REST framework': 'community/contributing.md'
- 'Project management': 'community/project-management.md' - 'Project management': 'community/project-management.md'
- 'Release Notes': 'community/release-notes.md' - 'Release Notes': 'community/release-notes.md'
- '3.15 Announcement': 'community/3.15-announcement.md'
- '3.14 Announcement': 'community/3.14-announcement.md' - '3.14 Announcement': 'community/3.14-announcement.md'
- '3.13 Announcement': 'community/3.13-announcement.md' - '3.13 Announcement': 'community/3.13-announcement.md'
- '3.12 Announcement': 'community/3.12-announcement.md' - '3.12 Announcement': 'community/3.12-announcement.md'

View File

@ -18,6 +18,7 @@ classifiers = [
"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",

View File

@ -10,7 +10,7 @@ ______ _____ _____ _____ __
import django import django
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '3.14.0' __version__ = '3.15.0'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 3-Clause' __license__ = 'BSD 3-Clause'
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd' __copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'

View File

@ -46,6 +46,12 @@ try:
except ImportError: except ImportError:
yaml = None yaml = None
# inflection is optional
try:
import inflection
except ImportError:
inflection = None
# requests is optional # requests is optional
try: try:

View File

@ -14,7 +14,7 @@ from django.utils.encoding import force_str
from rest_framework import ( from rest_framework import (
RemovedInDRF315Warning, exceptions, renderers, serializers 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.fields import _UnvalidatedField, empty
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -247,9 +247,8 @@ class AutoSchema(ViewInspector):
name = name[:-len(action)] name = name[:-len(action)]
if action == 'list': if action == 'list':
from inflection import pluralize assert inflection, '`inflection` must be installed for OpenAPI schema support.'
name = inflection.pluralize(name)
name = pluralize(name)
return name return name

View File

@ -603,12 +603,6 @@ class ListSerializer(BaseSerializer):
self.min_length = kwargs.pop('min_length', None) self.min_length = kwargs.pop('min_length', None)
assert self.child is not None, '`child` is a required argument.' assert self.child is not None, '`child` is a required argument.'
assert not inspect.isclass(self.child), '`child` has not been instantiated.' 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) super().__init__(*args, **kwargs)
self.child.bind(field_name='', parent=self) self.child.bind(field_name='', parent=self)
@ -694,13 +688,7 @@ class ListSerializer(BaseSerializer):
ret = [] ret = []
errors = [] errors = []
for idx, item in enumerate(data): for item in data:
if (
hasattr(self, 'instance')
and self.instance
and len(self.instance) > idx
):
self.child.instance = self.instance[idx]
try: try:
validated = self.run_child_validation(item) validated = self.run_child_validation(item)
except ValidationError as exc: except ValidationError as exc:

View File

@ -3,6 +3,12 @@ function replaceDocument(docString) {
doc.write(docString); doc.write(docString);
doc.close(); 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) { function doAjaxSubmit(e) {

View File

@ -27,8 +27,8 @@ def smart_repr(value):
if isinstance(value, models.Manager): if isinstance(value, models.Manager):
return manager_repr(value) return manager_repr(value)
if isinstance(value, Promise) and value._delegate_text: if isinstance(value, Promise):
value = force_str(value) value = force_str(value, strings_only=True)
value = repr(value) value = repr(value)

View File

@ -421,7 +421,7 @@ class APIView(View):
""" """
# Make the error obvious if a proper response is not returned # Make the error obvious if a proper response is not returned
assert isinstance(response, HttpResponseBase), ( 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`' 'to be returned from the view, but received a `%s`'
% type(response) % type(response)
) )

View File

@ -26,3 +26,8 @@ include = rest_framework/*,tests/*
exclude_lines = exclude_lines =
pragma: no cover pragma: no cover
raise NotImplementedError 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

View File

@ -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.
""" """

View File

@ -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):

View File

@ -2,7 +2,6 @@ import inspect
import pickle import pickle
import re import re
import sys import sys
import unittest
from collections import ChainMap from collections import ChainMap
from collections.abc import Mapping from collections.abc import Mapping
@ -784,63 +783,3 @@ class TestSetValueMethod:
ret = {'a': 1} ret = {'a': 1}
self.s.set_value(ret, ['x', 'y'], 2) self.s.set_value(ret, ['x', 'y'], 2)
assert ret == {'a': 1, '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,
)

View File

@ -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])}

View File

@ -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
@ -24,6 +24,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