Merge branch 'master' into sponsor-update

This commit is contained in:
Tom Christie 2019-06-04 11:30:47 +01:00
commit 78c37f3b00
152 changed files with 2684 additions and 2012 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: https://fund.django-rest-framework.org/topics/funding/

View File

@ -4,10 +4,6 @@ dist: xenial
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- { python: "2.7", env: DJANGO=1.11 }
- { python: "3.4", env: DJANGO=1.11 }
- { python: "3.4", env: DJANGO=2.0 }
- { python: "3.5", env: DJANGO=1.11 } - { python: "3.5", env: DJANGO=1.11 }
- { python: "3.5", env: DJANGO=2.0 } - { python: "3.5", env: DJANGO=2.0 }
@ -26,8 +22,8 @@ matrix:
- { python: "3.7", env: DJANGO=master } - { python: "3.7", env: DJANGO=master }
- { python: "3.7", env: TOXENV=base } - { python: "3.7", env: TOXENV=base }
- { python: "2.7", env: TOXENV=lint } - { python: "3.7", env: TOXENV=lint }
- { python: "2.7", env: TOXENV=docs } - { python: "3.7", env: TOXENV=docs }
- python: "3.7" - python: "3.7"
env: TOXENV=dist env: TOXENV=dist

1
CHANGELOG.md Symbolic link
View File

@ -0,0 +1 @@
docs/community/release-notes.md

View File

@ -59,7 +59,7 @@ Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recom
To run the tests, clone the repository, and then: To run the tests, clone the repository, and then:
# Setup the virtual environment # Setup the virtual environment
virtualenv env python3 -m venv env
source env/bin/activate source env/bin/activate
pip install django pip install django
pip install -r requirements.txt pip install -r requirements.txt
@ -115,7 +115,7 @@ It's also useful to remember that if you have an outstanding pull request then p
GitHub's documentation for working on pull requests is [available here][pull-requests]. GitHub's documentation for working on pull requests is [available here][pull-requests].
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django. Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible on all supported versions of Python and Django.
Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect. Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.

View File

@ -53,7 +53,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (2.7, 3.4, 3.5, 3.6, 3.7) * Python (3.5, 3.6, 3.7)
* Django (1.11, 2.0, 2.1, 2.2) * Django (1.11, 2.0, 2.1, 2.2)
We **highly recommend** and only officially support the latest patch release of We **highly recommend** and only officially support the latest patch release of
@ -175,9 +175,7 @@ You may also want to [follow the author on Twitter][twitter].
# Security # Security
If you believe you've found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**. Please see the [security policy][security-policy].
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
[build-status-image]: https://secure.travis-ci.org/encode/django-rest-framework.svg?branch=master [build-status-image]: https://secure.travis-ci.org/encode/django-rest-framework.svg?branch=master
[travis]: https://travis-ci.org/encode/django-rest-framework?branch=master [travis]: https://travis-ci.org/encode/django-rest-framework?branch=master
@ -199,7 +197,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
[cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png [cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png
[load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png [load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png
[kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png [kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png
[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history-readme.png [esg-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/esg-readme.png
[lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png [lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png
[sentry-url]: https://getsentry.com/welcome/ [sentry-url]: https://getsentry.com/welcome/
@ -223,4 +221,4 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
[image]: https://www.django-rest-framework.org/img/quickstart.png [image]: https://www.django-rest-framework.org/img/quickstart.png
[docs]: https://www.django-rest-framework.org/ [docs]: https://www.django-rest-framework.org/
[security-mail]: mailto:rest-framework-security@googlegroups.com [security-policy]: https://github.com/encode/django-rest-framework/security/policy

9
SECURITY.md Normal file
View File

@ -0,0 +1,9 @@
# Security Policy
## Reporting a Vulnerability
If you believe you've found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
[security-mail]: mailto:rest-framework-security@googlegroups.com

View File

@ -354,7 +354,7 @@ The following third party packages are also available.
## Django OAuth Toolkit ## Django OAuth Toolkit
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**. The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
#### Installation & configuration #### Installation & configuration

View File

@ -306,10 +306,11 @@ A date and time representation.
Corresponds to `django.db.models.fields.DateTimeField`. Corresponds to `django.db.models.fields.DateTimeField`.
**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)` **Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None, default_timezone=None)`
* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer. * `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer.
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
* `default_timezone` - A `pytz.timezone` representing the timezone. If not specified and the `USE_TZ` setting is enabled, this defaults to the [current timezone][django-current-timezone]. If `USE_TZ` is disabled, then datetime objects will be naive.
#### `DateTimeField` format strings. #### `DateTimeField` format strings.
@ -447,9 +448,10 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is
A field class that validates a list of objects. A field class that validates a list of objects.
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, min_length=None, max_length=None)` **Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None)`
- `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated. - `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
- `allow_empty` - Designates if empty lists are allowed.
- `min_length` - Validates that the list contains no fewer than this number of elements. - `min_length` - Validates that the list contains no fewer than this number of elements.
- `max_length` - Validates that the list contains no more than this number of elements. - `max_length` - Validates that the list contains no more than this number of elements.
@ -470,9 +472,10 @@ We can now reuse our custom `StringListField` class throughout our application,
A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values. A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.
**Signature**: `DictField(child=<A_FIELD_INSTANCE>)` **Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True)`
- `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated. - `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated.
- `allow_empty` - Designates if empty dictionaries are allowed.
For example, to create a field that validates a mapping of strings to strings, you would write something like this: For example, to create a field that validates a mapping of strings to strings, you would write something like this:
@ -487,9 +490,10 @@ You can also use the declarative style, as with `ListField`. For example:
A preconfigured `DictField` that is compatible with Django's postgres `HStoreField`. A preconfigured `DictField` that is compatible with Django's postgres `HStoreField`.
**Signature**: `HStoreField(child=<A_FIELD_INSTANCE>)` **Signature**: `HStoreField(child=<A_FIELD_INSTANCE>, allow_empty=True)`
- `child` - A field instance that is used for validating the values in the dictionary. The default child field accepts both empty strings and null values. - `child` - A field instance that is used for validating the values in the dictionary. The default child field accepts both empty strings and null values.
- `allow_empty` - Designates if empty dictionaries are allowed.
Note that the child field **must** be an instance of `CharField`, as the hstore extension stores values as strings. Note that the child field **must** be an instance of `CharField`, as the hstore extension stores values as strings.
@ -497,9 +501,10 @@ Note that the child field **must** be an instance of `CharField`, as the hstore
A field class that validates that the incoming data structure consists of valid JSON primitives. In its alternate binary mode, it will represent and validate JSON-encoded binary strings. A field class that validates that the incoming data structure consists of valid JSON primitives. In its alternate binary mode, it will represent and validate JSON-encoded binary strings.
**Signature**: `JSONField(binary)` **Signature**: `JSONField(binary, encoder)`
- `binary` - If set to `True` then the field will output and validate a JSON encoded string, rather than a primitive data structure. Defaults to `False`. - `binary` - If set to `True` then the field will output and validate a JSON encoded string, rather than a primitive data structure. Defaults to `False`.
- `encoder` - Use this JSON encoder to serialize input object. Defaults to `None`.
--- ---
@ -628,7 +633,7 @@ Our `ColorField` class above currently does not perform any data validation.
To indicate invalid data, we should raise a `serializers.ValidationError`, like so: To indicate invalid data, we should raise a `serializers.ValidationError`, like so:
def to_internal_value(self, data): def to_internal_value(self, data):
if not isinstance(data, six.text_type): if not isinstance(data, str):
msg = 'Incorrect type. Expected a string, but got %s' msg = 'Incorrect type. Expected a string, but got %s'
raise ValidationError(msg % type(data).__name__) raise ValidationError(msg % type(data).__name__)
@ -652,7 +657,7 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me
} }
def to_internal_value(self, data): def to_internal_value(self, data):
if not isinstance(data, six.text_type): if not isinstance(data, str):
self.fail('incorrect_type', input_type=type(data).__name__) self.fail('incorrect_type', input_type=type(data).__name__)
if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data): if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data):
@ -835,3 +840,4 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide
[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
[django-hstore]: https://github.com/djangonauts/django-hstore [django-hstore]: https://github.com/djangonauts/django-hstore
[python-decimal-rounding-modes]: https://docs.python.org/3/library/decimal.html#rounding-modes [python-decimal-rounding-modes]: https://docs.python.org/3/library/decimal.html#rounding-modes
[django-current-timezone]: https://docs.djangoproject.com/en/stable/topics/i18n/timezones/#default-time-zone-and-current-time-zone

View File

@ -51,7 +51,7 @@ For example:
--- ---
**Note**: With the exception of `DjangoObjectPermissions`, the provided **Note**: With the exception of `DjangoObjectPermissions`, the provided
permission classes in `rest_framework.permssions` **do not** implement the permission classes in `rest_framework.permissions` **do not** implement the
methods necessary to check object permissions. methods necessary to check object permissions.
If you wish to use the provided permission classes in order to check object If you wish to use the provided permission classes in order to check object

View File

@ -576,6 +576,8 @@ If you explicitly specify a relational field pointing to a
``ManyToManyField`` with a through model, be sure to set ``read_only`` ``ManyToManyField`` with a through model, be sure to set ``read_only``
to ``True``. to ``True``.
If you wish to represent [extra fields on a through model][django-intermediary-manytomany] then you may serialize the through model as [a nested object][dealing-with-nested-objects].
--- ---
# Third Party Packages # Third Party Packages
@ -596,3 +598,5 @@ The [rest-framework-generic-relations][drf-nested-relations] library provides re
[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1 [generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
[drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations [drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations
[django-intermediary-manytomany]: https://docs.djangoproject.com/en/2.2/topics/db/models/#intermediary-manytomany
[dealing-with-nested-objects]: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects

View File

@ -534,7 +534,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[messagepack]: https://msgpack.org/ [messagepack]: https://msgpack.org/
[juanriaza]: https://github.com/juanriaza [juanriaza]: https://github.com/juanriaza
[mjumbewu]: https://github.com/mjumbewu [mjumbewu]: https://github.com/mjumbewu
[flipperpa]: https://githuc.com/flipperpa [flipperpa]: https://github.com/flipperpa
[wharton]: https://github.com/wharton [wharton]: https://github.com/wharton
[drf-renderer-xlsx]: https://github.com/wharton/drf-renderer-xlsx [drf-renderer-xlsx]: https://github.com/wharton/drf-renderer-xlsx
[vbabiy]: https://github.com/vbabiy [vbabiy]: https://github.com/vbabiy

View File

@ -20,7 +20,7 @@ can render the schema into the commonly used YAML-based OpenAPI format.
## Quickstart ## Quickstart
There are two different ways you can serve a schema description for you API. There are two different ways you can serve a schema description for your API.
### Generating a schema with the `generateschema` management command ### Generating a schema with the `generateschema` management command

View File

@ -572,6 +572,8 @@ This option is a dictionary, mapping field names to a dictionary of keyword argu
user.save() user.save()
return user return user
Please keep in mind that, if the field has already been explicitly declared on the serializer class, then the `extra_kwargs` option will be ignored.
## Relational fields ## Relational fields
When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for `ModelSerializer` is to use the primary keys of the related instances. When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for `ModelSerializer` is to use the primary keys of the related instances.
@ -624,7 +626,7 @@ The default implementation returns a serializer class based on the `serializer_f
Called to generate a serializer field that maps to a relational model field. Called to generate a serializer field that maps to a relational model field.
The default implementation returns a serializer class based on the `serializer_relational_field` attribute. The default implementation returns a serializer class based on the `serializer_related_field` attribute.
The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties. The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties.
@ -963,7 +965,7 @@ The following class is an example of a generic serializer that can handle coerci
def to_representation(self, obj): def to_representation(self, obj):
for attribute_name in dir(obj): for attribute_name in dir(obj):
attribute = getattr(obj, attribute_name) attribute = getattr(obj, attribute_name)
if attribute_name('_'): if attribute_name.startswith('_'):
# Ignore private attributes. # Ignore private attributes.
pass pass
elif hasattr(attribute, '__call__'): elif hasattr(attribute, '__call__'):

View File

@ -100,7 +100,7 @@ The validator should be applied to *serializer classes*, like so:
--- ---
**Note**: The `UniqueTogetherValidation` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input. **Note**: The `UniqueTogetherValidator` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
--- ---
@ -159,7 +159,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden
--- ---
**Note**: The `UniqueFor<Range>Validation` classes impose an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input. **Note**: The `UniqueFor<Range>Validator` classes impose an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
--- ---

View File

@ -523,7 +523,7 @@ The following class is an example of a generic serializer that can handle coerci
def to_representation(self, obj): def to_representation(self, obj):
for attribute_name in dir(obj): for attribute_name in dir(obj):
attribute = getattr(obj, attribute_name) attribute = getattr(obj, attribute_name)
if attribute_name('_'): if attribute_name.startswith('_'):
# Ignore private attributes. # Ignore private attributes.
pass pass
elif hasattr(attribute, '__call__'): elif hasattr(attribute, '__call__'):

View File

@ -65,7 +65,7 @@ Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recom
To run the tests, clone the repository, and then: To run the tests, clone the repository, and then:
# Setup the virtual environment # Setup the virtual environment
virtualenv env python3 -m venv env
source env/bin/activate source env/bin/activate
pip install django pip install django
pip install -r requirements.txt pip install -r requirements.txt
@ -121,7 +121,7 @@ It's also useful to remember that if you have an outstanding pull request then p
GitHub's documentation for working on pull requests is [available here][pull-requests]. GitHub's documentation for working on pull requests is [available here][pull-requests].
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django. Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible on all supported versions of Python and Django.
Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect. Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.

View File

@ -9,6 +9,7 @@ Looking for a new Django REST Framework related role? On this site we provide a
* [https://www.python.org/jobs/][python-org-jobs] * [https://www.python.org/jobs/][python-org-jobs]
* [https://djangogigs.com][django-gigs-com] * [https://djangogigs.com][django-gigs-com]
* [https://djangojobs.net/jobs/][django-jobs-net] * [https://djangojobs.net/jobs/][django-jobs-net]
* [https://findwork.dev/django-rest-framework-jobs][findwork-dev]
* [https://www.indeed.com/q-Django-jobs.html][indeed-com] * [https://www.indeed.com/q-Django-jobs.html][indeed-com]
* [https://stackoverflow.com/jobs/developer-jobs-using-django][stackoverflow-com] * [https://stackoverflow.com/jobs/developer-jobs-using-django][stackoverflow-com]
* [https://www.upwork.com/o/jobs/browse/skill/django-framework/][upwork-com] * [https://www.upwork.com/o/jobs/browse/skill/django-framework/][upwork-com]
@ -26,6 +27,7 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram
[python-org-jobs]: https://www.python.org/jobs/ [python-org-jobs]: https://www.python.org/jobs/
[django-gigs-com]: https://djangogigs.com [django-gigs-com]: https://djangogigs.com
[django-jobs-net]: https://djangojobs.net/jobs/ [django-jobs-net]: https://djangojobs.net/jobs/
[findwork-dev]: https://findwork.dev/django-rest-framework-jobs
[indeed-com]: https://www.indeed.com/q-Django-jobs.html [indeed-com]: https://www.indeed.com/q-Django-jobs.html
[stackoverflow-com]: https://stackoverflow.com/jobs/developer-jobs-using-django [stackoverflow-com]: https://stackoverflow.com/jobs/developer-jobs-using-django
[upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/ [upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/

View File

@ -38,11 +38,36 @@ You can determine your currently installed version using `pip show`:
--- ---
## 3.10.x series
### 3.10.0
**Date**: [Unreleased][3.10.0-milestone]
* Resolve DeprecationWarning with markdown. [#6317][gh6317]
## 3.9.x series ## 3.9.x series
### 3.9.4
**Date**: 10th May 2019
This is a maintenance release that fixes an error handling bug under Python 2.
### 3.9.3
**Date**: 29th April 2019
This is the last Django REST Framework release that will support Python 2.
Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10.
* Adjusted the compat check for django-guardian to allow the last guardian
version (v1.4.9) compatible with Python 2. [#6613][gh6613]
### 3.9.2 ### 3.9.2
**Date**: [3rd March 2019][3.9.1-milestone] **Date**: [3rd March 2019][3.9.2-milestone]
* Routers: invalidate `_urls` cache on `register()` [#6407][gh6407] * Routers: invalidate `_urls` cache on `register()` [#6407][gh6407]
* Deferred schema renderer creation to avoid requiring pyyaml. [#6416][gh6416] * Deferred schema renderer creation to avoid requiring pyyaml. [#6416][gh6416]
@ -62,7 +87,7 @@ You can determine your currently installed version using `pip show`:
### 3.9.1 ### 3.9.1
**Date**: [16th Janurary 2019][3.9.1-milestone] **Date**: [16th January 2019][3.9.1-milestone]
* Resolve XSS issue in browsable API. [#6330][gh6330] * Resolve XSS issue in browsable API. [#6330][gh6330]
* Upgrade Bootstrap to 3.4.0 to resolve XSS issue. * Upgrade Bootstrap to 3.4.0 to resolve XSS issue.
@ -1165,7 +1190,8 @@ For older release notes, [please see the version 2.x documentation][old-release-
[3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1 [3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1
[3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1 [3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1
[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/70?closed=1 [3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/70?closed=1
[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/71?closed=1 [3.9.2-milestone]: https://github.com/encode/django-rest-framework/milestone/71?closed=1
[3.10.0-milestone]: https://github.com/encode/django-rest-framework/milestone/69?closed=1
<!-- 3.0.1 --> <!-- 3.0.1 -->
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013 [gh2013]: https://github.com/encode/django-rest-framework/issues/2013
@ -2106,3 +2132,9 @@ For older release notes, [please see the version 2.x documentation][old-release-
[gh6340]: https://github.com/encode/django-rest-framework/issues/6340 [gh6340]: https://github.com/encode/django-rest-framework/issues/6340
[gh6416]: https://github.com/encode/django-rest-framework/issues/6416 [gh6416]: https://github.com/encode/django-rest-framework/issues/6416
[gh6407]: https://github.com/encode/django-rest-framework/issues/6407 [gh6407]: https://github.com/encode/django-rest-framework/issues/6407
<!-- 3.9.3 -->
[gh6613]: https://github.com/encode/django-rest-framework/issues/6613
<!-- 3.10.0 -->
[gh6317]: https://github.com/encode/django-rest-framework/issues/6317

View File

@ -263,6 +263,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM. * [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM.
* [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework. * [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework.
* [djangorestframework-datatables][djangorestframework-datatables] - Seamless integration between Django REST framework and [Datatables](https://datatables.net). * [djangorestframework-datatables][djangorestframework-datatables] - Seamless integration between Django REST framework and [Datatables](https://datatables.net).
* [django-rest-framework-condition][django-rest-framework-condition] - Decorators for managing HTTP cache headers for Django REST framework (ETag and Last-modified).
* [django-rest-witchcraft][django-rest-witchcraft] - Provides DRF integration with SQLAlchemy with SQLAlchemy model serializers/viewsets and a bunch of other goodies
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
@ -336,3 +338,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
[djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy [djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy
[djangorestframework-datatables]: https://github.com/izimobil/django-rest-framework-datatables [djangorestframework-datatables]: https://github.com/izimobil/django-rest-framework-datatables
[django-rest-framework-condition]: https://github.com/jozo/django-rest-framework-condition
[django-rest-witchcraft]: https://github.com/shosca/django-rest-witchcraft

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -84,7 +84,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following: REST framework requires the following:
* Python (2.7, 3.4, 3.5, 3.6, 3.7) * Python (3.5, 3.6, 3.7)
* Django (1.11, 2.0, 2.1, 2.2) * Django (1.11, 2.0, 2.1, 2.2)
We **highly recommend** and only officially support the latest patch release of We **highly recommend** and only officially support the latest patch release of
@ -93,9 +93,9 @@ each Python and Django series.
The following packages are optional: The following packages are optional:
* [coreapi][coreapi] (1.32.0+) - Schema generation support. * [coreapi][coreapi] (1.32.0+) - Schema generation support.
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API. * [Markdown][markdown] (3.0.0+) - Markdown support for the browsable API.
* [Pygments][pygments] (2.4.0+) - Add sytax highlighting to Markdown processing.
* [django-filter][django-filter] (1.0.1+) - Filtering support. * [django-filter][django-filter] (1.0.1+) - Filtering support.
* [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering.
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support. * [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
## Installation ## Installation
@ -238,8 +238,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[eventbrite]: https://www.eventbrite.co.uk/about/ [eventbrite]: https://www.eventbrite.co.uk/about/
[coreapi]: https://pypi.org/project/coreapi/ [coreapi]: https://pypi.org/project/coreapi/
[markdown]: https://pypi.org/project/Markdown/ [markdown]: https://pypi.org/project/Markdown/
[pygments]: https://pypi.org/project/Pygments/
[django-filter]: https://pypi.org/project/django-filter/ [django-filter]: https://pypi.org/project/django-filter/
[django-crispy-forms]: https://github.com/maraujop/django-crispy-forms
[django-guardian]: https://github.com/django-guardian/django-guardian [django-guardian]: https://github.com/django-guardian/django-guardian
[index]: . [index]: .
[oauth1-section]: api-guide/authentication/#django-rest-framework-oauth [oauth1-section]: api-guide/authentication/#django-rest-framework-oauth

View File

@ -17,7 +17,7 @@ The built-in API documentation includes:
### Installation ### Installation
The `coreapi` library is required as a dependency for the API docs. Make sure The `coreapi` library is required as a dependency for the API docs. Make sure
to install the latest version. The `pygments` and `markdown` libraries to install the latest version. The `Pygments` and `Markdown` libraries
are optional but recommended. are optional but recommended.
To install the API documentation, you'll need to include it in your project's URLconf: To install the API documentation, you'll need to include it in your project's URLconf:
@ -195,18 +195,6 @@ This also translates into a very useful interactive documentation viewer in the
--- ---
#### DRF Docs
[DRF Docs][drfdocs-repo] allows you to document Web APIs made with Django REST Framework and it is authored by Emmanouil Konstantinidis. It's made to work out of the box and its setup should not take more than a couple of minutes. Complete documentation can be found on the [website][drfdocs-website] while there is also a [demo][drfdocs-demo] available for people to see what it looks like. **Live API Endpoints** allow you to utilize the endpoints from within the documentation in a neat way.
Features include customizing the template with your branding, settings for hiding the docs depending on the environment and more.
Both this package and Django REST Swagger are fully documented, well supported, and come highly recommended.
![Screenshot - DRF docs][image-drf-docs]
---
#### Django REST Swagger #### Django REST Swagger
Marc Gibbons' [Django REST Swagger][django-rest-swagger] integrates REST framework with the [Swagger][swagger] API documentation tool. The package produces well presented API documentation, and includes interactive tools for testing API endpoints. Marc Gibbons' [Django REST Swagger][django-rest-swagger] integrates REST framework with the [Swagger][swagger] API documentation tool. The package produces well presented API documentation, and includes interactive tools for testing API endpoints.
@ -215,7 +203,7 @@ Django REST Swagger supports REST framework versions 2.3 and above.
Mark is also the author of the [REST Framework Docs][rest-framework-docs] package which offers clean, simple autogenerated documentation for your API but is deprecated and has moved to Django REST Swagger. Mark is also the author of the [REST Framework Docs][rest-framework-docs] package which offers clean, simple autogenerated documentation for your API but is deprecated and has moved to Django REST Swagger.
Both this package and DRF docs are fully documented, well supported, and come highly recommended. This package is fully documented, well supported, and comes highly recommended.
![Screenshot - Django REST Swagger][image-django-rest-swagger] ![Screenshot - Django REST Swagger][image-django-rest-swagger]
@ -277,7 +265,7 @@ When working with viewsets, an appropriate suffix is appended to each generated
The description in the browsable API is generated from the docstring of the view or viewset. The description in the browsable API is generated from the docstring of the view or viewset.
If the python `markdown` library is installed, then [markdown syntax][markdown] may be used in the docstring, and will be converted to HTML in the browsable API. For example: If the python `Markdown` library is installed, then [markdown syntax][markdown] may be used in the docstring, and will be converted to HTML in the browsable API. For example:
class AccountListView(views.APIView): class AccountListView(views.APIView):
""" """
@ -322,18 +310,14 @@ To implement a hypermedia API you'll need to decide on an appropriate media type
[cite]: https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven [cite]: https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[drf-yasg]: https://github.com/axnsan12/drf-yasg/ [drf-yasg]: https://github.com/axnsan12/drf-yasg/
[image-drf-yasg]: ../img/drf-yasg.png [image-drf-yasg]: ../img/drf-yasg.png
[drfdocs-repo]: https://github.com/ekonstantinidis/django-rest-framework-docs
[drfdocs-website]: https://www.drfdocs.com/
[drfdocs-demo]: http://demo.drfdocs.com/
[drfautodocs-repo]: https://github.com/iMakedonsky/drf-autodocs [drfautodocs-repo]: https://github.com/iMakedonsky/drf-autodocs
[django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger [django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger
[swagger]: https://swagger.io/ [swagger]: https://swagger.io/
[open-api]: https://openapis.org/ [open-api]: https://openapis.org/
[rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs [rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
[apiary]: https://apiary.io/ [apiary]: https://apiary.io/
[markdown]: https://daringfireball.net/projects/markdown/ [markdown]: https://daringfireball.net/projects/markdown/syntax
[hypermedia-docs]: rest-hypermedia-hateoas.md [hypermedia-docs]: rest-hypermedia-hateoas.md
[image-drf-docs]: ../img/drfdocs.png
[image-django-rest-swagger]: ../img/django-rest-swagger.png [image-django-rest-swagger]: ../img/django-rest-swagger.png
[image-apiary]: ../img/apiary.png [image-apiary]: ../img/apiary.png
[image-self-describing-api]: ../img/self-describing.png [image-self-describing-api]: ../img/self-describing.png

View File

@ -8,24 +8,24 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
--- ---
**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox]. **Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
--- ---
## Setting up a new environment ## Setting up a new environment
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on. Before we do anything else we'll create a new virtual environment, using [venv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
virtualenv env python3 -m venv env
source env/bin/activate source env/bin/activate
Now that we're inside a virtualenv environment, we can install our package requirements. Now that we're inside a virtual environment, we can install our package requirements.
pip install django pip install django
pip install djangorestframework pip install djangorestframework
pip install pygments # We'll be using this for the code highlighting pip install pygments # We'll be using this for the code highlighting
**Note:** To exit the virtualenv environment at any time, just type `deactivate`. For more information see the [virtualenv documentation][virtualenv]. **Note:** To exit the virtual environment at any time, just type `deactivate`. For more information see the [venv documentation][venv].
## Getting started ## Getting started
@ -218,7 +218,6 @@ Edit the `snippets/views.py` file, and add the following.
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
from snippets.models import Snippet from snippets.models import Snippet
from snippets.serializers import SnippetSerializer from snippets.serializers import SnippetSerializer
@ -373,7 +372,7 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
[quickstart]: quickstart.md [quickstart]: quickstart.md
[repo]: https://github.com/encode/rest-framework-tutorial [repo]: https://github.com/encode/rest-framework-tutorial
[sandbox]: https://restframework.herokuapp.com/ [sandbox]: https://restframework.herokuapp.com/
[virtualenv]: http://www.virtualenv.org/en/latest/index.html [venv]: https://docs.python.org/3/library/venv.html
[tut-2]: 2-requests-and-responses.md [tut-2]: 2-requests-and-responses.md
[httpie]: https://github.com/jakubroztocil/httpie#installation [httpie]: https://github.com/jakubroztocil/httpie#installation
[curl]: https://curl.haxx.se/ [curl]: https://curl.haxx.se/

View File

@ -10,11 +10,11 @@ Create a new Django project named `tutorial`, then start a new app called `quick
mkdir tutorial mkdir tutorial
cd tutorial cd tutorial
# Create a virtualenv to isolate our package dependencies locally # Create a virtual environment to isolate our package dependencies locally
virtualenv env python3 -m venv env
source env/bin/activate # On Windows use `env\Scripts\activate` source env/bin/activate # On Windows use `env\Scripts\activate`
# Install Django and Django REST framework into the virtualenv # Install Django and Django REST framework into the virtual environment
pip install django pip install django
pip install djangorestframework pip install djangorestframework

View File

@ -141,7 +141,7 @@
<script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script> <script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
<script src="{{ base_url }}/js/prettify-1.0.js"></script> <script src="{{ base_url }}/js/prettify-1.0.js"></script>
<script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script> <script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
<script src="https://fund.django-rest-framework.org/sidebar_include.js"></script> <script async src="https://fund.django-rest-framework.org/sidebar_include.js"></script>
<script>var base_url = '{{ base_url }}';</script> <script>var base_url = '{{ base_url }}';</script>
<script src="{{ base_url }}/mkdocs/js/require.js"></script> <script src="{{ base_url }}/mkdocs/js/require.js"></script>
<script src="{{ base_url }}/js/theme.js"></script> <script src="{{ base_url }}/js/theme.js"></script>

View File

@ -1,8 +1,9 @@
# Optional packages which may be used with REST framework. # Optional packages which may be used with REST framework.
psycopg2-binary==2.7.5 psycopg2-binary>=2.8.2, <2.9
markdown==2.6.11 markdown==3.1.1
pygments==2.4.2
django-guardian==1.5.0 django-guardian==1.5.0
django-filter==1.1.0 django-filter>=2.1.0, <2.2
coreapi==2.3.1 coreapi==2.3.1
coreschema==0.0.4 coreschema==0.0.4
pyyaml pyyaml

View File

@ -1,4 +1,4 @@
# Pytest for running the tests. # Pytest for running the tests.
pytest==4.3.0 pytest>=4.5.0,<4.6
pytest-django==3.4.8 pytest-django>=3.4.8,<3.5
pytest-cov==2.6.1 pytest-cov>=2.7.1

View File

@ -8,7 +8,7 @@ ______ _____ _____ _____ __
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '3.9.2' __version__ = '3.9.3'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause' __license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'
@ -25,9 +25,9 @@ ISO_8601 = 'iso-8601'
default_app_config = 'rest_framework.apps.RestFrameworkConfig' default_app_config = 'rest_framework.apps.RestFrameworkConfig'
class RemovedInDRF310Warning(DeprecationWarning): class RemovedInDRF311Warning(DeprecationWarning):
pass pass
class RemovedInDRF311Warning(PendingDeprecationWarning): class RemovedInDRF312Warning(PendingDeprecationWarning):
pass pass

View File

@ -1,15 +1,12 @@
""" """
Provides various authentication policies. Provides various authentication policies.
""" """
from __future__ import unicode_literals
import base64 import base64
import binascii import binascii
from django.contrib.auth import authenticate, get_user_model from django.contrib.auth import authenticate, get_user_model
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware
from django.utils.six import text_type from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework import HTTP_HEADER_ENCODING, exceptions
@ -21,7 +18,7 @@ def get_authorization_header(request):
Hide some test client ickyness where the header can be unicode. Hide some test client ickyness where the header can be unicode.
""" """
auth = request.META.get('HTTP_AUTHORIZATION', b'') auth = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(auth, text_type): if isinstance(auth, str):
# Work around django test client oddness # Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING) auth = auth.encode(HTTP_HEADER_ENCODING)
return auth return auth
@ -33,7 +30,7 @@ class CSRFCheck(CsrfViewMiddleware):
return reason return reason
class BaseAuthentication(object): class BaseAuthentication:
""" """
All authentication classes should extend BaseAuthentication. All authentication classes should extend BaseAuthentication.
""" """

View File

@ -1,5 +1,5 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class AuthTokenConfig(AppConfig): class AuthTokenConfig(AppConfig):

View File

@ -38,8 +38,8 @@ class Command(BaseCommand):
token = self.create_user_token(username, reset_token) token = self.create_user_token(username, reset_token)
except UserModel.DoesNotExist: except UserModel.DoesNotExist:
raise CommandError( raise CommandError(
'Cannot create the Token: user {0} does not exist'.format( 'Cannot create the Token: user {} does not exist'.format(
username) username)
) )
self.stdout.write( self.stdout.write(
'Generated token {0} for user {1}'.format(token.key, username)) 'Generated token {} for user {}'.format(token.key, username))

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@ -3,11 +3,9 @@ import os
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
@python_2_unicode_compatible
class Token(models.Model): class Token(models.Model):
""" """
The default authorization token model. The default authorization token model.
@ -32,7 +30,7 @@ class Token(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.key: if not self.key:
self.key = self.generate_key() self.key = self.generate_key()
return super(Token, self).save(*args, **kwargs) return super().save(*args, **kwargs)
def generate_key(self): def generate_key(self):
return binascii.hexlify(os.urandom(20)).decode() return binascii.hexlify(os.urandom(20)).decode()

View File

@ -1,5 +1,5 @@
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers

View File

@ -2,23 +2,11 @@
The `compat` module provides support for backwards compatibility with older The `compat` module provides support for backwards compatibility with older
versions of Django/Python, and compatibility wrappers around optional packages. versions of Django/Python, and compatibility wrappers around optional packages.
""" """
from __future__ import unicode_literals
import sys import sys
from django.conf import settings from django.conf import settings
from django.core import validators
from django.utils import six
from django.views.generic import View from django.views.generic import View
try:
# Python 3
from collections.abc import Mapping, MutableMapping # noqa
except ImportError:
# Python 2.7
from collections import Mapping, MutableMapping # noqa
try: try:
from django.urls import ( # noqa from django.urls import ( # noqa
URLPattern, URLPattern,
@ -36,11 +24,6 @@ try:
except ImportError: except ImportError:
ProhibitNullCharactersValidator = None ProhibitNullCharactersValidator = None
try:
from unittest import mock
except ImportError:
mock = None
def get_original_route(urlpattern): def get_original_route(urlpattern):
""" """
@ -89,23 +72,6 @@ def make_url_resolver(regex, urlpatterns):
return URLResolver(regex, urlpatterns) return URLResolver(regex, urlpatterns)
def unicode_repr(instance):
# Get the repr of an instance, but ensure it is a unicode string
# on both python 3 (already the case) and 2 (not the case).
if six.PY2:
return repr(instance).decode('utf-8')
return repr(instance)
def unicode_to_repr(value):
# Coerce a unicode string to the correct repr return type, depending on
# the Python version. We wrap all our `__repr__` implementations with
# this and then use unicode throughout internally.
if six.PY2:
return value.encode('utf-8')
return value
def unicode_http_header(value): def unicode_http_header(value):
# Coerce HTTP header value to unicode. # Coerce HTTP header value to unicode.
if isinstance(value, bytes): if isinstance(value, bytes):
@ -150,13 +116,6 @@ except ImportError:
yaml = None yaml = None
# django-crispy-forms is optional
try:
import crispy_forms
except ImportError:
crispy_forms = None
# requests is optional # requests is optional
try: try:
import requests import requests
@ -164,35 +123,17 @@ except ImportError:
requests = None requests = None
def is_guardian_installed():
"""
django-guardian is optional and only imported if in INSTALLED_APPS.
"""
if six.PY2:
# Guardian 1.5.0, for Django 2.2 is NOT compatible with Python 2.7.
# Remove when dropping PY2.
return False
return 'guardian' in settings.INSTALLED_APPS
# PATCH method is not implemented by Django # PATCH method is not implemented by Django
if 'patch' not in View.http_method_names: if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch'] View.http_method_names = View.http_method_names + ['patch']
# Markdown is optional # Markdown is optional (version 3.0+ required)
try: try:
import markdown import markdown
if markdown.version <= '2.2': HEADERID_EXT_PATH = 'markdown.extensions.toc'
HEADERID_EXT_PATH = 'headerid' LEVEL_PARAM = 'baselevel'
LEVEL_PARAM = 'level'
elif markdown.version < '2.6':
HEADERID_EXT_PATH = 'markdown.extensions.headerid'
LEVEL_PARAM = 'level'
else:
HEADERID_EXT_PATH = 'markdown.extensions.toc'
LEVEL_PARAM = 'baselevel'
def apply_markdown(text): def apply_markdown(text):
""" """
@ -265,7 +206,7 @@ if markdown is not None and pygments is not None:
return ret.split("\n") return ret.split("\n")
def md_filter_add_syntax_highlight(md): def md_filter_add_syntax_highlight(md):
md.preprocessors.add('highlight', CodeBlockPreprocessor(), "_begin") md.preprocessors.register(CodeBlockPreprocessor(), 'highlight', 40)
return True return True
else: else:
def md_filter_add_syntax_highlight(md): def md_filter_add_syntax_highlight(md):
@ -284,43 +225,9 @@ except ImportError:
# `separators` argument to `json.dumps()` differs between 2.x and 3.x # `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: https://bugs.python.org/issue22767 # See: https://bugs.python.org/issue22767
if six.PY3: SHORT_SEPARATORS = (',', ':')
SHORT_SEPARATORS = (',', ':') LONG_SEPARATORS = (', ', ': ')
LONG_SEPARATORS = (', ', ': ') INDENT_SEPARATORS = (',', ': ')
INDENT_SEPARATORS = (',', ': ')
else:
SHORT_SEPARATORS = (b',', b':')
LONG_SEPARATORS = (b', ', b': ')
INDENT_SEPARATORS = (b',', b': ')
class CustomValidatorMessage(object):
"""
We need to avoid evaluation of `lazy` translated `message` in `django.core.validators.BaseValidator.__init__`.
https://github.com/django/django/blob/75ed5900321d170debef4ac452b8b3cf8a1c2384/django/core/validators.py#L297
Ref: https://github.com/encode/django-rest-framework/pull/5452
"""
def __init__(self, *args, **kwargs):
self.message = kwargs.pop('message', self.message)
super(CustomValidatorMessage, self).__init__(*args, **kwargs)
class MinValueValidator(CustomValidatorMessage, validators.MinValueValidator):
pass
class MaxValueValidator(CustomValidatorMessage, validators.MaxValueValidator):
pass
class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator):
pass
class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator):
pass
# Version Constants. # Version Constants.

View File

@ -6,15 +6,10 @@ There are also various decorators for setting the API policies on function
based views, as well as the `@detail_route` and `@list_route` decorators, which are based views, as well as the `@detail_route` and `@list_route` decorators, which are
used to annotate methods on viewsets that should be included by routers. used to annotate methods on viewsets that should be included by routers.
""" """
from __future__ import unicode_literals
import types import types
import warnings
from django.forms.utils import pretty_name from django.forms.utils import pretty_name
from django.utils import six
from rest_framework import RemovedInDRF310Warning
from rest_framework.views import APIView from rest_framework.views import APIView
@ -28,7 +23,7 @@ def api_view(http_method_names=None):
def decorator(func): def decorator(func):
WrappedAPIView = type( WrappedAPIView = type(
six.PY3 and 'WrappedAPIView' or b'WrappedAPIView', 'WrappedAPIView',
(APIView,), (APIView,),
{'__doc__': func.__doc__} {'__doc__': func.__doc__}
) )
@ -217,39 +212,3 @@ class MethodMapper(dict):
def trace(self, func): def trace(self, func):
return self._map('trace', func) return self._map('trace', func)
def detail_route(methods=None, **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail requests.
"""
warnings.warn(
"`detail_route` is deprecated and will be removed in 3.10 in favor of "
"`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.",
RemovedInDRF310Warning, stacklevel=2
)
def decorator(func):
func = action(methods, detail=True, **kwargs)(func)
if 'url_name' not in kwargs:
func.url_name = func.url_path.replace('_', '-')
return func
return decorator
def list_route(methods=None, **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for list requests.
"""
warnings.warn(
"`list_route` is deprecated and will be removed in 3.10 in favor of "
"`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.",
RemovedInDRF310Warning, stacklevel=2
)
def decorator(func):
func = action(methods, detail=False, **kwargs)(func)
if 'url_name' not in kwargs:
func.url_name = func.url_path.replace('_', '-')
return func
return decorator

View File

@ -4,18 +4,14 @@ Handled exceptions raised by REST framework.
In addition Django's built in 403 and 404 exceptions are handled. In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`) (`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
""" """
from __future__ import unicode_literals
import math import math
from django.http import JsonResponse from django.http import JsonResponse
from django.utils import six
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.translation import ungettext from django.utils.translation import ngettext
from rest_framework import status from rest_framework import status
from rest_framework.compat import unicode_to_repr
from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList
@ -64,19 +60,19 @@ def _get_full_details(detail):
} }
class ErrorDetail(six.text_type): class ErrorDetail(str):
""" """
A string-like object that can additionally have a code. A string-like object that can additionally have a code.
""" """
code = None code = None
def __new__(cls, string, code=None): def __new__(cls, string, code=None):
self = super(ErrorDetail, cls).__new__(cls, string) self = super().__new__(cls, string)
self.code = code self.code = code
return self return self
def __eq__(self, other): def __eq__(self, other):
r = super(ErrorDetail, self).__eq__(other) r = super().__eq__(other)
try: try:
return r and self.code == other.code return r and self.code == other.code
except AttributeError: except AttributeError:
@ -86,10 +82,10 @@ class ErrorDetail(six.text_type):
return not self.__eq__(other) return not self.__eq__(other)
def __repr__(self): def __repr__(self):
return unicode_to_repr('ErrorDetail(string=%r, code=%r)' % ( return 'ErrorDetail(string=%r, code=%r)' % (
six.text_type(self), str(self),
self.code, self.code,
)) )
def __hash__(self): def __hash__(self):
return hash(str(self)) return hash(str(self))
@ -113,7 +109,7 @@ class APIException(Exception):
self.detail = _get_error_details(detail, code) self.detail = _get_error_details(detail, code)
def __str__(self): def __str__(self):
return six.text_type(self.detail) return str(self.detail)
def get_codes(self): def get_codes(self):
""" """
@ -196,7 +192,7 @@ class MethodNotAllowed(APIException):
def __init__(self, method, detail=None, code=None): def __init__(self, method, detail=None, code=None):
if detail is None: if detail is None:
detail = force_text(self.default_detail).format(method=method) detail = force_text(self.default_detail).format(method=method)
super(MethodNotAllowed, self).__init__(detail, code) super().__init__(detail, code)
class NotAcceptable(APIException): class NotAcceptable(APIException):
@ -206,7 +202,7 @@ class NotAcceptable(APIException):
def __init__(self, detail=None, code=None, available_renderers=None): def __init__(self, detail=None, code=None, available_renderers=None):
self.available_renderers = available_renderers self.available_renderers = available_renderers
super(NotAcceptable, self).__init__(detail, code) super().__init__(detail, code)
class UnsupportedMediaType(APIException): class UnsupportedMediaType(APIException):
@ -217,7 +213,7 @@ class UnsupportedMediaType(APIException):
def __init__(self, media_type, detail=None, code=None): def __init__(self, media_type, detail=None, code=None):
if detail is None: if detail is None:
detail = force_text(self.default_detail).format(media_type=media_type) detail = force_text(self.default_detail).format(media_type=media_type)
super(UnsupportedMediaType, self).__init__(detail, code) super().__init__(detail, code)
class Throttled(APIException): class Throttled(APIException):
@ -234,11 +230,11 @@ class Throttled(APIException):
wait = math.ceil(wait) wait = math.ceil(wait)
detail = ' '.join(( detail = ' '.join((
detail, detail,
force_text(ungettext(self.extra_detail_singular.format(wait=wait), force_text(ngettext(self.extra_detail_singular.format(wait=wait),
self.extra_detail_plural.format(wait=wait), self.extra_detail_plural.format(wait=wait),
wait)))) wait))))
self.wait = wait self.wait = wait
super(Throttled, self).__init__(detail, code) super().__init__(detail, code)
def server_error(request, *args, **kwargs): def server_error(request, *args, **kwargs):

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
import copy import copy
import datetime import datetime
import decimal import decimal
@ -8,37 +6,35 @@ import inspect
import re import re
import uuid import uuid
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Mapping
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import ( from django.core.validators import (
EmailValidator, RegexValidator, URLValidator, ip_address_validators EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
MinValueValidator, RegexValidator, URLValidator, ip_address_validators
) )
from django.forms import FilePathField as DjangoFilePathField from django.forms import FilePathField as DjangoFilePathField
from django.forms import ImageField as DjangoImageField from django.forms import ImageField as DjangoImageField
from django.utils import six, timezone from django.utils import timezone
from django.utils.dateparse import ( from django.utils.dateparse import (
parse_date, parse_datetime, parse_duration, parse_time parse_date, parse_datetime, parse_duration, parse_time
) )
from django.utils.duration import duration_string from django.utils.duration import duration_string
from django.utils.encoding import is_protected_type, smart_text from django.utils.encoding import is_protected_type, smart_text
from django.utils.formats import localize_input, sanitize_separators from django.utils.formats import localize_input, sanitize_separators
from django.utils.functional import lazy
from django.utils.ipv6 import clean_ipv6_address from django.utils.ipv6 import clean_ipv6_address
from django.utils.timezone import utc from django.utils.timezone import utc
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pytz.exceptions import InvalidTimeError from pytz.exceptions import InvalidTimeError
from rest_framework import ISO_8601 from rest_framework import ISO_8601
from rest_framework.compat import ( from rest_framework.compat import ProhibitNullCharactersValidator
Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
MinValueValidator, ProhibitNullCharactersValidator, unicode_repr,
unicode_to_repr
)
from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils import html, humanize_datetime, json, representation from rest_framework.utils import html, humanize_datetime, json, representation
from rest_framework.utils.formatting import lazy_format
class empty: class empty:
@ -51,39 +47,21 @@ class empty:
pass pass
if six.PY3: def is_simple_callable(obj):
def is_simple_callable(obj): """
""" True if the object is a callable that takes no arguments.
True if the object is a callable that takes no arguments. """
""" if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)):
if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)): return False
return False
sig = inspect.signature(obj) sig = inspect.signature(obj)
params = sig.parameters.values() params = sig.parameters.values()
return all( return all(
param.kind == param.VAR_POSITIONAL or param.kind == param.VAR_POSITIONAL or
param.kind == param.VAR_KEYWORD or param.kind == param.VAR_KEYWORD or
param.default != param.empty param.default != param.empty
for param in params for param in params
) )
else:
def is_simple_callable(obj):
function = inspect.isfunction(obj)
method = inspect.ismethod(obj)
if not (function or method):
return False
if method:
is_unbound = obj.im_self is None
args, _, _, defaults = inspect.getargspec(obj)
len_args = len(args) if function or is_unbound else len(args) - 1
len_defaults = len(defaults) if defaults else 0
return len_args <= len_defaults
def get_attribute(instance, attrs): def get_attribute(instance, attrs):
@ -108,7 +86,7 @@ def get_attribute(instance, attrs):
# If we raised an Attribute or KeyError here it'd get treated # If we raised an Attribute or KeyError here it'd get treated
# as an omitted field in `Field.get_attribute()`. Instead we # as an omitted field in `Field.get_attribute()`. Instead we
# raise a ValueError to ensure the exception is not masked. # raise a ValueError to ensure the exception is not masked.
raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc)) raise ValueError('Exception raised in callable attribute "{}"; original exception was: {}'.format(attr, exc))
return instance return instance
@ -185,18 +163,18 @@ def iter_options(grouped_choices, cutoff=None, cutoff_text=None):
""" """
Helper function for options and option groups in templates. Helper function for options and option groups in templates.
""" """
class StartOptionGroup(object): class StartOptionGroup:
start_option_group = True start_option_group = True
end_option_group = False end_option_group = False
def __init__(self, label): def __init__(self, label):
self.label = label self.label = label
class EndOptionGroup(object): class EndOptionGroup:
start_option_group = False start_option_group = False
end_option_group = True end_option_group = True
class Option(object): class Option:
start_option_group = False start_option_group = False
end_option_group = False end_option_group = False
@ -251,7 +229,7 @@ def get_error_detail(exc_info):
} }
class CreateOnlyDefault(object): class CreateOnlyDefault:
""" """
This class may be used to provide default values that are only used This class may be used to provide default values that are only used
for create operations, but that do not return any value for update for create operations, but that do not return any value for update
@ -273,12 +251,10 @@ class CreateOnlyDefault(object):
return self.default return self.default
def __repr__(self): def __repr__(self):
return unicode_to_repr( return '%s(%s)' % (self.__class__.__name__, repr(self.default))
'%s(%s)' % (self.__class__.__name__, unicode_repr(self.default))
)
class CurrentUserDefault(object): class CurrentUserDefault:
def set_context(self, serializer_field): def set_context(self, serializer_field):
self.user = serializer_field.context['request'].user self.user = serializer_field.context['request'].user
@ -286,7 +262,7 @@ class CurrentUserDefault(object):
return self.user return self.user
def __repr__(self): def __repr__(self):
return unicode_to_repr('%s()' % self.__class__.__name__) return '%s()' % self.__class__.__name__
class SkipField(Exception): class SkipField(Exception):
@ -305,7 +281,7 @@ MISSING_ERROR_MESSAGE = (
) )
class Field(object): class Field:
_creation_counter = 0 _creation_counter = 0
default_error_messages = { default_error_messages = {
@ -515,6 +491,11 @@ class Field(object):
if data is None: if data is None:
if not self.allow_null: if not self.allow_null:
self.fail('null') self.fail('null')
# Nullable `source='*'` fields should not be skipped when its named
# field is given a null value. This is because `source='*'` means
# the field is passed the entire object, which is not null.
elif self.source == '*':
return (False, None)
return (True, None) return (True, None)
return (False, data) return (False, data)
@ -618,7 +599,7 @@ class Field(object):
When a field is instantiated, we store the arguments that were used, When a field is instantiated, we store the arguments that were used,
so that we can present a helpful representation of the object. so that we can present a helpful representation of the object.
""" """
instance = super(Field, cls).__new__(cls) instance = super().__new__(cls)
instance._args = args instance._args = args
instance._kwargs = kwargs instance._kwargs = kwargs
return instance return instance
@ -636,7 +617,7 @@ class Field(object):
for item in self._args for item in self._args
] ]
kwargs = { kwargs = {
key: (copy.deepcopy(value) if (key not in ('validators', 'regex')) else value) key: (copy.deepcopy(value, memo) if (key not in ('validators', 'regex')) else value)
for key, value in self._kwargs.items() for key, value in self._kwargs.items()
} }
return self.__class__(*args, **kwargs) return self.__class__(*args, **kwargs)
@ -647,7 +628,7 @@ class Field(object):
This allows us to create descriptive representations for serializer This allows us to create descriptive representations for serializer
instances that show all the declared fields on the serializer. instances that show all the declared fields on the serializer.
""" """
return unicode_to_repr(representation.field_repr(self)) return representation.field_repr(self)
# Boolean types... # Boolean types...
@ -724,7 +705,7 @@ class NullBooleanField(Field):
def __init__(self, **kwargs): def __init__(self, **kwargs):
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.' assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
kwargs['allow_null'] = True kwargs['allow_null'] = True
super(NullBooleanField, self).__init__(**kwargs) super().__init__(**kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
try: try:
@ -764,17 +745,13 @@ class CharField(Field):
self.trim_whitespace = kwargs.pop('trim_whitespace', True) self.trim_whitespace = kwargs.pop('trim_whitespace', True)
self.max_length = kwargs.pop('max_length', None) self.max_length = kwargs.pop('max_length', None)
self.min_length = kwargs.pop('min_length', None) self.min_length = kwargs.pop('min_length', None)
super(CharField, self).__init__(**kwargs) super().__init__(**kwargs)
if self.max_length is not None: if self.max_length is not None:
message = lazy( message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
self.error_messages['max_length'].format,
six.text_type)(max_length=self.max_length)
self.validators.append( self.validators.append(
MaxLengthValidator(self.max_length, message=message)) MaxLengthValidator(self.max_length, message=message))
if self.min_length is not None: if self.min_length is not None:
message = lazy( message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
self.error_messages['min_length'].format,
six.text_type)(min_length=self.min_length)
self.validators.append( self.validators.append(
MinLengthValidator(self.min_length, message=message)) MinLengthValidator(self.min_length, message=message))
@ -786,23 +763,23 @@ class CharField(Field):
# Test for the empty string here so that it does not get validated, # Test for the empty string here so that it does not get validated,
# and so that subclasses do not need to handle it explicitly # and so that subclasses do not need to handle it explicitly
# inside the `to_internal_value()` method. # inside the `to_internal_value()` method.
if data == '' or (self.trim_whitespace and six.text_type(data).strip() == ''): if data == '' or (self.trim_whitespace and str(data).strip() == ''):
if not self.allow_blank: if not self.allow_blank:
self.fail('blank') self.fail('blank')
return '' return ''
return super(CharField, self).run_validation(data) return super().run_validation(data)
def to_internal_value(self, data): def to_internal_value(self, data):
# We're lenient with allowing basic numerics to be coerced into strings, # We're lenient with allowing basic numerics to be coerced into strings,
# but other types should fail. Eg. unclear if booleans should represent as `true` or `True`, # but other types should fail. Eg. unclear if booleans should represent as `true` or `True`,
# and composites such as lists are likely user error. # and composites such as lists are likely user error.
if isinstance(data, bool) or not isinstance(data, six.string_types + six.integer_types + (float,)): if isinstance(data, bool) or not isinstance(data, (str, int, float,)):
self.fail('invalid') self.fail('invalid')
value = six.text_type(data) value = str(data)
return value.strip() if self.trim_whitespace else value return value.strip() if self.trim_whitespace else value
def to_representation(self, value): def to_representation(self, value):
return six.text_type(value) return str(value)
class EmailField(CharField): class EmailField(CharField):
@ -811,7 +788,7 @@ class EmailField(CharField):
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(EmailField, self).__init__(**kwargs) super().__init__(**kwargs)
validator = EmailValidator(message=self.error_messages['invalid']) validator = EmailValidator(message=self.error_messages['invalid'])
self.validators.append(validator) self.validators.append(validator)
@ -822,7 +799,7 @@ class RegexField(CharField):
} }
def __init__(self, regex, **kwargs): def __init__(self, regex, **kwargs):
super(RegexField, self).__init__(**kwargs) super().__init__(**kwargs)
validator = RegexValidator(regex, message=self.error_messages['invalid']) validator = RegexValidator(regex, message=self.error_messages['invalid'])
self.validators.append(validator) self.validators.append(validator)
@ -834,7 +811,7 @@ class SlugField(CharField):
} }
def __init__(self, allow_unicode=False, **kwargs): def __init__(self, allow_unicode=False, **kwargs):
super(SlugField, self).__init__(**kwargs) super().__init__(**kwargs)
self.allow_unicode = allow_unicode self.allow_unicode = allow_unicode
if self.allow_unicode: if self.allow_unicode:
validator = RegexValidator(re.compile(r'^[-\w]+\Z', re.UNICODE), message=self.error_messages['invalid_unicode']) validator = RegexValidator(re.compile(r'^[-\w]+\Z', re.UNICODE), message=self.error_messages['invalid_unicode'])
@ -849,7 +826,7 @@ class URLField(CharField):
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(URLField, self).__init__(**kwargs) super().__init__(**kwargs)
validator = URLValidator(message=self.error_messages['invalid']) validator = URLValidator(message=self.error_messages['invalid'])
self.validators.append(validator) self.validators.append(validator)
@ -866,16 +843,16 @@ class UUIDField(Field):
if self.uuid_format not in self.valid_formats: if self.uuid_format not in self.valid_formats:
raise ValueError( raise ValueError(
'Invalid format for uuid representation. ' 'Invalid format for uuid representation. '
'Must be one of "{0}"'.format('", "'.join(self.valid_formats)) 'Must be one of "{}"'.format('", "'.join(self.valid_formats))
) )
super(UUIDField, self).__init__(**kwargs) super().__init__(**kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
if not isinstance(data, uuid.UUID): if not isinstance(data, uuid.UUID):
try: try:
if isinstance(data, six.integer_types): if isinstance(data, int):
return uuid.UUID(int=data) return uuid.UUID(int=data)
elif isinstance(data, six.string_types): elif isinstance(data, str):
return uuid.UUID(hex=data) return uuid.UUID(hex=data)
else: else:
self.fail('invalid', value=data) self.fail('invalid', value=data)
@ -900,12 +877,12 @@ class IPAddressField(CharField):
def __init__(self, protocol='both', **kwargs): def __init__(self, protocol='both', **kwargs):
self.protocol = protocol.lower() self.protocol = protocol.lower()
self.unpack_ipv4 = (self.protocol == 'both') self.unpack_ipv4 = (self.protocol == 'both')
super(IPAddressField, self).__init__(**kwargs) super().__init__(**kwargs)
validators, error_message = ip_address_validators(protocol, self.unpack_ipv4) validators, error_message = ip_address_validators(protocol, self.unpack_ipv4)
self.validators.extend(validators) self.validators.extend(validators)
def to_internal_value(self, data): def to_internal_value(self, data):
if not isinstance(data, six.string_types): if not isinstance(data, str):
self.fail('invalid', value=data) self.fail('invalid', value=data)
if ':' in data: if ':' in data:
@ -915,7 +892,7 @@ class IPAddressField(CharField):
except DjangoValidationError: except DjangoValidationError:
self.fail('invalid', value=data) self.fail('invalid', value=data)
return super(IPAddressField, self).to_internal_value(data) return super().to_internal_value(data)
# Number types... # Number types...
@ -933,22 +910,18 @@ class IntegerField(Field):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.max_value = kwargs.pop('max_value', None) self.max_value = kwargs.pop('max_value', None)
self.min_value = kwargs.pop('min_value', None) self.min_value = kwargs.pop('min_value', None)
super(IntegerField, self).__init__(**kwargs) super().__init__(**kwargs)
if self.max_value is not None: if self.max_value is not None:
message = lazy( message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
self.error_messages['max_value'].format,
six.text_type)(max_value=self.max_value)
self.validators.append( self.validators.append(
MaxValueValidator(self.max_value, message=message)) MaxValueValidator(self.max_value, message=message))
if self.min_value is not None: if self.min_value is not None:
message = lazy( message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
self.error_messages['min_value'].format,
six.text_type)(min_value=self.min_value)
self.validators.append( self.validators.append(
MinValueValidator(self.min_value, message=message)) MinValueValidator(self.min_value, message=message))
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: if isinstance(data, str) and len(data) > self.MAX_STRING_LENGTH:
self.fail('max_string_length') self.fail('max_string_length')
try: try:
@ -973,23 +946,19 @@ class FloatField(Field):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.max_value = kwargs.pop('max_value', None) self.max_value = kwargs.pop('max_value', None)
self.min_value = kwargs.pop('min_value', None) self.min_value = kwargs.pop('min_value', None)
super(FloatField, self).__init__(**kwargs) super().__init__(**kwargs)
if self.max_value is not None: if self.max_value is not None:
message = lazy( message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
self.error_messages['max_value'].format,
six.text_type)(max_value=self.max_value)
self.validators.append( self.validators.append(
MaxValueValidator(self.max_value, message=message)) MaxValueValidator(self.max_value, message=message))
if self.min_value is not None: if self.min_value is not None:
message = lazy( message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
self.error_messages['min_value'].format,
six.text_type)(min_value=self.min_value)
self.validators.append( self.validators.append(
MinValueValidator(self.min_value, message=message)) MinValueValidator(self.min_value, message=message))
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: if isinstance(data, str) and len(data) > self.MAX_STRING_LENGTH:
self.fail('max_string_length') self.fail('max_string_length')
try: try:
@ -1031,18 +1000,14 @@ class DecimalField(Field):
else: else:
self.max_whole_digits = None self.max_whole_digits = None
super(DecimalField, self).__init__(**kwargs) super().__init__(**kwargs)
if self.max_value is not None: if self.max_value is not None:
message = lazy( message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
self.error_messages['max_value'].format,
six.text_type)(max_value=self.max_value)
self.validators.append( self.validators.append(
MaxValueValidator(self.max_value, message=message)) MaxValueValidator(self.max_value, message=message))
if self.min_value is not None: if self.min_value is not None:
message = lazy( message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
self.error_messages['min_value'].format,
six.text_type)(min_value=self.min_value)
self.validators.append( self.validators.append(
MinValueValidator(self.min_value, message=message)) MinValueValidator(self.min_value, message=message))
@ -1121,7 +1086,7 @@ class DecimalField(Field):
coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING) coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING)
if not isinstance(value, decimal.Decimal): if not isinstance(value, decimal.Decimal):
value = decimal.Decimal(six.text_type(value).strip()) value = decimal.Decimal(str(value).strip())
quantized = self.quantize(value) quantized = self.quantize(value)
@ -1130,7 +1095,7 @@ class DecimalField(Field):
if self.localize: if self.localize:
return localize_input(quantized) return localize_input(quantized)
return '{0:f}'.format(quantized) return '{:f}'.format(quantized)
def quantize(self, value): def quantize(self, value):
""" """
@ -1167,7 +1132,7 @@ class DateTimeField(Field):
self.input_formats = input_formats self.input_formats = input_formats
if default_timezone is not None: if default_timezone is not None:
self.timezone = default_timezone self.timezone = default_timezone
super(DateTimeField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def enforce_timezone(self, value): def enforce_timezone(self, value):
""" """
@ -1226,7 +1191,7 @@ class DateTimeField(Field):
output_format = getattr(self, 'format', api_settings.DATETIME_FORMAT) output_format = getattr(self, 'format', api_settings.DATETIME_FORMAT)
if output_format is None or isinstance(value, six.string_types): if output_format is None or isinstance(value, str):
return value return value
value = self.enforce_timezone(value) value = self.enforce_timezone(value)
@ -1251,7 +1216,7 @@ class DateField(Field):
self.format = format self.format = format
if input_formats is not None: if input_formats is not None:
self.input_formats = input_formats self.input_formats = input_formats
super(DateField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_internal_value(self, value): def to_internal_value(self, value):
input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS) input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS)
@ -1288,7 +1253,7 @@ class DateField(Field):
output_format = getattr(self, 'format', api_settings.DATE_FORMAT) output_format = getattr(self, 'format', api_settings.DATE_FORMAT)
if output_format is None or isinstance(value, six.string_types): if output_format is None or isinstance(value, str):
return value return value
# Applying a `DateField` to a datetime value is almost always # Applying a `DateField` to a datetime value is almost always
@ -1317,7 +1282,7 @@ class TimeField(Field):
self.format = format self.format = format
if input_formats is not None: if input_formats is not None:
self.input_formats = input_formats self.input_formats = input_formats
super(TimeField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_internal_value(self, value): def to_internal_value(self, value):
input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS) input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS)
@ -1351,7 +1316,7 @@ class TimeField(Field):
output_format = getattr(self, 'format', api_settings.TIME_FORMAT) output_format = getattr(self, 'format', api_settings.TIME_FORMAT)
if output_format is None or isinstance(value, six.string_types): if output_format is None or isinstance(value, str):
return value return value
# Applying a `TimeField` to a datetime value is almost always # Applying a `TimeField` to a datetime value is almost always
@ -1378,24 +1343,20 @@ class DurationField(Field):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.max_value = kwargs.pop('max_value', None) self.max_value = kwargs.pop('max_value', None)
self.min_value = kwargs.pop('min_value', None) self.min_value = kwargs.pop('min_value', None)
super(DurationField, self).__init__(**kwargs) super().__init__(**kwargs)
if self.max_value is not None: if self.max_value is not None:
message = lazy( message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
self.error_messages['max_value'].format,
six.text_type)(max_value=self.max_value)
self.validators.append( self.validators.append(
MaxValueValidator(self.max_value, message=message)) MaxValueValidator(self.max_value, message=message))
if self.min_value is not None: if self.min_value is not None:
message = lazy( message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
self.error_messages['min_value'].format,
six.text_type)(min_value=self.min_value)
self.validators.append( self.validators.append(
MinValueValidator(self.min_value, message=message)) MinValueValidator(self.min_value, message=message))
def to_internal_value(self, value): def to_internal_value(self, value):
if isinstance(value, datetime.timedelta): if isinstance(value, datetime.timedelta):
return value return value
parsed = parse_duration(six.text_type(value)) parsed = parse_duration(str(value))
if parsed is not None: if parsed is not None:
return parsed return parsed
self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]') self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]')
@ -1420,21 +1381,21 @@ class ChoiceField(Field):
self.allow_blank = kwargs.pop('allow_blank', False) self.allow_blank = kwargs.pop('allow_blank', False)
super(ChoiceField, self).__init__(**kwargs) super().__init__(**kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
if data == '' and self.allow_blank: if data == '' and self.allow_blank:
return '' return ''
try: try:
return self.choice_strings_to_values[six.text_type(data)] return self.choice_strings_to_values[str(data)]
except KeyError: except KeyError:
self.fail('invalid_choice', input=data) self.fail('invalid_choice', input=data)
def to_representation(self, value): def to_representation(self, value):
if value in ('', None): if value in ('', None):
return value return value
return self.choice_strings_to_values.get(six.text_type(value), value) return self.choice_strings_to_values.get(str(value), value)
def iter_options(self): def iter_options(self):
""" """
@ -1457,7 +1418,7 @@ class ChoiceField(Field):
# Allows us to deal with eg. integer choices while supporting either # Allows us to deal with eg. integer choices while supporting either
# integer or string input, but still get the correct datatype out. # integer or string input, but still get the correct datatype out.
self.choice_strings_to_values = { self.choice_strings_to_values = {
six.text_type(key): key for key in self.choices str(key): key for key in self.choices
} }
choices = property(_get_choices, _set_choices) choices = property(_get_choices, _set_choices)
@ -1473,7 +1434,7 @@ class MultipleChoiceField(ChoiceField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.allow_empty = kwargs.pop('allow_empty', True) self.allow_empty = kwargs.pop('allow_empty', True)
super(MultipleChoiceField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_value(self, dictionary): def get_value(self, dictionary):
if self.field_name not in dictionary: if self.field_name not in dictionary:
@ -1486,7 +1447,7 @@ class MultipleChoiceField(ChoiceField):
return dictionary.get(self.field_name, empty) return dictionary.get(self.field_name, empty)
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, six.text_type) or not hasattr(data, '__iter__'): if isinstance(data, str) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__) self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0: if not self.allow_empty and len(data) == 0:
self.fail('empty') self.fail('empty')
@ -1498,7 +1459,7 @@ class MultipleChoiceField(ChoiceField):
def to_representation(self, value): def to_representation(self, value):
return { return {
self.choice_strings_to_values.get(six.text_type(item), item) for item in value self.choice_strings_to_values.get(str(item), item) for item in value
} }
@ -1516,7 +1477,7 @@ class FilePathField(ChoiceField):
allow_folders=allow_folders, required=required allow_folders=allow_folders, required=required
) )
kwargs['choices'] = field.choices kwargs['choices'] = field.choices
super(FilePathField, self).__init__(**kwargs) super().__init__(**kwargs)
# File types... # File types...
@ -1535,7 +1496,7 @@ class FileField(Field):
self.allow_empty_file = kwargs.pop('allow_empty_file', False) self.allow_empty_file = kwargs.pop('allow_empty_file', False)
if 'use_url' in kwargs: if 'use_url' in kwargs:
self.use_url = kwargs.pop('use_url') self.use_url = kwargs.pop('use_url')
super(FileField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
try: try:
@ -1581,13 +1542,13 @@ class ImageField(FileField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField) self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField)
super(ImageField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
# Image validation is a bit grungy, so we'll just outright # Image validation is a bit grungy, so we'll just outright
# defer to Django's implementation so we don't need to # defer to Django's implementation so we don't need to
# consider it, or treat PIL as a test dependency. # consider it, or treat PIL as a test dependency.
file_object = super(ImageField, self).to_internal_value(data) file_object = super().to_internal_value(data)
django_field = self._DjangoImageField() django_field = self._DjangoImageField()
django_field.error_messages = self.error_messages django_field.error_messages = self.error_messages
return django_field.clean(file_object) return django_field.clean(file_object)
@ -1597,7 +1558,7 @@ class ImageField(FileField):
class _UnvalidatedField(Field): class _UnvalidatedField(Field):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(_UnvalidatedField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.allow_blank = True self.allow_blank = True
self.allow_null = True self.allow_null = True
@ -1630,13 +1591,13 @@ class ListField(Field):
"Remove `source=` from the field declaration." "Remove `source=` from the field declaration."
) )
super(ListField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.child.bind(field_name='', parent=self) self.child.bind(field_name='', parent=self)
if self.max_length is not None: if self.max_length is not None:
message = self.error_messages['max_length'].format(max_length=self.max_length) message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
self.validators.append(MaxLengthValidator(self.max_length, message=message)) self.validators.append(MaxLengthValidator(self.max_length, message=message))
if self.min_length is not None: if self.min_length is not None:
message = self.error_messages['min_length'].format(min_length=self.min_length) message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
self.validators.append(MinLengthValidator(self.min_length, message=message)) self.validators.append(MinLengthValidator(self.min_length, message=message))
def get_value(self, dictionary): def get_value(self, dictionary):
@ -1660,7 +1621,7 @@ class ListField(Field):
""" """
if html.is_html_input(data): if html.is_html_input(data):
data = html.parse_html_list(data, default=[]) data = html.parse_html_list(data, default=[])
if isinstance(data, (six.text_type, Mapping)) or not hasattr(data, '__iter__'): if isinstance(data, (str, Mapping)) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__) self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0: if not self.allow_empty and len(data) == 0:
self.fail('empty') self.fail('empty')
@ -1691,11 +1652,13 @@ class DictField(Field):
child = _UnvalidatedField() child = _UnvalidatedField()
initial = {} initial = {}
default_error_messages = { default_error_messages = {
'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".') 'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".'),
'empty': _('This dictionary may not be empty.'),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.child = kwargs.pop('child', copy.deepcopy(self.child))
self.allow_empty = kwargs.pop('allow_empty', True)
assert not inspect.isclass(self.child), '`child` has not been instantiated.' assert not inspect.isclass(self.child), '`child` has not been instantiated.'
assert self.child.source is None, ( assert self.child.source is None, (
@ -1703,7 +1666,7 @@ class DictField(Field):
"Remove `source=` from the field declaration." "Remove `source=` from the field declaration."
) )
super(DictField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.child.bind(field_name='', parent=self) self.child.bind(field_name='', parent=self)
def get_value(self, dictionary): def get_value(self, dictionary):
@ -1721,11 +1684,14 @@ class DictField(Field):
data = html.parse_html_dict(data) data = html.parse_html_dict(data)
if not isinstance(data, dict): if not isinstance(data, dict):
self.fail('not_a_dict', input_type=type(data).__name__) self.fail('not_a_dict', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')
return self.run_child_validation(data) return self.run_child_validation(data)
def to_representation(self, value): def to_representation(self, value):
return { return {
six.text_type(key): self.child.to_representation(val) if val is not None else None str(key): self.child.to_representation(val) if val is not None else None
for key, val in value.items() for key, val in value.items()
} }
@ -1734,7 +1700,7 @@ class DictField(Field):
errors = OrderedDict() errors = OrderedDict()
for key, value in data.items(): for key, value in data.items():
key = six.text_type(key) key = str(key)
try: try:
result[key] = self.child.run_validation(value) result[key] = self.child.run_validation(value)
@ -1750,7 +1716,7 @@ class HStoreField(DictField):
child = CharField(allow_blank=True, allow_null=True) child = CharField(allow_blank=True, allow_null=True)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(HStoreField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
assert isinstance(self.child, CharField), ( assert isinstance(self.child, CharField), (
"The `child` argument must be an instance of `CharField`, " "The `child` argument must be an instance of `CharField`, "
"as the hstore extension stores values as strings." "as the hstore extension stores values as strings."
@ -1764,15 +1730,16 @@ class JSONField(Field):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.binary = kwargs.pop('binary', False) self.binary = kwargs.pop('binary', False)
super(JSONField, self).__init__(*args, **kwargs) self.encoder = kwargs.pop('encoder', None)
super().__init__(*args, **kwargs)
def get_value(self, dictionary): def get_value(self, dictionary):
if html.is_html_input(dictionary) and self.field_name in dictionary: if html.is_html_input(dictionary) and self.field_name in dictionary:
# When HTML form input is used, mark up the input # When HTML form input is used, mark up the input
# as being a JSON string, rather than a JSON primitive. # as being a JSON string, rather than a JSON primitive.
class JSONString(six.text_type): class JSONString(str):
def __new__(self, value): def __new__(self, value):
ret = six.text_type.__new__(self, value) ret = str.__new__(self, value)
ret.is_json_string = True ret.is_json_string = True
return ret return ret
return JSONString(dictionary[self.field_name]) return JSONString(dictionary[self.field_name])
@ -1782,21 +1749,18 @@ class JSONField(Field):
try: try:
if self.binary or getattr(data, 'is_json_string', False): if self.binary or getattr(data, 'is_json_string', False):
if isinstance(data, bytes): if isinstance(data, bytes):
data = data.decode('utf-8') data = data.decode()
return json.loads(data) return json.loads(data)
else: else:
json.dumps(data) json.dumps(data, cls=self.encoder)
except (TypeError, ValueError): except (TypeError, ValueError):
self.fail('invalid') self.fail('invalid')
return data return data
def to_representation(self, value): def to_representation(self, value):
if self.binary: if self.binary:
value = json.dumps(value) value = json.dumps(value, cls=self.encoder)
# On python 2.x the return type for json.dumps() is underspecified. value = value.encode()
# On python 3.x json.dumps() returns unicode strings.
if isinstance(value, six.text_type):
value = bytes(value.encode('utf-8'))
return value return value
@ -1817,7 +1781,7 @@ class ReadOnlyField(Field):
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs['read_only'] = True kwargs['read_only'] = True
super(ReadOnlyField, self).__init__(**kwargs) super().__init__(**kwargs)
def to_representation(self, value): def to_representation(self, value):
return value return value
@ -1834,7 +1798,7 @@ class HiddenField(Field):
def __init__(self, **kwargs): def __init__(self, **kwargs):
assert 'default' in kwargs, 'default is a required argument.' assert 'default' in kwargs, 'default is a required argument.'
kwargs['write_only'] = True kwargs['write_only'] = True
super(HiddenField, self).__init__(**kwargs) super().__init__(**kwargs)
def get_value(self, dictionary): def get_value(self, dictionary):
# We always use the default value for `HiddenField`. # We always use the default value for `HiddenField`.
@ -1864,7 +1828,7 @@ class SerializerMethodField(Field):
self.method_name = method_name self.method_name = method_name
kwargs['source'] = '*' kwargs['source'] = '*'
kwargs['read_only'] = True kwargs['read_only'] = True
super(SerializerMethodField, self).__init__(**kwargs) super().__init__(**kwargs)
def bind(self, field_name, parent): def bind(self, field_name, parent):
# In order to enforce a consistent style, we error if a redundant # In order to enforce a consistent style, we error if a redundant
@ -1882,7 +1846,7 @@ class SerializerMethodField(Field):
if self.method_name is None: if self.method_name is None:
self.method_name = default_method_name self.method_name = default_method_name
super(SerializerMethodField, self).bind(field_name, parent) super().bind(field_name, parent)
def to_representation(self, value): def to_representation(self, value):
method = getattr(self.parent, self.method_name) method = getattr(self.parent, self.method_name)
@ -1905,11 +1869,9 @@ class ModelField(Field):
# The `max_length` option is supported by Django's base `Field` class, # The `max_length` option is supported by Django's base `Field` class,
# so we'd better support it here. # so we'd better support it here.
max_length = kwargs.pop('max_length', None) max_length = kwargs.pop('max_length', None)
super(ModelField, self).__init__(**kwargs) super().__init__(**kwargs)
if max_length is not None: if max_length is not None:
message = lazy( message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
self.error_messages['max_length'].format,
six.text_type)(max_length=self.max_length)
self.validators.append( self.validators.append(
MaxLengthValidator(self.max_length, message=message)) MaxLengthValidator(self.max_length, message=message))

View File

@ -2,10 +2,7 @@
Provides generic filtering backends that can be used to filter the results Provides generic filtering backends that can be used to filter the results
returned by list views. returned by list views.
""" """
from __future__ import unicode_literals
import operator import operator
import warnings
from functools import reduce from functools import reduce
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -13,18 +10,14 @@ from django.db import models
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.db.models.sql.constants import ORDER_PATTERN from django.db.models.sql.constants import ORDER_PATTERN
from django.template import loader from django.template import loader
from django.utils import six
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import RemovedInDRF310Warning from rest_framework.compat import coreapi, coreschema, distinct
from rest_framework.compat import (
coreapi, coreschema, distinct, is_guardian_installed
)
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
class BaseFilterBackend(object): class BaseFilterBackend:
""" """
A base class from which all filter backend classes should inherit. A base class from which all filter backend classes should inherit.
""" """
@ -40,6 +33,9 @@ class BaseFilterBackend(object):
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
return [] return []
def get_schema_operation_parameters(self, view):
return []
class SearchFilter(BaseFilterBackend): class SearchFilter(BaseFilterBackend):
# The URL query parameter used for the search. # The URL query parameter used for the search.
@ -109,7 +105,7 @@ class SearchFilter(BaseFilterBackend):
return queryset return queryset
orm_lookups = [ orm_lookups = [
self.construct_search(six.text_type(search_field)) self.construct_search(str(search_field))
for search_field in search_fields for search_field in search_fields
] ]
@ -159,6 +155,19 @@ class SearchFilter(BaseFilterBackend):
) )
] ]
def get_schema_operation_parameters(self, view):
return [
{
'name': self.search_param,
'required': False,
'in': 'query',
'description': force_text(self.search_description),
'schema': {
'type': 'string',
},
},
]
class OrderingFilter(BaseFilterBackend): class OrderingFilter(BaseFilterBackend):
# The URL query parameter used for the ordering. # The URL query parameter used for the ordering.
@ -188,7 +197,7 @@ class OrderingFilter(BaseFilterBackend):
def get_default_ordering(self, view): def get_default_ordering(self, view):
ordering = getattr(view, 'ordering', None) ordering = getattr(view, 'ordering', None)
if isinstance(ordering, six.string_types): if isinstance(ordering, str):
return (ordering,) return (ordering,)
return ordering return ordering
@ -237,7 +246,7 @@ class OrderingFilter(BaseFilterBackend):
] ]
else: else:
valid_fields = [ valid_fields = [
(item, item) if isinstance(item, six.string_types) else item (item, item) if isinstance(item, str) else item
for item in valid_fields for item in valid_fields
] ]
@ -290,40 +299,15 @@ class OrderingFilter(BaseFilterBackend):
) )
] ]
def get_schema_operation_parameters(self, view):
class DjangoObjectPermissionsFilter(BaseFilterBackend): return [
""" {
A filter backend that limits results to those where the requesting user 'name': self.ordering_param,
has read object level permissions. 'required': False,
""" 'in': 'query',
def __init__(self): 'description': force_text(self.ordering_description),
warnings.warn( 'schema': {
"`DjangoObjectPermissionsFilter` has been deprecated and moved to " 'type': 'string',
"the 3rd-party django-rest-framework-guardian package.", },
RemovedInDRF310Warning, stacklevel=2 },
) ]
assert is_guardian_installed(), 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed'
perm_format = '%(app_label)s.view_%(model_name)s'
def filter_queryset(self, request, queryset, view):
# We want to defer this import until run-time, rather than import-time.
# See https://github.com/encode/django-rest-framework/issues/4608
# (Also see #1624 for why we need to make this import explicitly)
from guardian import VERSION as guardian_version
from guardian.shortcuts import get_objects_for_user
extra = {}
user = request.user
model_cls = queryset.model
kwargs = {
'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.model_name
}
permission = self.perm_format % kwargs
if tuple(guardian_version) >= (1, 3):
# Maintain behavior compatibility with versions prior to 1.3
extra = {'accept_global_perms': False}
else:
extra = {}
return get_objects_for_user(user, permission, queryset, **extra)

View File

@ -1,8 +1,6 @@
""" """
Generic views that provide commonly needed behaviour. Generic views that provide commonly needed behaviour.
""" """
from __future__ import unicode_literals
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.http import Http404 from django.http import Http404

View File

@ -1,41 +1,56 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from rest_framework.compat import coreapi from rest_framework import renderers
from rest_framework.renderers import ( from rest_framework.schemas import coreapi
CoreJSONRenderer, JSONOpenAPIRenderer, OpenAPIRenderer from rest_framework.schemas.openapi import SchemaGenerator
)
from rest_framework.schemas.generators import SchemaGenerator OPENAPI_MODE = 'openapi'
COREAPI_MODE = 'coreapi'
class Command(BaseCommand): class Command(BaseCommand):
help = "Generates configured API schema for project." help = "Generates configured API schema for project."
def get_mode(self):
return COREAPI_MODE if coreapi.is_enabled() else OPENAPI_MODE
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('--title', dest="title", default=None, type=str) parser.add_argument('--title', dest="title", default='', type=str)
parser.add_argument('--url', dest="url", default=None, type=str) parser.add_argument('--url', dest="url", default=None, type=str)
parser.add_argument('--description', dest="description", default=None, type=str) parser.add_argument('--description', dest="description", default=None, type=str)
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str) if self.get_mode() == COREAPI_MODE:
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
else:
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json'], default='openapi', type=str)
def handle(self, *args, **options): def handle(self, *args, **options):
assert coreapi is not None, 'coreapi must be installed.' generator_class = self.get_generator_class()
generator = generator_class(
generator = SchemaGenerator(
url=options['url'], url=options['url'],
title=options['title'], title=options['title'],
description=options['description'] description=options['description']
) )
schema = generator.get_schema(request=None, public=True) schema = generator.get_schema(request=None, public=True)
renderer = self.get_renderer(options['format']) renderer = self.get_renderer(options['format'])
output = renderer.render(schema, renderer_context={}) output = renderer.render(schema, renderer_context={})
self.stdout.write(output.decode('utf-8')) self.stdout.write(output.decode())
def get_renderer(self, format): def get_renderer(self, format):
renderer_cls = { if self.get_mode() == COREAPI_MODE:
'corejson': CoreJSONRenderer, renderer_cls = {
'openapi': OpenAPIRenderer, 'corejson': renderers.CoreJSONRenderer,
'openapi-json': JSONOpenAPIRenderer, 'openapi': renderers.CoreAPIOpenAPIRenderer,
}[format] 'openapi-json': renderers.CoreAPIJSONOpenAPIRenderer,
}[format]
return renderer_cls()
renderer_cls = {
'openapi': renderers.OpenAPIRenderer,
'openapi-json': renderers.JSONOpenAPIRenderer,
}[format]
return renderer_cls() return renderer_cls()
def get_generator_class(self):
if self.get_mode() == COREAPI_MODE:
return coreapi.SchemaGenerator
return SchemaGenerator

View File

@ -6,8 +6,6 @@ some fairly ad-hoc information about the view.
Future implementations might use JSON schema or other definitions in order Future implementations might use JSON schema or other definitions in order
to return this information in a more standardized way. to return this information in a more standardized way.
""" """
from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@ -19,7 +17,7 @@ from rest_framework.request import clone_request
from rest_framework.utils.field_mapping import ClassLookupDict from rest_framework.utils.field_mapping import ClassLookupDict
class BaseMetadata(object): class BaseMetadata:
def determine_metadata(self, request, view): def determine_metadata(self, request, view):
""" """
Return a dictionary of metadata about the view. Return a dictionary of metadata about the view.

View File

@ -4,14 +4,12 @@ Basic building blocks for generic class based views.
We don't bind behaviour to http method handlers yet, We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways. which allows mixin classes to be composed in interesting ways.
""" """
from __future__ import unicode_literals
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
class CreateModelMixin(object): class CreateModelMixin:
""" """
Create a model instance. Create a model instance.
""" """
@ -32,7 +30,7 @@ class CreateModelMixin(object):
return {} return {}
class ListModelMixin(object): class ListModelMixin:
""" """
List a queryset. List a queryset.
""" """
@ -48,7 +46,7 @@ class ListModelMixin(object):
return Response(serializer.data) return Response(serializer.data)
class RetrieveModelMixin(object): class RetrieveModelMixin:
""" """
Retrieve a model instance. Retrieve a model instance.
""" """
@ -58,7 +56,7 @@ class RetrieveModelMixin(object):
return Response(serializer.data) return Response(serializer.data)
class UpdateModelMixin(object): class UpdateModelMixin:
""" """
Update a model instance. Update a model instance.
""" """
@ -84,7 +82,7 @@ class UpdateModelMixin(object):
return self.update(request, *args, **kwargs) return self.update(request, *args, **kwargs)
class DestroyModelMixin(object): class DestroyModelMixin:
""" """
Destroy a model instance. Destroy a model instance.
""" """

View File

@ -2,8 +2,6 @@
Content negotiation deals with selecting an appropriate renderer given the Content negotiation deals with selecting an appropriate renderer given the
incoming request. Typically this will be based on the request's Accept header. incoming request. Typically this will be based on the request's Accept header.
""" """
from __future__ import unicode_literals
from django.http import Http404 from django.http import Http404
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework import HTTP_HEADER_ENCODING, exceptions
@ -13,7 +11,7 @@ from rest_framework.utils.mediatypes import (
) )
class BaseContentNegotiation(object): class BaseContentNegotiation:
def select_parser(self, request, parsers): def select_parser(self, request, parsers):
raise NotImplementedError('.select_parser() must be implemented') raise NotImplementedError('.select_parser() must be implemented')
@ -66,7 +64,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
# Accepted media type is 'application/json' # Accepted media type is 'application/json'
full_media_type = ';'.join( full_media_type = ';'.join(
(renderer.media_type,) + (renderer.media_type,) +
tuple('{0}={1}'.format( tuple('{}={}'.format(
key, value.decode(HTTP_HEADER_ENCODING)) key, value.decode(HTTP_HEADER_ENCODING))
for key, value in media_type_wrapper.params.items())) for key, value in media_type_wrapper.params.items()))
return renderer, full_media_type return renderer, full_media_type

View File

@ -1,20 +1,16 @@
# coding: utf-8
""" """
Pagination serializers determine the structure of the output that should Pagination serializers determine the structure of the output that should
be used for paginated responses. be used for paginated responses.
""" """
from __future__ import unicode_literals
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from urllib import parse
from django.core.paginator import InvalidPage from django.core.paginator import InvalidPage
from django.core.paginator import Paginator as DjangoPaginator from django.core.paginator import Paginator as DjangoPaginator
from django.template import loader from django.template import loader
from django.utils import six
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import coreapi, coreschema from rest_framework.compat import coreapi, coreschema
from rest_framework.exceptions import NotFound from rest_framework.exceptions import NotFound
@ -133,7 +129,7 @@ PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True) PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True)
class BasePagination(object): class BasePagination:
display_page_controls = False display_page_controls = False
def paginate_queryset(self, queryset, request, view=None): # pragma: no cover def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
@ -152,6 +148,9 @@ class BasePagination(object):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
return [] return []
def get_schema_operation_parameters(self, view):
return []
class PageNumberPagination(BasePagination): class PageNumberPagination(BasePagination):
""" """
@ -204,7 +203,7 @@ class PageNumberPagination(BasePagination):
self.page = paginator.page(page_number) self.page = paginator.page(page_number)
except InvalidPage as exc: except InvalidPage as exc:
msg = self.invalid_page_message.format( msg = self.invalid_page_message.format(
page_number=page_number, message=six.text_type(exc) page_number=page_number, message=str(exc)
) )
raise NotFound(msg) raise NotFound(msg)
@ -305,6 +304,32 @@ class PageNumberPagination(BasePagination):
) )
return fields return fields
def get_schema_operation_parameters(self, view):
parameters = [
{
'name': self.page_query_param,
'required': False,
'in': 'query',
'description': force_text(self.page_query_description),
'schema': {
'type': 'integer',
},
},
]
if self.page_size_query_param is not None:
parameters.append(
{
'name': self.page_size_query_param,
'required': False,
'in': 'query',
'description': force_text(self.page_size_query_description),
'schema': {
'type': 'integer',
},
},
)
return parameters
class LimitOffsetPagination(BasePagination): class LimitOffsetPagination(BasePagination):
""" """
@ -434,6 +459,15 @@ class LimitOffsetPagination(BasePagination):
context = self.get_html_context() context = self.get_html_context()
return template.render(context) return template.render(context)
def get_count(self, queryset):
"""
Determine an object count, supporting either querysets or regular lists.
"""
try:
return queryset.count()
except (AttributeError, TypeError):
return len(queryset)
def get_schema_fields(self, view): def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
@ -458,14 +492,28 @@ class LimitOffsetPagination(BasePagination):
) )
] ]
def get_count(self, queryset): def get_schema_operation_parameters(self, view):
""" parameters = [
Determine an object count, supporting either querysets or regular lists. {
""" 'name': self.limit_query_param,
try: 'required': False,
return queryset.count() 'in': 'query',
except (AttributeError, TypeError): 'description': force_text(self.limit_query_description),
return len(queryset) 'schema': {
'type': 'integer',
},
},
{
'name': self.offset_query_param,
'required': False,
'in': 'query',
'description': force_text(self.offset_query_description),
'schema': {
'type': 'integer',
},
},
]
return parameters
class CursorPagination(BasePagination): class CursorPagination(BasePagination):
@ -589,7 +637,7 @@ class CursorPagination(BasePagination):
if not self.has_next: if not self.has_next:
return None return None
if self.cursor and self.cursor.reverse and self.cursor.offset != 0: if self.page and self.cursor and self.cursor.reverse and self.cursor.offset != 0:
# If we're reversing direction and we have an offset cursor # If we're reversing direction and we have an offset cursor
# then we cannot use the first position we find as a marker. # then we cannot use the first position we find as a marker.
compare = self._get_position_from_instance(self.page[-1], self.ordering) compare = self._get_position_from_instance(self.page[-1], self.ordering)
@ -597,12 +645,14 @@ class CursorPagination(BasePagination):
compare = self.next_position compare = self.next_position
offset = 0 offset = 0
has_item_with_unique_position = False
for item in reversed(self.page): for item in reversed(self.page):
position = self._get_position_from_instance(item, self.ordering) position = self._get_position_from_instance(item, self.ordering)
if position != compare: if position != compare:
# The item in this position and the item following it # The item in this position and the item following it
# have different positions. We can use this position as # have different positions. We can use this position as
# our marker. # our marker.
has_item_with_unique_position = True
break break
# The item in this position has the same position as the item # The item in this position has the same position as the item
@ -611,7 +661,7 @@ class CursorPagination(BasePagination):
compare = position compare = position
offset += 1 offset += 1
else: if self.page and not has_item_with_unique_position:
# There were no unique positions in the page. # There were no unique positions in the page.
if not self.has_previous: if not self.has_previous:
# We are on the first page. # We are on the first page.
@ -630,6 +680,9 @@ class CursorPagination(BasePagination):
offset = self.cursor.offset + self.page_size offset = self.cursor.offset + self.page_size
position = self.previous_position position = self.previous_position
if not self.page:
position = self.next_position
cursor = Cursor(offset=offset, reverse=False, position=position) cursor = Cursor(offset=offset, reverse=False, position=position)
return self.encode_cursor(cursor) return self.encode_cursor(cursor)
@ -637,7 +690,7 @@ class CursorPagination(BasePagination):
if not self.has_previous: if not self.has_previous:
return None return None
if self.cursor and not self.cursor.reverse and self.cursor.offset != 0: if self.page and self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
# If we're reversing direction and we have an offset cursor # If we're reversing direction and we have an offset cursor
# then we cannot use the first position we find as a marker. # then we cannot use the first position we find as a marker.
compare = self._get_position_from_instance(self.page[0], self.ordering) compare = self._get_position_from_instance(self.page[0], self.ordering)
@ -645,12 +698,14 @@ class CursorPagination(BasePagination):
compare = self.previous_position compare = self.previous_position
offset = 0 offset = 0
has_item_with_unique_position = False
for item in self.page: for item in self.page:
position = self._get_position_from_instance(item, self.ordering) position = self._get_position_from_instance(item, self.ordering)
if position != compare: if position != compare:
# The item in this position and the item following it # The item in this position and the item following it
# have different positions. We can use this position as # have different positions. We can use this position as
# our marker. # our marker.
has_item_with_unique_position = True
break break
# The item in this position has the same position as the item # The item in this position has the same position as the item
@ -659,7 +714,7 @@ class CursorPagination(BasePagination):
compare = position compare = position
offset += 1 offset += 1
else: if self.page and not has_item_with_unique_position:
# There were no unique positions in the page. # There were no unique positions in the page.
if not self.has_next: if not self.has_next:
# We are on the final page. # We are on the final page.
@ -678,6 +733,9 @@ class CursorPagination(BasePagination):
offset = 0 offset = 0
position = self.next_position position = self.next_position
if not self.page:
position = self.previous_position
cursor = Cursor(offset=offset, reverse=True, position=position) cursor = Cursor(offset=offset, reverse=True, position=position)
return self.encode_cursor(cursor) return self.encode_cursor(cursor)
@ -716,13 +774,13 @@ class CursorPagination(BasePagination):
'nearly-unique field on the model, such as "-created" or "pk".' 'nearly-unique field on the model, such as "-created" or "pk".'
) )
assert isinstance(ordering, (six.string_types, list, tuple)), ( assert isinstance(ordering, (str, list, tuple)), (
'Invalid ordering. Expected string or tuple, but got {type}'.format( 'Invalid ordering. Expected string or tuple, but got {type}'.format(
type=type(ordering).__name__ type=type(ordering).__name__
) )
) )
if isinstance(ordering, six.string_types): if isinstance(ordering, str):
return (ordering,) return (ordering,)
return tuple(ordering) return tuple(ordering)
@ -737,7 +795,7 @@ class CursorPagination(BasePagination):
try: try:
querystring = b64decode(encoded.encode('ascii')).decode('ascii') querystring = b64decode(encoded.encode('ascii')).decode('ascii')
tokens = urlparse.parse_qs(querystring, keep_blank_values=True) tokens = parse.parse_qs(querystring, keep_blank_values=True)
offset = tokens.get('o', ['0'])[0] offset = tokens.get('o', ['0'])[0]
offset = _positive_int(offset, cutoff=self.offset_cutoff) offset = _positive_int(offset, cutoff=self.offset_cutoff)
@ -763,7 +821,7 @@ class CursorPagination(BasePagination):
if cursor.position is not None: if cursor.position is not None:
tokens['p'] = cursor.position tokens['p'] = cursor.position
querystring = urlparse.urlencode(tokens, doseq=True) querystring = parse.urlencode(tokens, doseq=True)
encoded = b64encode(querystring.encode('ascii')).decode('ascii') encoded = b64encode(querystring.encode('ascii')).decode('ascii')
return replace_query_param(self.base_url, self.cursor_query_param, encoded) return replace_query_param(self.base_url, self.cursor_query_param, encoded)
@ -773,7 +831,7 @@ class CursorPagination(BasePagination):
attr = instance[field_name] attr = instance[field_name]
else: else:
attr = getattr(instance, field_name) attr = getattr(instance, field_name)
return six.text_type(attr) return str(attr)
def get_paginated_response(self, data): def get_paginated_response(self, data):
return Response(OrderedDict([ return Response(OrderedDict([
@ -820,3 +878,29 @@ class CursorPagination(BasePagination):
) )
) )
return fields return fields
def get_schema_operation_parameters(self, view):
parameters = [
{
'name': self.cursor_query_param,
'required': False,
'in': 'query',
'description': force_text(self.cursor_query_description),
'schema': {
'type': 'integer',
},
}
]
if self.page_size_query_param is not None:
parameters.append(
{
'name': self.page_size_query_param,
'required': False,
'in': 'query',
'description': force_text(self.page_size_query_description),
'schema': {
'type': 'integer',
},
}
)
return parameters

View File

@ -4,9 +4,8 @@ Parsers are used to parse the content of incoming HTTP requests.
They give us a generic way of being able to handle various media types They give us a generic way of being able to handle various media types
on the request, such as form content or json encoded data. on the request, such as form content or json encoded data.
""" """
from __future__ import unicode_literals
import codecs import codecs
from urllib import parse
from django.conf import settings from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers from django.core.files.uploadhandler import StopFutureHandlers
@ -15,9 +14,7 @@ from django.http.multipartparser import ChunkIter
from django.http.multipartparser import \ from django.http.multipartparser import \
MultiPartParser as DjangoMultiPartParser MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header from django.http.multipartparser import MultiPartParserError, parse_header
from django.utils import six
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.six.moves.urllib import parse as urlparse
from rest_framework import renderers from rest_framework import renderers
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
@ -25,13 +22,13 @@ from rest_framework.settings import api_settings
from rest_framework.utils import json from rest_framework.utils import json
class DataAndFiles(object): class DataAndFiles:
def __init__(self, data, files): def __init__(self, data, files):
self.data = data self.data = data
self.files = files self.files = files
class BaseParser(object): class BaseParser:
""" """
All parsers should extend `BaseParser`, specifying a `media_type` All parsers should extend `BaseParser`, specifying a `media_type`
attribute, and overriding the `.parse()` method. attribute, and overriding the `.parse()` method.
@ -67,7 +64,7 @@ class JSONParser(BaseParser):
parse_constant = json.strict_constant if self.strict else None parse_constant = json.strict_constant if self.strict else None
return json.load(decoded_stream, parse_constant=parse_constant) return json.load(decoded_stream, parse_constant=parse_constant)
except ValueError as exc: except ValueError as exc:
raise ParseError('JSON parse error - %s' % six.text_type(exc)) raise ParseError('JSON parse error - %s' % str(exc))
class FormParser(BaseParser): class FormParser(BaseParser):
@ -83,8 +80,7 @@ class FormParser(BaseParser):
""" """
parser_context = parser_context or {} parser_context = parser_context or {}
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
data = QueryDict(stream.read(), encoding=encoding) return QueryDict(stream.read(), encoding=encoding)
return data
class MultiPartParser(BaseParser): class MultiPartParser(BaseParser):
@ -113,7 +109,7 @@ class MultiPartParser(BaseParser):
data, files = parser.parse() data, files = parser.parse()
return DataAndFiles(data, files) return DataAndFiles(data, files)
except MultiPartParserError as exc: except MultiPartParserError as exc:
raise ParseError('Multipart form parse error - %s' % six.text_type(exc)) raise ParseError('Multipart form parse error - %s' % str(exc))
class FileUploadParser(BaseParser): class FileUploadParser(BaseParser):
@ -205,7 +201,7 @@ class FileUploadParser(BaseParser):
try: try:
meta = parser_context['request'].META meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8')) disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode())
filename_parm = disposition[1] filename_parm = disposition[1]
if 'filename*' in filename_parm: if 'filename*' in filename_parm:
return self.get_encoded_filename(filename_parm) return self.get_encoded_filename(filename_parm)
@ -221,7 +217,7 @@ class FileUploadParser(BaseParser):
encoded_filename = force_text(filename_parm['filename*']) encoded_filename = force_text(filename_parm['filename*'])
try: try:
charset, lang, filename = encoded_filename.split('\'', 2) charset, lang, filename = encoded_filename.split('\'', 2)
filename = urlparse.unquote(filename) filename = parse.unquote(filename)
except (ValueError, LookupError): except (ValueError, LookupError):
filename = force_text(filename_parm['filename']) filename = force_text(filename_parm['filename'])
return filename return filename

View File

@ -1,10 +1,7 @@
""" """
Provides a set of pluggable permission policies. Provides a set of pluggable permission policies.
""" """
from __future__ import unicode_literals
from django.http import Http404 from django.http import Http404
from django.utils import six
from rest_framework import exceptions from rest_framework import exceptions
@ -101,8 +98,7 @@ class BasePermissionMetaclass(OperationHolderMixin, type):
pass pass
@six.add_metaclass(BasePermissionMetaclass) class BasePermission(metaclass=BasePermissionMetaclass):
class BasePermission(object):
""" """
A base class from which all permission classes should inherit. A base class from which all permission classes should inherit.
""" """

View File

@ -1,19 +1,13 @@
# coding: utf-8
from __future__ import unicode_literals
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from urllib import parse
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.db.models import Manager from django.db.models import Manager
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve
from django.utils import six from django.utils.encoding import smart_text, uri_to_iri
from django.utils.encoding import ( from django.utils.translation import gettext_lazy as _
python_2_unicode_compatible, smart_text, uri_to_iri
)
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _
from rest_framework.fields import ( from rest_framework.fields import (
Field, empty, get_attribute, is_simple_callable, iter_options Field, empty, get_attribute, is_simple_callable, iter_options
@ -46,14 +40,14 @@ class ObjectTypeError(TypeError):
""" """
class Hyperlink(six.text_type): class Hyperlink(str):
""" """
A string like object that additionally has an associated name. A string like object that additionally has an associated name.
We use this for hyperlinked URLs that may render as a named link We use this for hyperlinked URLs that may render as a named link
in some contexts, or render as a plain URL in others. in some contexts, or render as a plain URL in others.
""" """
def __new__(self, url, obj): def __new__(self, url, obj):
ret = six.text_type.__new__(self, url) ret = str.__new__(self, url)
ret.obj = obj ret.obj = obj
return ret return ret
@ -65,13 +59,12 @@ class Hyperlink(six.text_type):
# This ensures that we only called `__str__` lazily, # This ensures that we only called `__str__` lazily,
# as in some cases calling __str__ on a model instances *might* # as in some cases calling __str__ on a model instances *might*
# involve a database lookup. # involve a database lookup.
return six.text_type(self.obj) return str(self.obj)
is_hyperlink = True is_hyperlink = True
@python_2_unicode_compatible class PKOnlyObject:
class PKOnlyObject(object):
""" """
This is a mock object, used for when we only need the pk of the object This is a mock object, used for when we only need the pk of the object
instance, but still want to return an object with a .pk attribute, instance, but still want to return an object with a .pk attribute,
@ -121,14 +114,14 @@ class RelatedField(Field):
) )
kwargs.pop('many', None) kwargs.pop('many', None)
kwargs.pop('allow_empty', None) kwargs.pop('allow_empty', None)
super(RelatedField, self).__init__(**kwargs) super().__init__(**kwargs)
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create # We override this method in order to automagically create
# `ManyRelatedField` classes instead when `many=True` is set. # `ManyRelatedField` classes instead when `many=True` is set.
if kwargs.pop('many', False): if kwargs.pop('many', False):
return cls.many_init(*args, **kwargs) return cls.many_init(*args, **kwargs)
return super(RelatedField, cls).__new__(cls, *args, **kwargs) return super().__new__(cls, *args, **kwargs)
@classmethod @classmethod
def many_init(cls, *args, **kwargs): def many_init(cls, *args, **kwargs):
@ -157,7 +150,7 @@ class RelatedField(Field):
# We force empty strings to None values for relational fields. # We force empty strings to None values for relational fields.
if data == '': if data == '':
data = None data = None
return super(RelatedField, self).run_validation(data) return super().run_validation(data)
def get_queryset(self): def get_queryset(self):
queryset = self.queryset queryset = self.queryset
@ -189,7 +182,7 @@ class RelatedField(Field):
pass pass
# Standard case, return the object instance. # Standard case, return the object instance.
return super(RelatedField, self).get_attribute(instance) return super().get_attribute(instance)
def get_choices(self, cutoff=None): def get_choices(self, cutoff=None):
queryset = self.get_queryset() queryset = self.get_queryset()
@ -225,7 +218,7 @@ class RelatedField(Field):
) )
def display_value(self, instance): def display_value(self, instance):
return six.text_type(instance) return str(instance)
class StringRelatedField(RelatedField): class StringRelatedField(RelatedField):
@ -236,10 +229,10 @@ class StringRelatedField(RelatedField):
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs['read_only'] = True kwargs['read_only'] = True
super(StringRelatedField, self).__init__(**kwargs) super().__init__(**kwargs)
def to_representation(self, value): def to_representation(self, value):
return six.text_type(value) return str(value)
class PrimaryKeyRelatedField(RelatedField): class PrimaryKeyRelatedField(RelatedField):
@ -251,7 +244,7 @@ class PrimaryKeyRelatedField(RelatedField):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.pk_field = kwargs.pop('pk_field', None) self.pk_field = kwargs.pop('pk_field', None)
super(PrimaryKeyRelatedField, self).__init__(**kwargs) super().__init__(**kwargs)
def use_pk_only_optimization(self): def use_pk_only_optimization(self):
return True return True
@ -297,7 +290,7 @@ class HyperlinkedRelatedField(RelatedField):
# implicit `self` argument to be passed. # implicit `self` argument to be passed.
self.reverse = reverse self.reverse = reverse
super(HyperlinkedRelatedField, self).__init__(**kwargs) super().__init__(**kwargs)
def use_pk_only_optimization(self): def use_pk_only_optimization(self):
return self.lookup_field == 'pk' return self.lookup_field == 'pk'
@ -317,10 +310,10 @@ class HyperlinkedRelatedField(RelatedField):
return queryset.get(**lookup_kwargs) return queryset.get(**lookup_kwargs)
except ValueError: except ValueError:
exc = ObjectValueError(str(sys.exc_info()[1])) exc = ObjectValueError(str(sys.exc_info()[1]))
six.reraise(type(exc), exc, sys.exc_info()[2]) raise exc.with_traceback(sys.exc_info()[2])
except TypeError: except TypeError:
exc = ObjectTypeError(str(sys.exc_info()[1])) exc = ObjectTypeError(str(sys.exc_info()[1]))
six.reraise(type(exc), exc, sys.exc_info()[2]) raise exc.with_traceback(sys.exc_info()[2])
def get_url(self, obj, view_name, request, format): def get_url(self, obj, view_name, request, format):
""" """
@ -346,7 +339,7 @@ class HyperlinkedRelatedField(RelatedField):
if http_prefix: if http_prefix:
# If needed convert absolute URLs to relative path # If needed convert absolute URLs to relative path
data = urlparse.urlparse(data).path data = parse.urlparse(data).path
prefix = get_script_prefix() prefix = get_script_prefix()
if data.startswith(prefix): if data.startswith(prefix):
data = '/' + data[len(prefix):] data = '/' + data[len(prefix):]
@ -432,7 +425,7 @@ class HyperlinkedIdentityField(HyperlinkedRelatedField):
assert view_name is not None, 'The `view_name` argument is required.' assert view_name is not None, 'The `view_name` argument is required.'
kwargs['read_only'] = True kwargs['read_only'] = True
kwargs['source'] = '*' kwargs['source'] = '*'
super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs) super().__init__(view_name, **kwargs)
def use_pk_only_optimization(self): def use_pk_only_optimization(self):
# We have the complete object instance already. We don't need # We have the complete object instance already. We don't need
@ -453,7 +446,7 @@ class SlugRelatedField(RelatedField):
def __init__(self, slug_field=None, **kwargs): def __init__(self, slug_field=None, **kwargs):
assert slug_field is not None, 'The `slug_field` argument is required.' assert slug_field is not None, 'The `slug_field` argument is required.'
self.slug_field = slug_field self.slug_field = slug_field
super(SlugRelatedField, self).__init__(**kwargs) super().__init__(**kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
try: try:
@ -502,7 +495,7 @@ class ManyRelatedField(Field):
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
) )
assert child_relation is not None, '`child_relation` is a required argument.' assert child_relation is not None, '`child_relation` is a required argument.'
super(ManyRelatedField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.child_relation.bind(field_name='', parent=self) self.child_relation.bind(field_name='', parent=self)
def get_value(self, dictionary): def get_value(self, dictionary):
@ -518,7 +511,7 @@ class ManyRelatedField(Field):
return dictionary.get(self.field_name, empty) return dictionary.get(self.field_name, empty)
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, six.text_type) or not hasattr(data, '__iter__'): if isinstance(data, str) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__) self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0: if not self.allow_empty and len(data) == 0:
self.fail('empty') self.fail('empty')

View File

@ -6,10 +6,9 @@ on the response, such as JSON encoded data or HTML output.
REST framework also provides an HTML renderer that renders the browsable API. REST framework also provides an HTML renderer that renders the browsable API.
""" """
from __future__ import unicode_literals
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
from urllib import parse
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@ -19,9 +18,7 @@ from django.http.multipartparser import parse_header
from django.template import engines, loader from django.template import engines, loader
from django.test.client import encode_multipart from django.test.client import encode_multipart
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
from django.utils import six
from django.utils.html import mark_safe from django.utils.html import mark_safe
from django.utils.six.moves.urllib import parse as urlparse
from rest_framework import VERSION, exceptions, serializers, status from rest_framework import VERSION, exceptions, serializers, status
from rest_framework.compat import ( from rest_framework.compat import (
@ -40,7 +37,7 @@ def zero_as_none(value):
return None if value == 0 else value return None if value == 0 else value
class BaseRenderer(object): class BaseRenderer:
""" """
All renderers should extend this class, setting the `media_type` All renderers should extend this class, setting the `media_type`
and `format` attributes, and override the `.render()` method. and `format` attributes, and override the `.render()` method.
@ -91,7 +88,7 @@ class JSONRenderer(BaseRenderer):
Render `data` into JSON, returning a bytestring. Render `data` into JSON, returning a bytestring.
""" """
if data is None: if data is None:
return bytes() return b''
renderer_context = renderer_context or {} renderer_context = renderer_context or {}
indent = self.get_indent(accepted_media_type, renderer_context) indent = self.get_indent(accepted_media_type, renderer_context)
@ -107,18 +104,11 @@ class JSONRenderer(BaseRenderer):
allow_nan=not self.strict, separators=separators allow_nan=not self.strict, separators=separators
) )
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, # We always fully escape \u2028 and \u2029 to ensure we output JSON
# but if ensure_ascii=False, the return type is underspecified, # that is a strict javascript subset.
# and may (or may not) be unicode. # See: http://timelessrepo.com/json-isnt-a-javascript-subset
# On python 3.x json.dumps() returns unicode strings. ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
if isinstance(ret, six.text_type): return ret.encode()
# We always fully escape \u2028 and \u2029 to ensure we output JSON
# that is a strict javascript subset. If bytes were returned
# by json.dumps() then we don't have these characters in any case.
# See: http://timelessrepo.com/json-isnt-a-javascript-subset
ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
return bytes(ret.encode('utf-8'))
return ret
class TemplateHTMLRenderer(BaseRenderer): class TemplateHTMLRenderer(BaseRenderer):
@ -349,7 +339,7 @@ class HTMLFormRenderer(BaseRenderer):
# Get a clone of the field with text-only value representation. # Get a clone of the field with text-only value representation.
field = field.as_form_field() field = field.as_form_field()
if style.get('input_type') == 'datetime-local' and isinstance(field.value, six.text_type): if style.get('input_type') == 'datetime-local' and isinstance(field.value, str):
field.value = field.value.rstrip('Z') field.value = field.value.rstrip('Z')
if 'template' in style: if 'template' in style:
@ -577,7 +567,7 @@ class BrowsableAPIRenderer(BaseRenderer):
data.pop(name, None) data.pop(name, None)
content = renderer.render(data, accepted, context) content = renderer.render(data, accepted, context)
# Renders returns bytes, but CharField expects a str. # Renders returns bytes, but CharField expects a str.
content = content.decode('utf-8') content = content.decode()
else: else:
content = None content = None
@ -684,7 +674,7 @@ class BrowsableAPIRenderer(BaseRenderer):
csrf_header_name = csrf_header_name[5:] csrf_header_name = csrf_header_name[5:]
csrf_header_name = csrf_header_name.replace('_', '-') csrf_header_name = csrf_header_name.replace('_', '-')
context = { return {
'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
'code_style': pygments_css(self.code_style), 'code_style': pygments_css(self.code_style),
'view': view, 'view': view,
@ -720,7 +710,6 @@ class BrowsableAPIRenderer(BaseRenderer):
'csrf_cookie_name': csrf_cookie_name, 'csrf_cookie_name': csrf_cookie_name,
'csrf_header_name': csrf_header_name 'csrf_header_name': csrf_header_name
} }
return context
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
@ -791,7 +780,7 @@ class AdminRenderer(BrowsableAPIRenderer):
""" """
Render the HTML for the browsable API representation. Render the HTML for the browsable API representation.
""" """
context = super(AdminRenderer, self).get_context( context = super().get_context(
data, accepted_media_type, renderer_context data, accepted_media_type, renderer_context
) )
@ -995,14 +984,14 @@ class _BaseOpenAPIRenderer:
tag = None tag = None
for name, link in document.links.items(): for name, link in document.links.items():
path = urlparse.urlparse(link.url).path path = parse.urlparse(link.url).path
method = link.action.lower() method = link.action.lower()
paths.setdefault(path, {}) paths.setdefault(path, {})
paths[path][method] = self.get_operation(link, name, tag=tag) paths[path][method] = self.get_operation(link, name, tag=tag)
for tag, section in document.data.items(): for tag, section in document.data.items():
for name, link in section.links.items(): for name, link in section.links.items():
path = urlparse.urlparse(link.url).path path = parse.urlparse(link.url).path
method = link.action.lower() method = link.action.lower()
paths.setdefault(path, {}) paths.setdefault(path, {})
paths[path][method] = self.get_operation(link, name, tag=tag) paths[path][method] = self.get_operation(link, name, tag=tag)
@ -1024,28 +1013,49 @@ class _BaseOpenAPIRenderer:
} }
class OpenAPIRenderer(_BaseOpenAPIRenderer): class CoreAPIOpenAPIRenderer(_BaseOpenAPIRenderer):
media_type = 'application/vnd.oai.openapi' media_type = 'application/vnd.oai.openapi'
charset = None charset = None
format = 'openapi' format = 'openapi'
def __init__(self): def __init__(self):
assert coreapi, 'Using OpenAPIRenderer, but `coreapi` is not installed.' assert coreapi, 'Using CoreAPIOpenAPIRenderer, but `coreapi` is not installed.'
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.' assert yaml, 'Using CoreAPIOpenAPIRenderer, but `pyyaml` is not installed.'
def render(self, data, media_type=None, renderer_context=None): def render(self, data, media_type=None, renderer_context=None):
structure = self.get_structure(data) structure = self.get_structure(data)
return yaml.dump(structure, default_flow_style=False).encode('utf-8') return yaml.dump(structure, default_flow_style=False).encode()
class JSONOpenAPIRenderer(_BaseOpenAPIRenderer): class CoreAPIJSONOpenAPIRenderer(_BaseOpenAPIRenderer):
media_type = 'application/vnd.oai.openapi+json' media_type = 'application/vnd.oai.openapi+json'
charset = None charset = None
format = 'openapi-json' format = 'openapi-json'
def __init__(self): def __init__(self):
assert coreapi, 'Using JSONOpenAPIRenderer, but `coreapi` is not installed.' assert coreapi, 'Using CoreAPIJSONOpenAPIRenderer, but `coreapi` is not installed.'
def render(self, data, media_type=None, renderer_context=None): def render(self, data, media_type=None, renderer_context=None):
structure = self.get_structure(data) structure = self.get_structure(data)
return json.dumps(structure, indent=4).encode('utf-8') return json.dumps(structure, indent=4).encode('utf-8')
class OpenAPIRenderer(BaseRenderer):
media_type = 'application/vnd.oai.openapi'
charset = None
format = 'openapi'
def __init__(self):
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
def render(self, data, media_type=None, renderer_context=None):
return yaml.dump(data, default_flow_style=False).encode('utf-8')
class JSONOpenAPIRenderer(BaseRenderer):
media_type = 'application/vnd.oai.openapi+json'
charset = None
format = 'openapi-json'
def render(self, data, media_type=None, renderer_context=None):
return json.dumps(data, indent=2).encode('utf-8')

View File

@ -8,8 +8,6 @@ The wrapped request then offers a richer API, in particular :
- full support of PUT method, including support for file uploads - full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content - form overloading of HTTP method, content type and content
""" """
from __future__ import unicode_literals
import io import io
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
@ -18,7 +16,6 @@ from django.conf import settings
from django.http import HttpRequest, QueryDict from django.http import HttpRequest, QueryDict
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.http.request import RawPostDataException from django.http.request import RawPostDataException
from django.utils import six
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework import HTTP_HEADER_ENCODING, exceptions
@ -34,7 +31,7 @@ def is_form_media_type(media_type):
base_media_type == 'multipart/form-data') base_media_type == 'multipart/form-data')
class override_method(object): class override_method:
""" """
A context manager that temporarily overrides the method on a request, A context manager that temporarily overrides the method on a request,
additionally setting the `view.request` attribute. additionally setting the `view.request` attribute.
@ -78,10 +75,10 @@ def wrap_attributeerrors():
except AttributeError: except AttributeError:
info = sys.exc_info() info = sys.exc_info()
exc = WrappedAttributeError(str(info[1])) exc = WrappedAttributeError(str(info[1]))
six.reraise(type(exc), exc, info[2]) raise exc.with_traceback(info[2])
class Empty(object): class Empty:
""" """
Placeholder for unset attributes. Placeholder for unset attributes.
Cannot use `None`, as that may be a valid value. Cannot use `None`, as that may be a valid value.
@ -126,7 +123,7 @@ def clone_request(request, method):
return ret return ret
class ForcedAuthentication(object): class ForcedAuthentication:
""" """
This authentication class is used if the test client or request factory This authentication class is used if the test client or request factory
forcibly authenticated the request. forcibly authenticated the request.
@ -140,7 +137,7 @@ class ForcedAuthentication(object):
return (self.force_user, self.force_token) return (self.force_user, self.force_token)
class Request(object): class Request:
""" """
Wrapper allowing to enhance a standard `HttpRequest` instance. Wrapper allowing to enhance a standard `HttpRequest` instance.

View File

@ -4,11 +4,9 @@ it is initialized with unrendered data, instead of a pre-rendered string.
The appropriate renderer is called during Django's template response rendering. The appropriate renderer is called during Django's template response rendering.
""" """
from __future__ import unicode_literals from http.client import responses
from django.template.response import SimpleTemplateResponse from django.template.response import SimpleTemplateResponse
from django.utils import six
from django.utils.six.moves.http_client import responses
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
@ -29,7 +27,7 @@ class Response(SimpleTemplateResponse):
Setting 'renderer' and 'media_type' will typically be deferred, Setting 'renderer' and 'media_type' will typically be deferred,
For example being set automatically by the `APIView`. For example being set automatically by the `APIView`.
""" """
super(Response, self).__init__(None, status=status) super().__init__(None, status=status)
if isinstance(data, Serializer): if isinstance(data, Serializer):
msg = ( msg = (
@ -45,7 +43,7 @@ class Response(SimpleTemplateResponse):
self.content_type = content_type self.content_type = content_type
if headers: if headers:
for name, value in six.iteritems(headers): for name, value in headers.items():
self[name] = value self[name] = value
@property @property
@ -64,18 +62,18 @@ class Response(SimpleTemplateResponse):
content_type = self.content_type content_type = self.content_type
if content_type is None and charset is not None: if content_type is None and charset is not None:
content_type = "{0}; charset={1}".format(media_type, charset) content_type = "{}; charset={}".format(media_type, charset)
elif content_type is None: elif content_type is None:
content_type = media_type content_type = media_type
self['Content-Type'] = content_type self['Content-Type'] = content_type
ret = renderer.render(self.data, accepted_media_type, context) ret = renderer.render(self.data, accepted_media_type, context)
if isinstance(ret, six.text_type): if isinstance(ret, str):
assert charset, ( assert charset, (
'renderer returned unicode, and did not specify ' 'renderer returned unicode, and did not specify '
'a charset value.' 'a charset value.'
) )
return bytes(ret.encode(charset)) return ret.encode(charset)
if not ret: if not ret:
del self['Content-Type'] del self['Content-Type']
@ -94,7 +92,7 @@ class Response(SimpleTemplateResponse):
""" """
Remove attributes from the response that shouldn't be cached. Remove attributes from the response that shouldn't be cached.
""" """
state = super(Response, self).__getstate__() state = super().__getstate__()
for key in ( for key in (
'accepted_renderer', 'renderer_context', 'resolver_match', 'accepted_renderer', 'renderer_context', 'resolver_match',
'client', 'request', 'json', 'wsgi_request' 'client', 'request', 'json', 'wsgi_request'

View File

@ -1,11 +1,8 @@
""" """
Provide urlresolver functions that return fully qualified URLs or view names Provide urlresolver functions that return fully qualified URLs or view names
""" """
from __future__ import unicode_literals
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
from django.urls import reverse as django_reverse from django.urls import reverse as django_reverse
from django.utils import six
from django.utils.functional import lazy from django.utils.functional import lazy
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -66,4 +63,4 @@ def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extr
return url return url
reverse_lazy = lazy(reverse, six.text_type) reverse_lazy = lazy(reverse, str)

View File

@ -13,8 +13,6 @@ For example, you might have a `urls.py` that looks something like this:
urlpatterns = router.urls urlpatterns = router.urls
""" """
from __future__ import unicode_literals
import itertools import itertools
import warnings import warnings
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
@ -22,12 +20,9 @@ from collections import OrderedDict, namedtuple
from django.conf.urls import url from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
from django.utils import six
from django.utils.deprecation import RenameMethodsBase from django.utils.deprecation import RenameMethodsBase
from rest_framework import ( from rest_framework import RemovedInDRF311Warning, views
RemovedInDRF310Warning, RemovedInDRF311Warning, views
)
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.schemas import SchemaGenerator from rest_framework.schemas import SchemaGenerator
@ -39,28 +34,6 @@ Route = namedtuple('Route', ['url', 'mapping', 'name', 'detail', 'initkwargs'])
DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs']) DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs'])
class DynamicDetailRoute(object):
def __new__(cls, url, name, initkwargs):
warnings.warn(
"`DynamicDetailRoute` is deprecated and will be removed in 3.10 "
"in favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
"`DynamicRoute(url, name, True, initkwargs)` instead.",
RemovedInDRF310Warning, stacklevel=2
)
return DynamicRoute(url, name, True, initkwargs)
class DynamicListRoute(object):
def __new__(cls, url, name, initkwargs):
warnings.warn(
"`DynamicListRoute` is deprecated and will be removed in 3.10 in "
"favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
"`DynamicRoute(url, name, False, initkwargs)` instead.",
RemovedInDRF310Warning, stacklevel=2
)
return DynamicRoute(url, name, False, initkwargs)
def escape_curly_brackets(url_path): def escape_curly_brackets(url_path):
""" """
Double brackets in regex of url_path for escape string formatting Double brackets in regex of url_path for escape string formatting
@ -83,7 +56,7 @@ class RenameRouterMethods(RenameMethodsBase):
) )
class BaseRouter(six.with_metaclass(RenameRouterMethods)): class BaseRouter(metaclass=RenameRouterMethods):
def __init__(self): def __init__(self):
self.registry = [] self.registry = []
@ -173,7 +146,7 @@ class SimpleRouter(BaseRouter):
def __init__(self, trailing_slash=True): def __init__(self, trailing_slash=True):
self.trailing_slash = '/' if trailing_slash else '' self.trailing_slash = '/' if trailing_slash else ''
super(SimpleRouter, self).__init__() super().__init__()
def get_default_basename(self, viewset): def get_default_basename(self, viewset):
""" """
@ -365,7 +338,7 @@ class DefaultRouter(SimpleRouter):
self.root_renderers = kwargs.pop('root_renderers') self.root_renderers = kwargs.pop('root_renderers')
else: else:
self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES)
super(DefaultRouter, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_api_root_view(self, api_urls=None): def get_api_root_view(self, api_urls=None):
""" """
@ -383,7 +356,7 @@ class DefaultRouter(SimpleRouter):
Generate the list of URL patterns, including a default root view Generate the list of URL patterns, including a default root view
for the API, and appending `.json` style format suffixes. for the API, and appending `.json` style format suffixes.
""" """
urls = super(DefaultRouter, self).get_urls() urls = super().get_urls()
if self.include_root_view: if self.include_root_view:
view = self.get_api_root_view(api_urls=urls) view = self.get_api_root_view(api_urls=urls)

View File

@ -22,24 +22,32 @@ Other access should target the submodules directly
""" """
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from .generators import SchemaGenerator from . import coreapi, openapi
from .inspectors import AutoSchema, DefaultSchema, ManualSchema # noqa from .inspectors import DefaultSchema # noqa
from .coreapi import AutoSchema, ManualSchema, SchemaGenerator # noqa
def get_schema_view( def get_schema_view(
title=None, url=None, description=None, urlconf=None, renderer_classes=None, title=None, url=None, description=None, urlconf=None, renderer_classes=None,
public=False, patterns=None, generator_class=SchemaGenerator, public=False, patterns=None, generator_class=None,
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES, authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES): permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
""" """
Return a schema view. Return a schema view.
""" """
# Avoid import cycle on APIView if generator_class is None:
from .views import SchemaView if coreapi.is_enabled():
generator_class = coreapi.SchemaGenerator
else:
generator_class = openapi.SchemaGenerator
generator = generator_class( generator = generator_class(
title=title, url=url, description=description, title=title, url=url, description=description,
urlconf=urlconf, patterns=patterns, urlconf=urlconf, patterns=patterns,
) )
# Avoid import cycle on APIView
from .views import SchemaView
return SchemaView.as_view( return SchemaView.as_view(
renderer_classes=renderer_classes, renderer_classes=renderer_classes,
schema_generator=generator, schema_generator=generator,

View File

@ -0,0 +1,616 @@
import re
import warnings
from collections import Counter, OrderedDict
from urllib import parse
from django.db import models
from django.utils.encoding import force_text, smart_text
from rest_framework import exceptions, serializers
from rest_framework.compat import coreapi, coreschema, uritemplate
from rest_framework.settings import api_settings
from rest_framework.utils import formatting
from .generators import BaseSchemaGenerator
from .inspectors import ViewInspector
from .utils import get_pk_description, is_list_view
# Used in _get_description_section()
# TODO: ???: move up to base.
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
# Generator #
# TODO: Pull some of this into base.
def is_custom_action(action):
return action not in {
'retrieve', 'list', 'create', 'update', 'partial_update', 'destroy'
}
def distribute_links(obj):
for key, value in obj.items():
distribute_links(value)
for preferred_key, link in obj.links:
key = obj.get_available_key(preferred_key)
obj[key] = link
INSERT_INTO_COLLISION_FMT = """
Schema Naming Collision.
coreapi.Link for URL path {value_url} cannot be inserted into schema.
Position conflicts with coreapi.Link for URL path {target_url}.
Attempted to insert link with keys: {keys}.
Adjust URLs to avoid naming collision or override `SchemaGenerator.get_keys()`
to customise schema structure.
"""
class LinkNode(OrderedDict):
def __init__(self):
self.links = []
self.methods_counter = Counter()
super(LinkNode, self).__init__()
def get_available_key(self, preferred_key):
if preferred_key not in self:
return preferred_key
while True:
current_val = self.methods_counter[preferred_key]
self.methods_counter[preferred_key] += 1
key = '{}_{}'.format(preferred_key, current_val)
if key not in self:
return key
def insert_into(target, keys, value):
"""
Nested dictionary insertion.
>>> example = {}
>>> insert_into(example, ['a', 'b', 'c'], 123)
>>> example
LinkNode({'a': LinkNode({'b': LinkNode({'c': LinkNode(links=[123])}}})))
"""
for key in keys[:-1]:
if key not in target:
target[key] = LinkNode()
target = target[key]
try:
target.links.append((keys[-1], value))
except TypeError:
msg = INSERT_INTO_COLLISION_FMT.format(
value_url=value.url,
target_url=target.url,
keys=keys
)
raise ValueError(msg)
class SchemaGenerator(BaseSchemaGenerator):
"""
Original CoreAPI version.
"""
# Map HTTP methods onto actions.
default_mapping = {
'get': 'retrieve',
'post': 'create',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy',
}
# Map the method names we use for viewset actions onto external schema names.
# These give us names that are more suitable for the external representation.
# Set by 'SCHEMA_COERCE_METHOD_NAMES'.
coerce_method_names = None
def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None):
assert coreapi, '`coreapi` must be installed for schema support.'
assert coreschema, '`coreschema` must be installed for schema support.'
super(SchemaGenerator, self).__init__(title, url, description, patterns, urlconf)
self.coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
def get_links(self, request=None):
"""
Return a dictionary containing all the links that should be
included in the API schema.
"""
links = LinkNode()
paths, view_endpoints = self._get_paths_and_endpoints(request)
# Only generate the path prefix for paths that will be included
if not paths:
return None
prefix = self.determine_path_prefix(paths)
for path, method, view in view_endpoints:
if not self.has_view_permissions(path, method, view):
continue
link = view.schema.get_link(path, method, base_url=self.url)
subpath = path[len(prefix):]
keys = self.get_keys(subpath, method, view)
insert_into(links, keys, link)
return links
def get_schema(self, request=None, public=False):
"""
Generate a `coreapi.Document` representing the API schema.
"""
self._initialise_endpoints()
links = self.get_links(None if public else request)
if not links:
return None
url = self.url
if not url and request is not None:
url = request.build_absolute_uri()
distribute_links(links)
return coreapi.Document(
title=self.title, description=self.description,
url=url, content=links
)
# Method for generating the link layout....
def get_keys(self, subpath, method, view):
"""
Return a list of keys that should be used to layout a link within
the schema document.
/users/ ("users", "list"), ("users", "create")
/users/{pk}/ ("users", "read"), ("users", "update"), ("users", "delete")
/users/enabled/ ("users", "enabled") # custom viewset list action
/users/{pk}/star/ ("users", "star") # custom viewset detail action
/users/{pk}/groups/ ("users", "groups", "list"), ("users", "groups", "create")
/users/{pk}/groups/{pk}/ ("users", "groups", "read"), ("users", "groups", "update"), ("users", "groups", "delete")
"""
if hasattr(view, 'action'):
# Viewsets have explicitly named actions.
action = view.action
else:
# Views have no associated action, so we determine one from the method.
if is_list_view(subpath, method, view):
action = 'list'
else:
action = self.default_mapping[method.lower()]
named_path_components = [
component for component
in subpath.strip('/').split('/')
if '{' not in component
]
if is_custom_action(action):
# Custom action, eg "/users/{pk}/activate/", "/users/active/"
if len(view.action_map) > 1:
action = self.default_mapping[method.lower()]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
return named_path_components + [action]
else:
return named_path_components[:-1] + [action]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
# Default action, eg "/users/", "/users/{pk}/"
return named_path_components + [action]
# View Inspectors #
def field_to_schema(field):
title = force_text(field.label) if field.label else ''
description = force_text(field.help_text) if field.help_text else ''
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
child_schema = field_to_schema(field.child)
return coreschema.Array(
items=child_schema,
title=title,
description=description
)
elif isinstance(field, serializers.DictField):
return coreschema.Object(
title=title,
description=description
)
elif isinstance(field, serializers.Serializer):
return coreschema.Object(
properties=OrderedDict([
(key, field_to_schema(value))
for key, value
in field.fields.items()
]),
title=title,
description=description
)
elif isinstance(field, serializers.ManyRelatedField):
related_field_schema = field_to_schema(field.child_relation)
return coreschema.Array(
items=related_field_schema,
title=title,
description=description
)
elif isinstance(field, serializers.PrimaryKeyRelatedField):
schema_cls = coreschema.String
model = getattr(field.queryset, 'model', None)
if model is not None:
model_field = model._meta.pk
if isinstance(model_field, models.AutoField):
schema_cls = coreschema.Integer
return schema_cls(title=title, description=description)
elif isinstance(field, serializers.RelatedField):
return coreschema.String(title=title, description=description)
elif isinstance(field, serializers.MultipleChoiceField):
return coreschema.Array(
items=coreschema.Enum(enum=list(field.choices)),
title=title,
description=description
)
elif isinstance(field, serializers.ChoiceField):
return coreschema.Enum(
enum=list(field.choices),
title=title,
description=description
)
elif isinstance(field, serializers.BooleanField):
return coreschema.Boolean(title=title, description=description)
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
return coreschema.Number(title=title, description=description)
elif isinstance(field, serializers.IntegerField):
return coreschema.Integer(title=title, description=description)
elif isinstance(field, serializers.DateField):
return coreschema.String(
title=title,
description=description,
format='date'
)
elif isinstance(field, serializers.DateTimeField):
return coreschema.String(
title=title,
description=description,
format='date-time'
)
elif isinstance(field, serializers.JSONField):
return coreschema.Object(title=title, description=description)
if field.style.get('base_template') == 'textarea.html':
return coreschema.String(
title=title,
description=description,
format='textarea'
)
return coreschema.String(title=title, description=description)
class AutoSchema(ViewInspector):
"""
Default inspector for APIView
Responsible for per-view introspection and schema generation.
"""
def __init__(self, manual_fields=None):
"""
Parameters:
* `manual_fields`: list of `coreapi.Field` instances that
will be added to auto-generated fields, overwriting on `Field.name`
"""
super(AutoSchema, self).__init__()
if manual_fields is None:
manual_fields = []
self._manual_fields = manual_fields
def get_link(self, path, method, base_url):
"""
Generate `coreapi.Link` for self.view, path and method.
This is the main _public_ access point.
Parameters:
* path: Route path for view from URLConf.
* method: The HTTP request method.
* base_url: The project "mount point" as given to SchemaGenerator
"""
fields = self.get_path_fields(path, method)
fields += self.get_serializer_fields(path, method)
fields += self.get_pagination_fields(path, method)
fields += self.get_filter_fields(path, method)
manual_fields = self.get_manual_fields(path, method)
fields = self.update_fields(fields, manual_fields)
if fields and any([field.location in ('form', 'body') for field in fields]):
encoding = self.get_encoding(path, method)
else:
encoding = None
description = self.get_description(path, method)
if base_url and path.startswith('/'):
path = path[1:]
return coreapi.Link(
url=parse.urljoin(base_url, path),
action=method.lower(),
encoding=encoding,
fields=fields,
description=description
)
def get_description(self, path, method):
"""
Determine a link description.
This will be based on the method docstring if one exists,
or else the class docstring.
"""
view = self.view
method_name = getattr(view, 'action', method.lower())
method_docstring = getattr(view, method_name, None).__doc__
if method_docstring:
# An explicit docstring on the method or action.
return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring)))
else:
return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description())
def _get_description_section(self, view, header, description):
lines = [line for line in description.splitlines()]
current_section = ''
sections = {'': ''}
for line in lines:
if header_regex.match(line):
current_section, seperator, lead = line.partition(':')
sections[current_section] = lead.strip()
else:
sections[current_section] += '\n' + line
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
if header in sections:
return sections[header].strip()
if header in coerce_method_names:
if coerce_method_names[header] in sections:
return sections[coerce_method_names[header]].strip()
return sections[''].strip()
def get_path_fields(self, path, method):
"""
Return a list of `coreapi.Field` instances corresponding to any
templated path variables.
"""
view = self.view
model = getattr(getattr(view, 'queryset', None), 'model', None)
fields = []
for variable in uritemplate.variables(path):
title = ''
description = ''
schema_cls = coreschema.String
kwargs = {}
if model is not None:
# Attempt to infer a field description if possible.
try:
model_field = model._meta.get_field(variable)
except Exception:
model_field = None
if model_field is not None and model_field.verbose_name:
title = force_text(model_field.verbose_name)
if model_field is not None and model_field.help_text:
description = force_text(model_field.help_text)
elif model_field is not None and model_field.primary_key:
description = get_pk_description(model, model_field)
if hasattr(view, 'lookup_value_regex') and view.lookup_field == variable:
kwargs['pattern'] = view.lookup_value_regex
elif isinstance(model_field, models.AutoField):
schema_cls = coreschema.Integer
field = coreapi.Field(
name=variable,
location='path',
required=True,
schema=schema_cls(title=title, description=description, **kwargs)
)
fields.append(field)
return fields
def get_serializer_fields(self, path, method):
"""
Return a list of `coreapi.Field` instances corresponding to any
request body input, as determined by the serializer class.
"""
view = self.view
if method not in ('PUT', 'PATCH', 'POST'):
return []
if not hasattr(view, 'get_serializer'):
return []
try:
serializer = view.get_serializer()
except exceptions.APIException:
serializer = None
warnings.warn('{}.get_serializer() raised an exception during '
'schema generation. Serializer fields will not be '
'generated for {} {}.'
.format(view.__class__.__name__, method, path))
if isinstance(serializer, serializers.ListSerializer):
return [
coreapi.Field(
name='data',
location='body',
required=True,
schema=coreschema.Array()
)
]
if not isinstance(serializer, serializers.Serializer):
return []
fields = []
for field in serializer.fields.values():
if field.read_only or isinstance(field, serializers.HiddenField):
continue
required = field.required and method != 'PATCH'
field = coreapi.Field(
name=field.field_name,
location='form',
required=required,
schema=field_to_schema(field)
)
fields.append(field)
return fields
def get_pagination_fields(self, path, method):
view = self.view
if not is_list_view(path, method, view):
return []
pagination = getattr(view, 'pagination_class', None)
if not pagination:
return []
paginator = view.pagination_class()
return paginator.get_schema_fields(view)
def _allows_filters(self, path, method):
"""
Determine whether to include filter Fields in schema.
Default implementation looks for ModelViewSet or GenericAPIView
actions/methods that cause filtering on the default implementation.
Override to adjust behaviour for your view.
Note: Introduced in v3.7: Initially "private" (i.e. with leading underscore)
to allow changes based on user experience.
"""
if getattr(self.view, 'filter_backends', None) is None:
return False
if hasattr(self.view, 'action'):
return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
return method.lower() in ["get", "put", "patch", "delete"]
def get_filter_fields(self, path, method):
if not self._allows_filters(path, method):
return []
fields = []
for filter_backend in self.view.filter_backends:
fields += filter_backend().get_schema_fields(self.view)
return fields
def get_manual_fields(self, path, method):
return self._manual_fields
@staticmethod
def update_fields(fields, update_with):
"""
Update list of coreapi.Field instances, overwriting on `Field.name`.
Utility function to handle replacing coreapi.Field fields
from a list by name. Used to handle `manual_fields`.
Parameters:
* `fields`: list of `coreapi.Field` instances to update
* `update_with: list of `coreapi.Field` instances to add or replace.
"""
if not update_with:
return fields
by_name = OrderedDict((f.name, f) for f in fields)
for f in update_with:
by_name[f.name] = f
fields = list(by_name.values())
return fields
def get_encoding(self, path, method):
"""
Return the 'encoding' parameter to use for a given endpoint.
"""
view = self.view
# Core API supports the following request encodings over HTTP...
supported_media_types = {
'application/json',
'application/x-www-form-urlencoded',
'multipart/form-data',
}
parser_classes = getattr(view, 'parser_classes', [])
for parser_class in parser_classes:
media_type = getattr(parser_class, 'media_type', None)
if media_type in supported_media_types:
return media_type
# Raw binary uploads are supported with "application/octet-stream"
if media_type == '*/*':
return 'application/octet-stream'
return None
class ManualSchema(ViewInspector):
"""
Allows providing a list of coreapi.Fields,
plus an optional description.
"""
def __init__(self, fields, description='', encoding=None):
"""
Parameters:
* `fields`: list of `coreapi.Field` instances.
* `description`: String description for view. Optional.
"""
super(ManualSchema, self).__init__()
assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances"
self._fields = fields
self._description = description
self._encoding = encoding
def get_link(self, path, method, base_url):
if base_url and path.startswith('/'):
path = path[1:]
return coreapi.Link(
url=parse.urljoin(base_url, path),
action=method.lower(),
encoding=self._encoding,
fields=self._fields,
description=self._description
)
def is_enabled():
"""Is CoreAPI Mode enabled?"""
return issubclass(api_settings.DEFAULT_SCHEMA_CLASS, AutoSchema)

View File

@ -4,25 +4,19 @@ generators.py # Top-down schema generation
See schemas.__init__.py for package overview. See schemas.__init__.py for package overview.
""" """
import re import re
from collections import Counter, OrderedDict
from importlib import import_module from importlib import import_module
from django.conf import settings from django.conf import settings
from django.contrib.admindocs.views import simplify_regex from django.contrib.admindocs.views import simplify_regex
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.utils import six
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.compat import ( from rest_framework.compat import URLPattern, URLResolver, get_original_route
URLPattern, URLResolver, coreapi, coreschema, get_original_route
)
from rest_framework.request import clone_request from rest_framework.request import clone_request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils.model_meta import _get_pk from rest_framework.utils.model_meta import _get_pk
from .utils import is_list_view
def common_path(paths): def common_path(paths):
split_paths = [path.strip('/').split('/') for path in paths] split_paths = [path.strip('/').split('/') for path in paths]
@ -51,78 +45,6 @@ def is_api_view(callback):
return (cls is not None) and issubclass(cls, APIView) return (cls is not None) and issubclass(cls, APIView)
INSERT_INTO_COLLISION_FMT = """
Schema Naming Collision.
coreapi.Link for URL path {value_url} cannot be inserted into schema.
Position conflicts with coreapi.Link for URL path {target_url}.
Attempted to insert link with keys: {keys}.
Adjust URLs to avoid naming collision or override `SchemaGenerator.get_keys()`
to customise schema structure.
"""
class LinkNode(OrderedDict):
def __init__(self):
self.links = []
self.methods_counter = Counter()
super(LinkNode, self).__init__()
def get_available_key(self, preferred_key):
if preferred_key not in self:
return preferred_key
while True:
current_val = self.methods_counter[preferred_key]
self.methods_counter[preferred_key] += 1
key = '{}_{}'.format(preferred_key, current_val)
if key not in self:
return key
def insert_into(target, keys, value):
"""
Nested dictionary insertion.
>>> example = {}
>>> insert_into(example, ['a', 'b', 'c'], 123)
>>> example
LinkNode({'a': LinkNode({'b': LinkNode({'c': LinkNode(links=[123])}}})))
"""
for key in keys[:-1]:
if key not in target:
target[key] = LinkNode()
target = target[key]
try:
target.links.append((keys[-1], value))
except TypeError:
msg = INSERT_INTO_COLLISION_FMT.format(
value_url=value.url,
target_url=target.url,
keys=keys
)
raise ValueError(msg)
def distribute_links(obj):
for key, value in obj.items():
distribute_links(value)
for preferred_key, link in obj.links:
key = obj.get_available_key(preferred_key)
obj[key] = link
def is_custom_action(action):
return action not in {
'retrieve', 'list', 'create', 'update', 'partial_update', 'destroy'
}
def endpoint_ordering(endpoint): def endpoint_ordering(endpoint):
path, method, callback = endpoint path, method, callback = endpoint
method_priority = { method_priority = {
@ -140,7 +62,7 @@ _PATH_PARAMETER_COMPONENT_RE = re.compile(
) )
class EndpointEnumerator(object): class EndpointEnumerator:
""" """
A class to determine the available API endpoints that a project exposes. A class to determine the available API endpoints that a project exposes.
""" """
@ -151,7 +73,7 @@ class EndpointEnumerator(object):
urlconf = settings.ROOT_URLCONF urlconf = settings.ROOT_URLCONF
# Load the given URLconf module # Load the given URLconf module
if isinstance(urlconf, six.string_types): if isinstance(urlconf, str):
urls = import_module(urlconf) urls = import_module(urlconf)
else: else:
urls = urlconf urls = urlconf
@ -185,19 +107,20 @@ class EndpointEnumerator(object):
) )
api_endpoints.extend(nested_endpoints) api_endpoints.extend(nested_endpoints)
api_endpoints = sorted(api_endpoints, key=endpoint_ordering) return sorted(api_endpoints, key=endpoint_ordering)
return api_endpoints
def get_path_from_regex(self, path_regex): def get_path_from_regex(self, path_regex):
""" """
Given a URL conf regex, return a URI template string. Given a URL conf regex, return a URI template string.
""" """
# ???: Would it be feasible to adjust this such that we generate the
# path, plus the kwargs, plus the type from the convertor, such that we
# could feed that straight into the parameter schema object?
path = simplify_regex(path_regex) path = simplify_regex(path_regex)
# Strip Django 2.0 convertors as they are incompatible with uritemplate format # Strip Django 2.0 convertors as they are incompatible with uritemplate format
path = re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path) return re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
return path
def should_include_endpoint(self, path, callback): def should_include_endpoint(self, path, callback):
""" """
@ -232,35 +155,18 @@ class EndpointEnumerator(object):
return [method for method in methods if method not in ('OPTIONS', 'HEAD')] return [method for method in methods if method not in ('OPTIONS', 'HEAD')]
class SchemaGenerator(object): class BaseSchemaGenerator(object):
# Map HTTP methods onto actions.
default_mapping = {
'get': 'retrieve',
'post': 'create',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy',
}
endpoint_inspector_cls = EndpointEnumerator endpoint_inspector_cls = EndpointEnumerator
# Map the method names we use for viewset actions onto external schema names.
# These give us names that are more suitable for the external representation.
# Set by 'SCHEMA_COERCE_METHOD_NAMES'.
coerce_method_names = None
# 'pk' isn't great as an externally exposed name for an identifier, # 'pk' isn't great as an externally exposed name for an identifier,
# so by default we prefer to use the actual model field name for schemas. # so by default we prefer to use the actual model field name for schemas.
# Set by 'SCHEMA_COERCE_PATH_PK'. # Set by 'SCHEMA_COERCE_PATH_PK'.
coerce_path_pk = None coerce_path_pk = None
def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None): def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None):
assert coreapi, '`coreapi` must be installed for schema support.'
assert coreschema, '`coreschema` must be installed for schema support.'
if url and not url.endswith('/'): if url and not url.endswith('/'):
url += '/' url += '/'
self.coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
self.coerce_path_pk = api_settings.SCHEMA_COERCE_PATH_PK self.coerce_path_pk = api_settings.SCHEMA_COERCE_PATH_PK
self.patterns = patterns self.patterns = patterns
@ -270,36 +176,15 @@ class SchemaGenerator(object):
self.url = url self.url = url
self.endpoints = None self.endpoints = None
def get_schema(self, request=None, public=False): def _initialise_endpoints(self):
"""
Generate a `coreapi.Document` representing the API schema.
"""
if self.endpoints is None: if self.endpoints is None:
inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf) inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf)
self.endpoints = inspector.get_api_endpoints() self.endpoints = inspector.get_api_endpoints()
links = self.get_links(None if public else request) def _get_paths_and_endpoints(self, request):
if not links:
return None
url = self.url
if not url and request is not None:
url = request.build_absolute_uri()
distribute_links(links)
return coreapi.Document(
title=self.title, description=self.description,
url=url, content=links
)
def get_links(self, request=None):
""" """
Return a dictionary containing all the links that should be Generate (path, method, view) given (path, method, callback) for paths.
included in the API schema.
""" """
links = LinkNode()
# Generate (path, method, view) given (path, method, callback).
paths = [] paths = []
view_endpoints = [] view_endpoints = []
for path, method, callback in self.endpoints: for path, method, callback in self.endpoints:
@ -308,22 +193,48 @@ class SchemaGenerator(object):
paths.append(path) paths.append(path)
view_endpoints.append((path, method, view)) view_endpoints.append((path, method, view))
# Only generate the path prefix for paths that will be included return paths, view_endpoints
if not paths:
return None
prefix = self.determine_path_prefix(paths)
for path, method, view in view_endpoints: def create_view(self, callback, method, request=None):
if not self.has_view_permissions(path, method, view): """
continue Given a callback, return an actual view instance.
link = view.schema.get_link(path, method, base_url=self.url) """
subpath = path[len(prefix):] view = callback.cls(**getattr(callback, 'initkwargs', {}))
keys = self.get_keys(subpath, method, view) view.args = ()
insert_into(links, keys, link) view.kwargs = {}
view.format_kwarg = None
view.request = None
view.action_map = getattr(callback, 'actions', None)
return links actions = getattr(callback, 'actions', None)
if actions is not None:
if method == 'OPTIONS':
view.action = 'metadata'
else:
view.action = actions.get(method.lower())
# Methods used when we generate a view instance from the raw callback... if request is not None:
view.request = clone_request(request, method)
return view
def coerce_path(self, path, method, view):
"""
Coerce {pk} path arguments into the name of the model field,
where possible. This is cleaner for an external representation.
(Ie. "this is an identifier", not "this is a database primary key")
"""
if not self.coerce_path_pk or '{pk}' not in path:
return path
model = getattr(getattr(view, 'queryset', None), 'model', None)
if model:
field_name = get_pk_name(model)
else:
field_name = 'id'
return path.replace('{pk}', '{%s}' % field_name)
def get_schema(self, request=None, public=False):
raise NotImplementedError(".get_schema() must be implemented in subclasses.")
def determine_path_prefix(self, paths): def determine_path_prefix(self, paths):
""" """
@ -356,29 +267,6 @@ class SchemaGenerator(object):
prefixes.append('/' + prefix + '/') prefixes.append('/' + prefix + '/')
return common_path(prefixes) return common_path(prefixes)
def create_view(self, callback, method, request=None):
"""
Given a callback, return an actual view instance.
"""
view = callback.cls(**getattr(callback, 'initkwargs', {}))
view.args = ()
view.kwargs = {}
view.format_kwarg = None
view.request = None
view.action_map = getattr(callback, 'actions', None)
actions = getattr(callback, 'actions', None)
if actions is not None:
if method == 'OPTIONS':
view.action = 'metadata'
else:
view.action = actions.get(method.lower())
if request is not None:
view.request = clone_request(request, method)
return view
def has_view_permissions(self, path, method, view): def has_view_permissions(self, path, method, view):
""" """
Return `True` if the incoming request has the correct view permissions. Return `True` if the incoming request has the correct view permissions.
@ -391,64 +279,3 @@ class SchemaGenerator(object):
except (exceptions.APIException, Http404, PermissionDenied): except (exceptions.APIException, Http404, PermissionDenied):
return False return False
return True return True
def coerce_path(self, path, method, view):
"""
Coerce {pk} path arguments into the name of the model field,
where possible. This is cleaner for an external representation.
(Ie. "this is an identifier", not "this is a database primary key")
"""
if not self.coerce_path_pk or '{pk}' not in path:
return path
model = getattr(getattr(view, 'queryset', None), 'model', None)
if model:
field_name = get_pk_name(model)
else:
field_name = 'id'
return path.replace('{pk}', '{%s}' % field_name)
# Method for generating the link layout....
def get_keys(self, subpath, method, view):
"""
Return a list of keys that should be used to layout a link within
the schema document.
/users/ ("users", "list"), ("users", "create")
/users/{pk}/ ("users", "read"), ("users", "update"), ("users", "delete")
/users/enabled/ ("users", "enabled") # custom viewset list action
/users/{pk}/star/ ("users", "star") # custom viewset detail action
/users/{pk}/groups/ ("users", "groups", "list"), ("users", "groups", "create")
/users/{pk}/groups/{pk}/ ("users", "groups", "read"), ("users", "groups", "update"), ("users", "groups", "delete")
"""
if hasattr(view, 'action'):
# Viewsets have explicitly named actions.
action = view.action
else:
# Views have no associated action, so we determine one from the method.
if is_list_view(subpath, method, view):
action = 'list'
else:
action = self.default_mapping[method.lower()]
named_path_components = [
component for component
in subpath.strip('/').split('/')
if '{' not in component
]
if is_custom_action(action):
# Custom action, eg "/users/{pk}/activate/", "/users/active/"
if len(view.action_map) > 1:
action = self.default_mapping[method.lower()]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
return named_path_components + [action]
else:
return named_path_components[:-1] + [action]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
# Default action, eg "/users/", "/users/{pk}/"
return named_path_components + [action]

View File

@ -1,129 +1,14 @@
# -*- coding: utf-8 -*-
""" """
inspectors.py # Per-endpoint view introspection inspectors.py # Per-endpoint view introspection
See schemas.__init__.py for package overview. See schemas.__init__.py for package overview.
""" """
import re
import warnings
from collections import OrderedDict
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
from django.db import models
from django.utils.encoding import force_text, smart_text
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions, serializers
from rest_framework.compat import coreapi, coreschema, uritemplate
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils import formatting
from .utils import is_list_view
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
def field_to_schema(field): class ViewInspector:
title = force_text(field.label) if field.label else ''
description = force_text(field.help_text) if field.help_text else ''
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
child_schema = field_to_schema(field.child)
return coreschema.Array(
items=child_schema,
title=title,
description=description
)
elif isinstance(field, serializers.DictField):
return coreschema.Object(
title=title,
description=description
)
elif isinstance(field, serializers.Serializer):
return coreschema.Object(
properties=OrderedDict([
(key, field_to_schema(value))
for key, value
in field.fields.items()
]),
title=title,
description=description
)
elif isinstance(field, serializers.ManyRelatedField):
return coreschema.Array(
items=coreschema.String(),
title=title,
description=description
)
elif isinstance(field, serializers.PrimaryKeyRelatedField):
schema_cls = coreschema.String
model = getattr(field.queryset, 'model', None)
if model is not None:
model_field = model._meta.pk
if isinstance(model_field, models.AutoField):
schema_cls = coreschema.Integer
return schema_cls(title=title, description=description)
elif isinstance(field, serializers.RelatedField):
return coreschema.String(title=title, description=description)
elif isinstance(field, serializers.MultipleChoiceField):
return coreschema.Array(
items=coreschema.Enum(enum=list(field.choices)),
title=title,
description=description
)
elif isinstance(field, serializers.ChoiceField):
return coreschema.Enum(
enum=list(field.choices),
title=title,
description=description
)
elif isinstance(field, serializers.BooleanField):
return coreschema.Boolean(title=title, description=description)
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
return coreschema.Number(title=title, description=description)
elif isinstance(field, serializers.IntegerField):
return coreschema.Integer(title=title, description=description)
elif isinstance(field, serializers.DateField):
return coreschema.String(
title=title,
description=description,
format='date'
)
elif isinstance(field, serializers.DateTimeField):
return coreschema.String(
title=title,
description=description,
format='date-time'
)
elif isinstance(field, serializers.JSONField):
return coreschema.Object(title=title, description=description)
if field.style.get('base_template') == 'textarea.html':
return coreschema.String(
title=title,
description=description,
format='textarea'
)
return coreschema.String(title=title, description=description)
def get_pk_description(model, model_field):
if isinstance(model_field, models.AutoField):
value_type = _('unique integer value')
elif isinstance(model_field, models.UUIDField):
value_type = _('UUID string')
else:
value_type = _('unique value')
return _('A {value_type} identifying this {name}.').format(
value_type=value_type,
name=model._meta.verbose_name,
)
class ViewInspector(object):
""" """
Descriptor class on APIView. Descriptor class on APIView.
@ -177,326 +62,11 @@ class ViewInspector(object):
def view(self): def view(self):
self._view = None self._view = None
def get_link(self, path, method, base_url):
"""
Generate `coreapi.Link` for self.view, path and method.
This is the main _public_ access point.
Parameters:
* path: Route path for view from URLConf.
* method: The HTTP request method.
* base_url: The project "mount point" as given to SchemaGenerator
"""
raise NotImplementedError(".get_link() must be overridden.")
class AutoSchema(ViewInspector):
"""
Default inspector for APIView
Responsible for per-view introspection and schema generation.
"""
def __init__(self, manual_fields=None):
"""
Parameters:
* `manual_fields`: list of `coreapi.Field` instances that
will be added to auto-generated fields, overwriting on `Field.name`
"""
super(AutoSchema, self).__init__()
if manual_fields is None:
manual_fields = []
self._manual_fields = manual_fields
def get_link(self, path, method, base_url):
fields = self.get_path_fields(path, method)
fields += self.get_serializer_fields(path, method)
fields += self.get_pagination_fields(path, method)
fields += self.get_filter_fields(path, method)
manual_fields = self.get_manual_fields(path, method)
fields = self.update_fields(fields, manual_fields)
if fields and any([field.location in ('form', 'body') for field in fields]):
encoding = self.get_encoding(path, method)
else:
encoding = None
description = self.get_description(path, method)
if base_url and path.startswith('/'):
path = path[1:]
return coreapi.Link(
url=urlparse.urljoin(base_url, path),
action=method.lower(),
encoding=encoding,
fields=fields,
description=description
)
def get_description(self, path, method):
"""
Determine a link description.
This will be based on the method docstring if one exists,
or else the class docstring.
"""
view = self.view
method_name = getattr(view, 'action', method.lower())
method_docstring = getattr(view, method_name, None).__doc__
if method_docstring:
# An explicit docstring on the method or action.
return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring)))
else:
return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description())
def _get_description_section(self, view, header, description):
lines = [line for line in description.splitlines()]
current_section = ''
sections = {'': ''}
for line in lines:
if header_regex.match(line):
current_section, seperator, lead = line.partition(':')
sections[current_section] = lead.strip()
else:
sections[current_section] += '\n' + line
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
if header in sections:
return sections[header].strip()
if header in coerce_method_names:
if coerce_method_names[header] in sections:
return sections[coerce_method_names[header]].strip()
return sections[''].strip()
def get_path_fields(self, path, method):
"""
Return a list of `coreapi.Field` instances corresponding to any
templated path variables.
"""
view = self.view
model = getattr(getattr(view, 'queryset', None), 'model', None)
fields = []
for variable in uritemplate.variables(path):
title = ''
description = ''
schema_cls = coreschema.String
kwargs = {}
if model is not None:
# Attempt to infer a field description if possible.
try:
model_field = model._meta.get_field(variable)
except Exception:
model_field = None
if model_field is not None and model_field.verbose_name:
title = force_text(model_field.verbose_name)
if model_field is not None and model_field.help_text:
description = force_text(model_field.help_text)
elif model_field is not None and model_field.primary_key:
description = get_pk_description(model, model_field)
if hasattr(view, 'lookup_value_regex') and view.lookup_field == variable:
kwargs['pattern'] = view.lookup_value_regex
elif isinstance(model_field, models.AutoField):
schema_cls = coreschema.Integer
field = coreapi.Field(
name=variable,
location='path',
required=True,
schema=schema_cls(title=title, description=description, **kwargs)
)
fields.append(field)
return fields
def get_serializer_fields(self, path, method):
"""
Return a list of `coreapi.Field` instances corresponding to any
request body input, as determined by the serializer class.
"""
view = self.view
if method not in ('PUT', 'PATCH', 'POST'):
return []
if not hasattr(view, 'get_serializer'):
return []
try:
serializer = view.get_serializer()
except exceptions.APIException:
serializer = None
warnings.warn('{}.get_serializer() raised an exception during '
'schema generation. Serializer fields will not be '
'generated for {} {}.'
.format(view.__class__.__name__, method, path))
if isinstance(serializer, serializers.ListSerializer):
return [
coreapi.Field(
name='data',
location='body',
required=True,
schema=coreschema.Array()
)
]
if not isinstance(serializer, serializers.Serializer):
return []
fields = []
for field in serializer.fields.values():
if field.read_only or isinstance(field, serializers.HiddenField):
continue
required = field.required and method != 'PATCH'
field = coreapi.Field(
name=field.field_name,
location='form',
required=required,
schema=field_to_schema(field)
)
fields.append(field)
return fields
def get_pagination_fields(self, path, method):
view = self.view
if not is_list_view(path, method, view):
return []
pagination = getattr(view, 'pagination_class', None)
if not pagination:
return []
paginator = view.pagination_class()
return paginator.get_schema_fields(view)
def _allows_filters(self, path, method):
"""
Determine whether to include filter Fields in schema.
Default implementation looks for ModelViewSet or GenericAPIView
actions/methods that cause filtering on the default implementation.
Override to adjust behaviour for your view.
Note: Introduced in v3.7: Initially "private" (i.e. with leading underscore)
to allow changes based on user experience.
"""
if getattr(self.view, 'filter_backends', None) is None:
return False
if hasattr(self.view, 'action'):
return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
return method.lower() in ["get", "put", "patch", "delete"]
def get_filter_fields(self, path, method):
if not self._allows_filters(path, method):
return []
fields = []
for filter_backend in self.view.filter_backends:
fields += filter_backend().get_schema_fields(self.view)
return fields
def get_manual_fields(self, path, method):
return self._manual_fields
@staticmethod
def update_fields(fields, update_with):
"""
Update list of coreapi.Field instances, overwriting on `Field.name`.
Utility function to handle replacing coreapi.Field fields
from a list by name. Used to handle `manual_fields`.
Parameters:
* `fields`: list of `coreapi.Field` instances to update
* `update_with: list of `coreapi.Field` instances to add or replace.
"""
if not update_with:
return fields
by_name = OrderedDict((f.name, f) for f in fields)
for f in update_with:
by_name[f.name] = f
fields = list(by_name.values())
return fields
def get_encoding(self, path, method):
"""
Return the 'encoding' parameter to use for a given endpoint.
"""
view = self.view
# Core API supports the following request encodings over HTTP...
supported_media_types = {
'application/json',
'application/x-www-form-urlencoded',
'multipart/form-data',
}
parser_classes = getattr(view, 'parser_classes', [])
for parser_class in parser_classes:
media_type = getattr(parser_class, 'media_type', None)
if media_type in supported_media_types:
return media_type
# Raw binary uploads are supported with "application/octet-stream"
if media_type == '*/*':
return 'application/octet-stream'
return None
class ManualSchema(ViewInspector):
"""
Allows providing a list of coreapi.Fields,
plus an optional description.
"""
def __init__(self, fields, description='', encoding=None):
"""
Parameters:
* `fields`: list of `coreapi.Field` instances.
* `description`: String description for view. Optional.
"""
super(ManualSchema, self).__init__()
assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances"
self._fields = fields
self._description = description
self._encoding = encoding
def get_link(self, path, method, base_url):
if base_url and path.startswith('/'):
path = path[1:]
return coreapi.Link(
url=urlparse.urljoin(base_url, path),
action=method.lower(),
encoding=self._encoding,
fields=self._fields,
description=self._description
)
class DefaultSchema(ViewInspector): class DefaultSchema(ViewInspector):
"""Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting""" """Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting"""
def __get__(self, instance, owner): def __get__(self, instance, owner):
result = super(DefaultSchema, self).__get__(instance, owner) result = super().__get__(instance, owner)
if not isinstance(result, DefaultSchema): if not isinstance(result, DefaultSchema):
return result return result

View File

@ -0,0 +1,377 @@
import warnings
from django.db import models
from django.utils.encoding import force_text
from rest_framework import exceptions, serializers
from rest_framework.compat import uritemplate
from .generators import BaseSchemaGenerator
from .inspectors import ViewInspector
from .utils import get_pk_description, is_list_view
# Generator
class SchemaGenerator(BaseSchemaGenerator):
def get_info(self):
info = {
'title': self.title,
'version': 'TODO',
}
if self.description is not None:
info['description'] = self.description
return info
def get_paths(self, request=None):
result = {}
paths, view_endpoints = self._get_paths_and_endpoints(request)
# Only generate the path prefix for paths that will be included
if not paths:
return None
prefix = self.determine_path_prefix(paths)
for path, method, view in view_endpoints:
if not self.has_view_permissions(path, method, view):
continue
operation = view.schema.get_operation(path, method)
subpath = '/' + path[len(prefix):]
result.setdefault(subpath, {})
result[subpath][method.lower()] = operation
return result
def get_schema(self, request=None, public=False):
"""
Generate a OpenAPI schema.
"""
self._initialise_endpoints()
paths = self.get_paths(None if public else request)
if not paths:
return None
schema = {
'openapi': '3.0.2',
'info': self.get_info(),
'paths': paths,
}
return schema
# View Inspectors
class AutoSchema(ViewInspector):
content_types = ['application/json']
method_mapping = {
'get': 'Retrieve',
'post': 'Create',
'put': 'Update',
'patch': 'PartialUpdate',
'delete': 'Destroy',
}
def get_operation(self, path, method):
operation = {}
operation['operationId'] = self._get_operation_id(path, method)
parameters = []
parameters += self._get_path_parameters(path, method)
parameters += self._get_pagination_parameters(path, method)
parameters += self._get_filter_parameters(path, method)
operation['parameters'] = parameters
request_body = self._get_request_body(path, method)
if request_body:
operation['requestBody'] = request_body
operation['responses'] = self._get_responses(path, method)
return operation
def _get_operation_id(self, path, method):
"""
Compute an operation ID from the model, serializer or view name.
"""
method_name = getattr(self.view, 'action', method.lower())
if is_list_view(path, method, self.view):
action = 'List'
elif method_name not in self.method_mapping:
action = method_name
else:
action = self.method_mapping[method.lower()]
# Try to deduce the ID from the view's model
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
if model is not None:
name = model.__name__
# Try with the serializer class name
elif hasattr(self.view, 'get_serializer_class'):
name = self.view.get_serializer_class().__name__
if name.endswith('Serializer'):
name = name[:-10]
# Fallback to the view name
else:
name = self.view.__class__.__name__
if name.endswith('APIView'):
name = name[:-7]
elif name.endswith('View'):
name = name[:-4]
if name.endswith(action): # ListView, UpdateAPIView, ThingDelete ...
name = name[:-len(action)]
if action == 'List' and not name.endswith('s'): # ListThings instead of ListThing
name += 's'
return action + name
def _get_path_parameters(self, path, method):
"""
Return a list of parameters from templated path variables.
"""
assert uritemplate, '`uritemplate` must be installed for OpenAPI schema support.'
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
parameters = []
for variable in uritemplate.variables(path):
description = ''
if model is not None: # TODO: test this.
# Attempt to infer a field description if possible.
try:
model_field = model._meta.get_field(variable)
except Exception:
model_field = None
if model_field is not None and model_field.help_text:
description = force_text(model_field.help_text)
elif model_field is not None and model_field.primary_key:
description = get_pk_description(model, model_field)
parameter = {
"name": variable,
"in": "path",
"required": True,
"description": description,
'schema': {
'type': 'string', # TODO: integer, pattern, ...
},
}
parameters.append(parameter)
return parameters
def _get_filter_parameters(self, path, method):
if not self._allows_filters(path, method):
return []
parameters = []
for filter_backend in self.view.filter_backends:
parameters += filter_backend().get_schema_operation_parameters(self.view)
return parameters
def _allows_filters(self, path, method):
"""
Determine whether to include filter Fields in schema.
Default implementation looks for ModelViewSet or GenericAPIView
actions/methods that cause filtering on the default implementation.
"""
if getattr(self.view, 'filter_backends', None) is None:
return False
if hasattr(self.view, 'action'):
return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
return method.lower() in ["get", "put", "patch", "delete"]
def _get_pagination_parameters(self, path, method):
view = self.view
if not is_list_view(path, method, view):
return []
pagination = getattr(view, 'pagination_class', None)
if not pagination:
return []
paginator = view.pagination_class()
return paginator.get_schema_operation_parameters(view)
def _map_field(self, field):
# Nested Serializers, `many` or not.
if isinstance(field, serializers.ListSerializer):
return {
'type': 'array',
'items': self._map_serializer(field.child)
}
if isinstance(field, serializers.Serializer):
data = self._map_serializer(field)
data['type'] = 'object'
return data
# Related fields.
if isinstance(field, serializers.ManyRelatedField):
return {
'type': 'array',
'items': self._map_field(field.child_relation)
}
if isinstance(field, serializers.PrimaryKeyRelatedField):
model = getattr(field.queryset, 'model', None)
if model is not None:
model_field = model._meta.pk
if isinstance(model_field, models.AutoField):
return {'type': 'integer'}
# ChoiceFields (single and multiple).
# Q:
# - Is 'type' required?
# - can we determine the TYPE of a choicefield?
if isinstance(field, serializers.MultipleChoiceField):
return {
'type': 'array',
'items': {
'enum': list(field.choices)
},
}
if isinstance(field, serializers.ChoiceField):
return {
'enum': list(field.choices),
}
# ListField.
if isinstance(field, serializers.ListField):
return {
'type': 'array',
}
# DateField and DateTimeField type is string
if isinstance(field, serializers.DateField):
return {
'type': 'string',
'format': 'date',
}
if isinstance(field, serializers.DateTimeField):
return {
'type': 'string',
'format': 'date-time',
}
# Simplest cases, default to 'string' type:
FIELD_CLASS_SCHEMA_TYPE = {
serializers.BooleanField: 'boolean',
serializers.DecimalField: 'number',
serializers.FloatField: 'number',
serializers.IntegerField: 'integer',
serializers.JSONField: 'object',
serializers.DictField: 'object',
}
return {'type': FIELD_CLASS_SCHEMA_TYPE.get(field.__class__, 'string')}
def _map_serializer(self, serializer):
# Assuming we have a valid serializer instance.
# TODO:
# - field is Nested or List serializer.
# - Handle read_only/write_only for request/response differences.
# - could do this with readOnly/writeOnly and then filter dict.
required = []
properties = {}
for field in serializer.fields.values():
if isinstance(field, serializers.HiddenField):
continue
if field.required:
required.append(field.field_name)
schema = self._map_field(field)
if field.read_only:
schema['readOnly'] = True
if field.write_only:
schema['writeOnly'] = True
if field.allow_null:
schema['nullable'] = True
properties[field.field_name] = schema
return {
'required': required,
'properties': properties,
}
def _get_request_body(self, path, method):
view = self.view
if method not in ('PUT', 'PATCH', 'POST'):
return {}
if not hasattr(view, 'get_serializer'):
return {}
try:
serializer = view.get_serializer()
except exceptions.APIException:
serializer = None
warnings.warn('{}.get_serializer() raised an exception during '
'schema generation. Serializer fields will not be '
'generated for {} {}.'
.format(view.__class__.__name__, method, path))
if not isinstance(serializer, serializers.Serializer):
return {}
content = self._map_serializer(serializer)
# No required fields for PATCH
if method == 'PATCH':
del content['required']
# No read_only fields for request.
for name, schema in content['properties'].copy().items():
if 'readOnly' in schema:
del content['properties'][name]
return {
'content': {
ct: {'schema': content}
for ct in self.content_types
}
}
def _get_responses(self, path, method):
# TODO: Handle multiple codes.
content = {}
view = self.view
if hasattr(view, 'get_serializer'):
try:
serializer = view.get_serializer()
except exceptions.APIException:
serializer = None
warnings.warn('{}.get_serializer() raised an exception during '
'schema generation. Serializer fields will not be '
'generated for {} {}.'
.format(view.__class__.__name__, method, path))
if isinstance(serializer, serializers.Serializer):
content = self._map_serializer(serializer)
# No write_only fields for response.
for name, schema in content['properties'].copy().items():
if 'writeOnly' in schema:
del content['properties'][name]
content['required'] = [f for f in content['required'] if f != name]
return {
'200': {
'content': {
ct: {'schema': content}
for ct in self.content_types
}
}
}

View File

@ -3,6 +3,9 @@ utils.py # Shared helper functions
See schemas.__init__.py for package overview. See schemas.__init__.py for package overview.
""" """
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.mixins import RetrieveModelMixin from rest_framework.mixins import RetrieveModelMixin
@ -22,3 +25,17 @@ def is_list_view(path, method, view):
if path_components and '{' in path_components[-1]: if path_components and '{' in path_components[-1]:
return False return False
return True return True
def get_pk_description(model, model_field):
if isinstance(model_field, models.AutoField):
value_type = _('unique integer value')
elif isinstance(model_field, models.UUIDField):
value_type = _('UUID string')
else:
value_type = _('unique value')
return _('A {value_type} identifying this {name}.').format(
value_type=value_type,
name=model._meta.verbose_name,
)

View File

@ -5,6 +5,7 @@ See schemas.__init__.py for package overview.
""" """
from rest_framework import exceptions, renderers from rest_framework import exceptions, renderers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.schemas import coreapi
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.views import APIView from rest_framework.views import APIView
@ -17,12 +18,18 @@ class SchemaView(APIView):
public = False public = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SchemaView, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.renderer_classes is None: if self.renderer_classes is None:
self.renderer_classes = [ if coreapi.is_enabled():
renderers.OpenAPIRenderer, self.renderer_classes = [
renderers.CoreJSONRenderer renderers.CoreAPIOpenAPIRenderer,
] renderers.CoreJSONRenderer
]
else:
self.renderer_classes = [
renderers.OpenAPIRenderer,
renderers.JSONOpenAPIRenderer,
]
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES: if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
self.renderer_classes += [renderers.BrowsableAPIRenderer] self.renderer_classes += [renderers.BrowsableAPIRenderer]
@ -38,4 +45,4 @@ class SchemaView(APIView):
self.renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES self.renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
neg = self.perform_content_negotiation(self.request, force=True) neg = self.perform_content_negotiation(self.request, force=True)
self.request.accepted_renderer, self.request.accepted_media_type = neg self.request.accepted_renderer, self.request.accepted_media_type = neg
return super(SchemaView, self).handle_exception(exc) return super().handle_exception(exc)

View File

@ -10,12 +10,11 @@ python primitives.
2. The process of marshalling between python primitives and request and 2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers. response content is handled by parsers and renderers.
""" """
from __future__ import unicode_literals
import copy import copy
import inspect import inspect
import traceback import traceback
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Mapping
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
@ -23,11 +22,11 @@ from django.db import models
from django.db.models import DurationField as ModelDurationField from django.db.models import DurationField as ModelDurationField
from django.db.models.fields import Field as DjangoModelField from django.db.models.fields import Field as DjangoModelField
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
from django.utils import six, timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.compat import Mapping, postgres_fields, unicode_to_repr from rest_framework.compat import postgres_fields
from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.fields import get_error_detail, set_value from rest_framework.fields import get_error_detail, set_value
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -115,14 +114,14 @@ class BaseSerializer(Field):
self.partial = kwargs.pop('partial', False) self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {}) self._context = kwargs.pop('context', {})
kwargs.pop('many', None) kwargs.pop('many', None)
super(BaseSerializer, self).__init__(**kwargs) super().__init__(**kwargs)
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create # We override this method in order to automagically create
# `ListSerializer` classes instead when `many=True` is set. # `ListSerializer` classes instead when `many=True` is set.
if kwargs.pop('many', False): if kwargs.pop('many', False):
return cls.many_init(*args, **kwargs) return cls.many_init(*args, **kwargs)
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs) return super().__new__(cls, *args, **kwargs)
@classmethod @classmethod
def many_init(cls, *args, **kwargs): def many_init(cls, *args, **kwargs):
@ -315,7 +314,7 @@ class SerializerMetaclass(type):
def __new__(cls, name, bases, attrs): def __new__(cls, name, bases, attrs):
attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) return super().__new__(cls, name, bases, attrs)
def as_serializer_error(exc): def as_serializer_error(exc):
@ -344,13 +343,12 @@ def as_serializer_error(exc):
} }
@six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
class Serializer(BaseSerializer):
default_error_messages = { default_error_messages = {
'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
} }
@property @cached_property
def fields(self): def fields(self):
""" """
A dictionary of {field_name: field_instance}. A dictionary of {field_name: field_instance}.
@ -358,24 +356,22 @@ class Serializer(BaseSerializer):
# `fields` is evaluated lazily. We do this to ensure that we don't # `fields` is evaluated lazily. We do this to ensure that we don't
# have issues importing modules that use ModelSerializers as fields, # have issues importing modules that use ModelSerializers as fields,
# even if Django's app-loading stage has not yet run. # even if Django's app-loading stage has not yet run.
if not hasattr(self, '_fields'): fields = BindingDict(self)
self._fields = BindingDict(self) for key, value in self.get_fields().items():
for key, value in self.get_fields().items(): fields[key] = value
self._fields[key] = value return fields
return self._fields
@cached_property @property
def _writable_fields(self): def _writable_fields(self):
return [ for field in self.fields.values():
field for field in self.fields.values() if not field.read_only if not field.read_only:
] yield field
@cached_property @property
def _readable_fields(self): def _readable_fields(self):
return [ for field in self.fields.values():
field for field in self.fields.values() if not field.write_only:
if not field.write_only yield field
]
def get_fields(self): def get_fields(self):
""" """
@ -466,7 +462,7 @@ class Serializer(BaseSerializer):
to_validate.update(value) to_validate.update(value)
else: else:
to_validate = value to_validate = value
super(Serializer, self).run_validators(to_validate) super().run_validators(to_validate)
def to_internal_value(self, data): def to_internal_value(self, data):
""" """
@ -535,7 +531,7 @@ class Serializer(BaseSerializer):
return attrs return attrs
def __repr__(self): def __repr__(self):
return unicode_to_repr(representation.serializer_repr(self, indent=1)) return representation.serializer_repr(self, indent=1)
# The following are used for accessing `BoundField` instances on the # The following are used for accessing `BoundField` instances on the
# serializer, for the purposes of presenting a form-like API onto the # serializer, for the purposes of presenting a form-like API onto the
@ -560,12 +556,12 @@ class Serializer(BaseSerializer):
@property @property
def data(self): def data(self):
ret = super(Serializer, self).data ret = super().data
return ReturnDict(ret, serializer=self) return ReturnDict(ret, serializer=self)
@property @property
def errors(self): def errors(self):
ret = super(Serializer, self).errors ret = super().errors
if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null': if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null':
# Edge case. Provide a more descriptive error than # Edge case. Provide a more descriptive error than
# "this field may not be null", when no data is passed. # "this field may not be null", when no data is passed.
@ -591,11 +587,11 @@ class ListSerializer(BaseSerializer):
self.allow_empty = kwargs.pop('allow_empty', True) self.allow_empty = kwargs.pop('allow_empty', True)
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.'
super(ListSerializer, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.child.bind(field_name='', parent=self) self.child.bind(field_name='', parent=self)
def bind(self, field_name, parent): def bind(self, field_name, parent):
super(ListSerializer, self).bind(field_name, parent) super().bind(field_name, parent)
self.partial = self.parent.partial self.partial = self.parent.partial
def get_initial(self): def get_initial(self):
@ -758,19 +754,19 @@ class ListSerializer(BaseSerializer):
return not bool(self._errors) return not bool(self._errors)
def __repr__(self): def __repr__(self):
return unicode_to_repr(representation.list_repr(self, indent=1)) return representation.list_repr(self, indent=1)
# Include a backlink to the serializer class on return objects. # Include a backlink to the serializer class on return objects.
# Allows renderers such as HTMLFormRenderer to get the full field info. # Allows renderers such as HTMLFormRenderer to get the full field info.
@property @property
def data(self): def data(self):
ret = super(ListSerializer, self).data ret = super().data
return ReturnList(ret, serializer=self) return ReturnList(ret, serializer=self)
@property @property
def errors(self): def errors(self):
ret = super(ListSerializer, self).errors ret = super().errors
if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null': if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null':
# Edge case. Provide a more descriptive error than # Edge case. Provide a more descriptive error than
# "this field may not be null", when no data is passed. # "this field may not be null", when no data is passed.
@ -1236,6 +1232,11 @@ class ModelSerializer(Serializer):
# `allow_blank` is only valid for textual fields. # `allow_blank` is only valid for textual fields.
field_kwargs.pop('allow_blank', None) field_kwargs.pop('allow_blank', None)
if postgres_fields and isinstance(model_field, postgres_fields.JSONField):
# Populate the `encoder` argument of `JSONField` instances generated
# for the PostgreSQL specific `JSONField`.
field_kwargs['encoder'] = getattr(model_field, 'encoder', None)
if postgres_fields and isinstance(model_field, postgres_fields.ArrayField): if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
# Populate the `child` argument on `ListField` instances generated # Populate the `child` argument on `ListField` instances generated
# for the PostgreSQL specific `ArrayField`. # for the PostgreSQL specific `ArrayField`.

View File

@ -18,13 +18,9 @@ This module provides the `api_setting` object, that is used to access
REST framework settings, checking for user settings first, then falling REST framework settings, checking for user settings first, then falling
back to the defaults. back to the defaults.
""" """
from __future__ import unicode_literals
from importlib import import_module
from django.conf import settings from django.conf import settings
from django.test.signals import setting_changed from django.test.signals import setting_changed
from django.utils import six from django.utils.module_loading import import_string
from rest_framework import ISO_8601 from rest_framework import ISO_8601
@ -56,7 +52,7 @@ DEFAULTS = {
'DEFAULT_FILTER_BACKENDS': (), 'DEFAULT_FILTER_BACKENDS': (),
# Schema # Schema
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema', 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema',
# Throttling # Throttling
'DEFAULT_THROTTLE_RATES': { 'DEFAULT_THROTTLE_RATES': {
@ -166,7 +162,7 @@ def perform_import(val, setting_name):
""" """
if val is None: if val is None:
return None return None
elif isinstance(val, six.string_types): elif isinstance(val, str):
return import_from_string(val, setting_name) return import_from_string(val, setting_name)
elif isinstance(val, (list, tuple)): elif isinstance(val, (list, tuple)):
return [import_from_string(item, setting_name) for item in val] return [import_from_string(item, setting_name) for item in val]
@ -178,16 +174,13 @@ def import_from_string(val, setting_name):
Attempt to import a class from a string representation. Attempt to import a class from a string representation.
""" """
try: try:
# Nod to tastypie's use of importlib. return import_string(val)
module_path, class_name = val.rsplit('.', 1) except ImportError as e:
module = import_module(module_path)
return getattr(module, class_name)
except (ImportError, AttributeError) as e:
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e) msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
raise ImportError(msg) raise ImportError(msg)
class APISettings(object): class APISettings:
""" """
A settings object, that allows API settings to be accessed as properties. A settings object, that allows API settings to be accessed as properties.
For example: For example:

View File

@ -5,7 +5,6 @@ See RFC 2616 - https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
And RFC 6585 - https://tools.ietf.org/html/rfc6585 And RFC 6585 - https://tools.ietf.org/html/rfc6585
And RFC 4918 - https://tools.ietf.org/html/rfc4918 And RFC 4918 - https://tools.ietf.org/html/rfc4918
""" """
from __future__ import unicode_literals
def is_informational(code): def is_informational(code):
@ -38,6 +37,8 @@ HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205 HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206 HTTP_206_PARTIAL_CONTENT = 206
HTTP_207_MULTI_STATUS = 207 HTTP_207_MULTI_STATUS = 207
HTTP_208_ALREADY_REPORTED = 208
HTTP_226_IM_USED = 226
HTTP_300_MULTIPLE_CHOICES = 300 HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301 HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302 HTTP_302_FOUND = 302
@ -46,6 +47,7 @@ HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305 HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306 HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307 HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_308_PERMANENT_REDIRECT = 308
HTTP_400_BAD_REQUEST = 400 HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401 HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402 HTTP_402_PAYMENT_REQUIRED = 402
@ -67,6 +69,7 @@ HTTP_417_EXPECTATION_FAILED = 417
HTTP_422_UNPROCESSABLE_ENTITY = 422 HTTP_422_UNPROCESSABLE_ENTITY = 422
HTTP_423_LOCKED = 423 HTTP_423_LOCKED = 423
HTTP_424_FAILED_DEPENDENCY = 424 HTTP_424_FAILED_DEPENDENCY = 424
HTTP_426_UPGRADE_REQUIRED = 426
HTTP_428_PRECONDITION_REQUIRED = 428 HTTP_428_PRECONDITION_REQUIRED = 428
HTTP_429_TOO_MANY_REQUESTS = 429 HTTP_429_TOO_MANY_REQUESTS = 429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
@ -77,5 +80,9 @@ HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503 HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504 HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
HTTP_507_INSUFFICIENT_STORAGE = 507 HTTP_507_INSUFFICIENT_STORAGE = 507
HTTP_508_LOOP_DETECTED = 508
HTTP_509_BANDWIDTH_LIMIT_EXCEEDED = 509
HTTP_510_NOT_EXTENDED = 510
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511

View File

@ -12,7 +12,7 @@
<div class="modal-body"> <div class="modal-body">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<h4 class="text-center">You are logged in as {{ user.username }}.</h4> <h4 class="text-center">You are logged in as {{ user.get_username }}.</h4>
{% else %} {% else %}
<div class="text-center"> <div class="text-center">

View File

@ -1,12 +1,9 @@
from __future__ import absolute_import, unicode_literals
import re import re
from collections import OrderedDict from collections import OrderedDict
from django import template from django import template
from django.template import loader from django.template import loader
from django.urls import NoReverseMatch, reverse from django.urls import NoReverseMatch, reverse
from django.utils import six
from django.utils.encoding import force_text, iri_to_uri from django.utils.encoding import force_text, iri_to_uri
from django.utils.html import escape, format_html, smart_urlquote from django.utils.html import escape, format_html, smart_urlquote
from django.utils.safestring import SafeData, mark_safe from django.utils.safestring import SafeData, mark_safe
@ -187,7 +184,7 @@ def add_class(value, css_class):
In the case of REST Framework, the filter is used to add Bootstrap-specific In the case of REST Framework, the filter is used to add Bootstrap-specific
classes to the forms. classes to the forms.
""" """
html = six.text_type(value) html = str(value)
match = class_re.search(html) match = class_re.search(html)
if match: if match:
m = re.search(r'^%s$|^%s\s|\s%s\s|\s%s$' % (css_class, css_class, m = re.search(r'^%s$|^%s\s|\s%s\s|\s%s$' % (css_class, css_class,
@ -204,7 +201,7 @@ def add_class(value, css_class):
@register.filter @register.filter
def format_value(value): def format_value(value):
if getattr(value, 'is_hyperlink', False): if getattr(value, 'is_hyperlink', False):
name = six.text_type(value.obj) name = str(value.obj)
return mark_safe('<a href=%s>%s</a>' % (value, escape(name))) return mark_safe('<a href=%s>%s</a>' % (value, escape(name)))
if value is None or isinstance(value, bool): if value is None or isinstance(value, bool):
return mark_safe('<code>%s</code>' % {True: 'true', False: 'false', None: 'null'}[value]) return mark_safe('<code>%s</code>' % {True: 'true', False: 'false', None: 'null'}[value])
@ -219,7 +216,7 @@ def format_value(value):
template = loader.get_template('rest_framework/admin/dict_value.html') template = loader.get_template('rest_framework/admin/dict_value.html')
context = {'value': value} context = {'value': value}
return template.render(context) return template.render(context)
elif isinstance(value, six.string_types): elif isinstance(value, str):
if ( if (
(value.startswith('http:') or value.startswith('https:')) and not (value.startswith('http:') or value.startswith('https:')) and not
re.search(r'\s', value) re.search(r'\s', value)
@ -229,7 +226,7 @@ def format_value(value):
return mark_safe('<a href="mailto:{value}">{value}</a>'.format(value=escape(value))) return mark_safe('<a href="mailto:{value}">{value}</a>'.format(value=escape(value)))
elif '\n' in value: elif '\n' in value:
return mark_safe('<pre>%s</pre>' % escape(value)) return mark_safe('<pre>%s</pre>' % escape(value))
return six.text_type(value) return str(value)
@register.filter @register.filter

View File

@ -1,9 +1,5 @@
# -- coding: utf-8 --
# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order # Note that we import as `DjangoRequestFactory` and `DjangoClient` in order
# to make it harder for the user to import the wrong thing without realizing. # to make it harder for the user to import the wrong thing without realizing.
from __future__ import unicode_literals
import io import io
from importlib import import_module from importlib import import_module
@ -14,7 +10,6 @@ from django.test import override_settings, testcases
from django.test.client import Client as DjangoClient from django.test.client import Client as DjangoClient
from django.test.client import ClientHandler from django.test.client import ClientHandler
from django.test.client import RequestFactory as DjangoRequestFactory from django.test.client import RequestFactory as DjangoRequestFactory
from django.utils import six
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.http import urlencode from django.utils.http import urlencode
@ -32,7 +27,7 @@ if requests is not None:
def get_all(self, key, default): def get_all(self, key, default):
return self.getheaders(key) return self.getheaders(key)
class MockOriginalResponse(object): class MockOriginalResponse:
def __init__(self, headers): def __init__(self, headers):
self.msg = HeaderDict(headers) self.msg = HeaderDict(headers)
self.closed = False self.closed = False
@ -109,7 +104,7 @@ if requests is not None:
class RequestsClient(requests.Session): class RequestsClient(requests.Session):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RequestsClient, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
adapter = DjangoTestAdapter() adapter = DjangoTestAdapter()
self.mount('http://', adapter) self.mount('http://', adapter)
self.mount('https://', adapter) self.mount('https://', adapter)
@ -117,7 +112,7 @@ if requests is not None:
def request(self, method, url, *args, **kwargs): def request(self, method, url, *args, **kwargs):
if not url.startswith('http'): if not url.startswith('http'):
raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url) raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url)
return super(RequestsClient, self).request(method, url, *args, **kwargs) return super().request(method, url, *args, **kwargs)
else: else:
def RequestsClient(*args, **kwargs): def RequestsClient(*args, **kwargs):
@ -129,7 +124,7 @@ if coreapi is not None:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._session = RequestsClient() self._session = RequestsClient()
kwargs['transports'] = [coreapi.transports.HTTPTransport(session=self.session)] kwargs['transports'] = [coreapi.transports.HTTPTransport(session=self.session)]
return super(CoreAPIClient, self).__init__(*args, **kwargs) return super().__init__(*args, **kwargs)
@property @property
def session(self): def session(self):
@ -149,7 +144,7 @@ class APIRequestFactory(DjangoRequestFactory):
self.renderer_classes = {} self.renderer_classes = {}
for cls in self.renderer_classes_list: for cls in self.renderer_classes_list:
self.renderer_classes[cls.format] = cls self.renderer_classes[cls.format] = cls
super(APIRequestFactory, self).__init__(**defaults) super().__init__(**defaults)
def _encode_data(self, data, format=None, content_type=None): def _encode_data(self, data, format=None, content_type=None):
""" """
@ -171,7 +166,7 @@ class APIRequestFactory(DjangoRequestFactory):
format = format or self.default_format format = format or self.default_format
assert format in self.renderer_classes, ( assert format in self.renderer_classes, (
"Invalid format '{0}'. Available formats are {1}. " "Invalid format '{}'. Available formats are {}. "
"Set TEST_REQUEST_RENDERER_CLASSES to enable " "Set TEST_REQUEST_RENDERER_CLASSES to enable "
"extra request formats.".format( "extra request formats.".format(
format, format,
@ -184,13 +179,13 @@ class APIRequestFactory(DjangoRequestFactory):
ret = renderer.render(data) ret = renderer.render(data)
# Determine the content-type header from the renderer # Determine the content-type header from the renderer
content_type = "{0}; charset={1}".format( content_type = "{}; charset={}".format(
renderer.media_type, renderer.charset renderer.media_type, renderer.charset
) )
# Coerce text to bytes if required. # Coerce text to bytes if required.
if isinstance(ret, six.text_type): if isinstance(ret, str):
ret = bytes(ret.encode(renderer.charset)) ret = ret.encode(renderer.charset)
return ret, content_type return ret, content_type
@ -202,8 +197,7 @@ class APIRequestFactory(DjangoRequestFactory):
# Fix to support old behavior where you have the arguments in the # Fix to support old behavior where you have the arguments in the
# url. See #1461. # url. See #1461.
query_string = force_bytes(path.split('?')[1]) query_string = force_bytes(path.split('?')[1])
if six.PY3: query_string = query_string.decode('iso-8859-1')
query_string = query_string.decode('iso-8859-1')
r['QUERY_STRING'] = query_string r['QUERY_STRING'] = query_string
r.update(extra) r.update(extra)
return self.generic('GET', path, **r) return self.generic('GET', path, **r)
@ -234,11 +228,11 @@ class APIRequestFactory(DjangoRequestFactory):
if content_type is not None: if content_type is not None:
extra['CONTENT_TYPE'] = str(content_type) extra['CONTENT_TYPE'] = str(content_type)
return super(APIRequestFactory, self).generic( return super().generic(
method, path, data, content_type, secure, **extra) method, path, data, content_type, secure, **extra)
def request(self, **kwargs): def request(self, **kwargs):
request = super(APIRequestFactory, self).request(**kwargs) request = super().request(**kwargs)
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
return request return request
@ -252,18 +246,18 @@ class ForceAuthClientHandler(ClientHandler):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._force_user = None self._force_user = None
self._force_token = None self._force_token = None
super(ForceAuthClientHandler, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_response(self, request): def get_response(self, request):
# This is the simplest place we can hook into to patch the # This is the simplest place we can hook into to patch the
# request object. # request object.
force_authenticate(request, self._force_user, self._force_token) force_authenticate(request, self._force_user, self._force_token)
return super(ForceAuthClientHandler, self).get_response(request) return super().get_response(request)
class APIClient(APIRequestFactory, DjangoClient): class APIClient(APIRequestFactory, DjangoClient):
def __init__(self, enforce_csrf_checks=False, **defaults): def __init__(self, enforce_csrf_checks=False, **defaults):
super(APIClient, self).__init__(**defaults) super().__init__(**defaults)
self.handler = ForceAuthClientHandler(enforce_csrf_checks) self.handler = ForceAuthClientHandler(enforce_csrf_checks)
self._credentials = {} self._credentials = {}
@ -286,17 +280,17 @@ class APIClient(APIRequestFactory, DjangoClient):
def request(self, **kwargs): def request(self, **kwargs):
# Ensure that any credentials set get added to every request. # Ensure that any credentials set get added to every request.
kwargs.update(self._credentials) kwargs.update(self._credentials)
return super(APIClient, self).request(**kwargs) return super().request(**kwargs)
def get(self, path, data=None, follow=False, **extra): def get(self, path, data=None, follow=False, **extra):
response = super(APIClient, self).get(path, data=data, **extra) response = super().get(path, data=data, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
return response return response
def post(self, path, data=None, format=None, content_type=None, def post(self, path, data=None, format=None, content_type=None,
follow=False, **extra): follow=False, **extra):
response = super(APIClient, self).post( response = super().post(
path, data=data, format=format, content_type=content_type, **extra) path, data=data, format=format, content_type=content_type, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
@ -304,7 +298,7 @@ class APIClient(APIRequestFactory, DjangoClient):
def put(self, path, data=None, format=None, content_type=None, def put(self, path, data=None, format=None, content_type=None,
follow=False, **extra): follow=False, **extra):
response = super(APIClient, self).put( response = super().put(
path, data=data, format=format, content_type=content_type, **extra) path, data=data, format=format, content_type=content_type, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
@ -312,7 +306,7 @@ class APIClient(APIRequestFactory, DjangoClient):
def patch(self, path, data=None, format=None, content_type=None, def patch(self, path, data=None, format=None, content_type=None,
follow=False, **extra): follow=False, **extra):
response = super(APIClient, self).patch( response = super().patch(
path, data=data, format=format, content_type=content_type, **extra) path, data=data, format=format, content_type=content_type, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
@ -320,7 +314,7 @@ class APIClient(APIRequestFactory, DjangoClient):
def delete(self, path, data=None, format=None, content_type=None, def delete(self, path, data=None, format=None, content_type=None,
follow=False, **extra): follow=False, **extra):
response = super(APIClient, self).delete( response = super().delete(
path, data=data, format=format, content_type=content_type, **extra) path, data=data, format=format, content_type=content_type, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
@ -328,7 +322,7 @@ class APIClient(APIRequestFactory, DjangoClient):
def options(self, path, data=None, format=None, content_type=None, def options(self, path, data=None, format=None, content_type=None,
follow=False, **extra): follow=False, **extra):
response = super(APIClient, self).options( response = super().options(
path, data=data, format=format, content_type=content_type, **extra) path, data=data, format=format, content_type=content_type, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
@ -342,7 +336,7 @@ class APIClient(APIRequestFactory, DjangoClient):
self.handler._force_token = None self.handler._force_token = None
if self.session: if self.session:
super(APIClient, self).logout() super().logout()
class APITransactionTestCase(testcases.TransactionTestCase): class APITransactionTestCase(testcases.TransactionTestCase):
@ -389,11 +383,11 @@ class URLPatternsTestCase(testcases.SimpleTestCase):
cls._module.urlpatterns = cls.urlpatterns cls._module.urlpatterns = cls.urlpatterns
cls._override.enable() cls._override.enable()
super(URLPatternsTestCase, cls).setUpClass() super().setUpClass()
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
super(URLPatternsTestCase, cls).tearDownClass() super().tearDownClass()
cls._override.disable() cls._override.disable()
if hasattr(cls, '_module_urlpatterns'): if hasattr(cls, '_module_urlpatterns'):

View File

@ -1,8 +1,6 @@
""" """
Provides various throttling policies. Provides various throttling policies.
""" """
from __future__ import unicode_literals
import time import time
from django.core.cache import cache as default_cache from django.core.cache import cache as default_cache
@ -11,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
class BaseThrottle(object): class BaseThrottle:
""" """
Rate throttling of requests. Rate throttling of requests.
""" """
@ -232,7 +230,7 @@ class ScopedRateThrottle(SimpleRateThrottle):
self.num_requests, self.duration = self.parse_rate(self.rate) self.num_requests, self.duration = self.parse_rate(self.rate)
# We can now proceed as normal. # We can now proceed as normal.
return super(ScopedRateThrottle, self).allow_request(request, view) return super().allow_request(request, view)
def get_cache_key(self, request, view): def get_cache_key(self, request, view):
""" """

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.conf.urls import include, url from django.conf.urls import include, url
from rest_framework.compat import ( from rest_framework.compat import (

View File

@ -11,8 +11,6 @@ your API requires authentication:
You should make sure your authentication settings include `SessionAuthentication`. You should make sure your authentication settings include `SessionAuthentication`.
""" """
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth import views from django.contrib.auth import views

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.urls import get_script_prefix, resolve from django.urls import get_script_prefix, resolve

View File

@ -1,15 +1,13 @@
""" """
Helper classes for parsers. Helper classes for parsers.
""" """
from __future__ import absolute_import, unicode_literals
import datetime import datetime
import decimal import decimal
import json # noqa import json # noqa
import uuid import uuid
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils import six, timezone from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.functional import Promise from django.utils.functional import Promise
@ -39,17 +37,17 @@ class JSONEncoder(json.JSONEncoder):
representation = obj.isoformat() representation = obj.isoformat()
return representation return representation
elif isinstance(obj, datetime.timedelta): elif isinstance(obj, datetime.timedelta):
return six.text_type(obj.total_seconds()) return str(obj.total_seconds())
elif isinstance(obj, decimal.Decimal): elif isinstance(obj, decimal.Decimal):
# Serializers will coerce decimals to strings by default. # Serializers will coerce decimals to strings by default.
return float(obj) return float(obj)
elif isinstance(obj, uuid.UUID): elif isinstance(obj, uuid.UUID):
return six.text_type(obj) return str(obj)
elif isinstance(obj, QuerySet): elif isinstance(obj, QuerySet):
return tuple(obj) return tuple(obj)
elif isinstance(obj, bytes): elif isinstance(obj, bytes):
# Best-effort for binary blobs. See #4187. # Best-effort for binary blobs. See #4187.
return obj.decode('utf-8') return obj.decode()
elif hasattr(obj, 'tolist'): elif hasattr(obj, 'tolist'):
# Numpy arrays and array scalars. # Numpy arrays and array scalars.
return obj.tolist() return obj.tolist()
@ -65,4 +63,4 @@ class JSONEncoder(json.JSONEncoder):
pass pass
elif hasattr(obj, '__iter__'): elif hasattr(obj, '__iter__'):
return tuple(item for item in obj) return tuple(item for item in obj)
return super(JSONEncoder, self).default(obj) return super().default(obj)

View File

@ -16,7 +16,7 @@ NUMERIC_FIELD_TYPES = (
) )
class ClassLookupDict(object): class ClassLookupDict:
""" """
Takes a dictionary with classes as keys. Takes a dictionary with classes as keys.
Lookups against this object will traverses the object's inheritance Lookups against this object will traverses the object's inheritance
@ -106,8 +106,7 @@ def get_field_kwargs(field_name, model_field):
if model_field.null and not isinstance(model_field, models.NullBooleanField): if model_field.null and not isinstance(model_field, models.NullBooleanField):
kwargs['allow_null'] = True kwargs['allow_null'] = True
if model_field.blank and (isinstance(model_field, models.CharField) or if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
isinstance(model_field, models.TextField)):
kwargs['allow_blank'] = True kwargs['allow_blank'] = True
if isinstance(model_field, models.FilePathField): if isinstance(model_field, models.FilePathField):
@ -193,9 +192,7 @@ def get_field_kwargs(field_name, model_field):
# Ensure that max_length is passed explicitly as a keyword arg, # Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator. # rather than as a validator.
max_length = getattr(model_field, 'max_length', None) max_length = getattr(model_field, 'max_length', None)
if max_length is not None and (isinstance(model_field, models.CharField) or if max_length is not None and (isinstance(model_field, (models.CharField, models.TextField, models.FileField))):
isinstance(model_field, models.TextField) or
isinstance(model_field, models.FileField)):
kwargs['max_length'] = max_length kwargs['max_length'] = max_length
validator_kwarg = [ validator_kwarg = [
validator for validator in validator_kwarg validator for validator in validator_kwarg

View File

@ -1,8 +1,6 @@
""" """
Utility functions to return a formatted name and description for a given view. Utility functions to return a formatted name and description for a given view.
""" """
from __future__ import unicode_literals
import re import re
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -67,3 +65,29 @@ def markup_description(description):
description = escape(description).replace('\n', '<br />') description = escape(description).replace('\n', '<br />')
description = '<p>' + description + '</p>' description = '<p>' + description + '</p>'
return mark_safe(description) return mark_safe(description)
class lazy_format:
"""
Delay formatting until it's actually needed.
Useful when the format string or one of the arguments is lazy.
Not using Django's lazy because it is too slow.
"""
__slots__ = ('format_string', 'args', 'kwargs', 'result')
def __init__(self, format_string, *args, **kwargs):
self.result = None
self.format_string = format_string
self.args = args
self.kwargs = kwargs
def __str__(self):
if self.result is None:
self.result = self.format_string.format(*self.args, **self.kwargs)
self.format_string, self.args, self.kwargs = None, None, None
return self.result
def __mod__(self, value):
return str(self) % value

View File

@ -5,9 +5,6 @@ REST framework should always import this wrapper module in order to maintain
spec-compliant encoding/decoding. Support for non-standard features should be spec-compliant encoding/decoding. Support for non-standard features should be
handled by users at the renderer and parser layer. handled by users at the renderer and parser layer.
""" """
from __future__ import absolute_import
import functools import functools
import json # noqa import json # noqa

View File

@ -3,10 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers.
See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
""" """
from __future__ import unicode_literals
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.utils.encoding import python_2_unicode_compatible
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
@ -46,8 +43,7 @@ def order_by_precedence(media_type_lst):
return [media_types for media_types in ret if media_types] return [media_types for media_types in ret if media_types]
@python_2_unicode_compatible class _MediaType:
class _MediaType(object):
def __init__(self, media_type_str): def __init__(self, media_type_str):
self.orig = '' if (media_type_str is None) else media_type_str self.orig = '' if (media_type_str is None) else media_type_str
self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING)) self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING))

View File

@ -2,16 +2,12 @@
Helper functions for creating user-friendly representations Helper functions for creating user-friendly representations
of serializer classes and serializer fields. of serializer classes and serializer fields.
""" """
from __future__ import unicode_literals
import re import re
from django.db import models from django.db import models
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.functional import Promise from django.utils.functional import Promise
from rest_framework.compat import unicode_repr
def manager_repr(value): def manager_repr(value):
model = value.model model = value.model
@ -34,7 +30,7 @@ def smart_repr(value):
if isinstance(value, Promise) and value._delegate_text: if isinstance(value, Promise) and value._delegate_text:
value = force_text(value) value = force_text(value)
value = unicode_repr(value) value = repr(value)
# Representations like u'help text' # Representations like u'help text'
# should simply be presented as 'help text' # should simply be presented as 'help text'
@ -45,9 +41,7 @@ def smart_repr(value):
# <django.core.validators.RegexValidator object at 0x1047af050> # <django.core.validators.RegexValidator object at 0x1047af050>
# Should be presented as # Should be presented as
# <django.core.validators.RegexValidator object> # <django.core.validators.RegexValidator object>
value = re.sub(' at 0x[0-9A-Fa-f]{4,32}>', '>', value) return re.sub(' at 0x[0-9A-Fa-f]{4,32}>', '>', value)
return value
def field_repr(field, force_many=False): def field_repr(field, force_many=False):

View File

@ -1,10 +1,8 @@
from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from collections.abc import MutableMapping
from django.utils.encoding import force_text from django.utils.encoding import force_text
from rest_framework.compat import MutableMapping, unicode_to_repr
from rest_framework.utils import json from rest_framework.utils import json
@ -17,7 +15,7 @@ class ReturnDict(OrderedDict):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.serializer = kwargs.pop('serializer') self.serializer = kwargs.pop('serializer')
super(ReturnDict, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def copy(self): def copy(self):
return ReturnDict(self, serializer=self.serializer) return ReturnDict(self, serializer=self.serializer)
@ -40,7 +38,7 @@ class ReturnList(list):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.serializer = kwargs.pop('serializer') self.serializer = kwargs.pop('serializer')
super(ReturnList, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def __repr__(self): def __repr__(self):
return list.__repr__(self) return list.__repr__(self)
@ -51,7 +49,7 @@ class ReturnList(list):
return (list, (list(self),)) return (list, (list(self),))
class BoundField(object): class BoundField:
""" """
A field object that also includes `.value` and `.error` properties. A field object that also includes `.value` and `.error` properties.
Returned when iterating over a serializer instance, Returned when iterating over a serializer instance,
@ -73,9 +71,9 @@ class BoundField(object):
return self._field.__class__ return self._field.__class__
def __repr__(self): def __repr__(self):
return unicode_to_repr('<%s value=%s errors=%s>' % ( return '<%s value=%s errors=%s>' % (
self.__class__.__name__, self.value, self.errors self.__class__.__name__, self.value, self.errors
)) )
def as_form_field(self): def as_form_field(self):
value = '' if (self.value is None or self.value is False) else self.value value = '' if (self.value is None or self.value is False) else self.value
@ -103,9 +101,9 @@ class NestedBoundField(BoundField):
""" """
def __init__(self, field, value, errors, prefix=''): def __init__(self, field, value, errors, prefix=''):
if value is None or value is '': if value is None or value == '':
value = {} value = {}
super(NestedBoundField, self).__init__(field, value, errors, prefix) super().__init__(field, value, errors, prefix)
def __iter__(self): def __iter__(self):
for field in self.fields.values(): for field in self.fields.values():

View File

@ -1,5 +1,6 @@
from urllib import parse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.six.moves.urllib import parse as urlparse
def replace_query_param(url, key, val): def replace_query_param(url, key, val):
@ -7,11 +8,11 @@ def replace_query_param(url, key, val):
Given a URL and a key/val pair, set or replace an item in the query Given a URL and a key/val pair, set or replace an item in the query
parameters of the URL, and return the new URL. parameters of the URL, and return the new URL.
""" """
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(force_str(url)) (scheme, netloc, path, query, fragment) = parse.urlsplit(force_str(url))
query_dict = urlparse.parse_qs(query, keep_blank_values=True) query_dict = parse.parse_qs(query, keep_blank_values=True)
query_dict[force_str(key)] = [force_str(val)] query_dict[force_str(key)] = [force_str(val)]
query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) query = parse.urlencode(sorted(list(query_dict.items())), doseq=True)
return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) return parse.urlunsplit((scheme, netloc, path, query, fragment))
def remove_query_param(url, key): def remove_query_param(url, key):
@ -19,8 +20,8 @@ def remove_query_param(url, key):
Given a URL and a key/val pair, remove an item in the query Given a URL and a key/val pair, remove an item in the query
parameters of the URL, and return the new URL. parameters of the URL, and return the new URL.
""" """
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(force_str(url)) (scheme, netloc, path, query, fragment) = parse.urlsplit(force_str(url))
query_dict = urlparse.parse_qs(query, keep_blank_values=True) query_dict = parse.parse_qs(query, keep_blank_values=True)
query_dict.pop(key, None) query_dict.pop(key, None)
query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) query = parse.urlencode(sorted(list(query_dict.items())), doseq=True)
return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) return parse.urlunsplit((scheme, netloc, path, query, fragment))

View File

@ -6,12 +6,9 @@ This gives us better separation of concerns, allows us to use single-step
object creation, and makes it possible to switch between using the implicit object creation, and makes it possible to switch between using the implicit
`ModelSerializer` class and an equivalent explicit `Serializer` class. `ModelSerializer` class and an equivalent explicit `Serializer` class.
""" """
from __future__ import unicode_literals
from django.db import DataError from django.db import DataError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.compat import unicode_to_repr
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.utils.representation import smart_repr from rest_framework.utils.representation import smart_repr
@ -33,7 +30,7 @@ def qs_filter(queryset, **kwargs):
return queryset.none() return queryset.none()
class UniqueValidator(object): class UniqueValidator:
""" """
Validator that corresponds to `unique=True` on a model field. Validator that corresponds to `unique=True` on a model field.
@ -82,13 +79,13 @@ class UniqueValidator(object):
raise ValidationError(self.message, code='unique') raise ValidationError(self.message, code='unique')
def __repr__(self): def __repr__(self):
return unicode_to_repr('<%s(queryset=%s)>' % ( return '<%s(queryset=%s)>' % (
self.__class__.__name__, self.__class__.__name__,
smart_repr(self.queryset) smart_repr(self.queryset)
)) )
class UniqueTogetherValidator(object): class UniqueTogetherValidator:
""" """
Validator that corresponds to `unique_together = (...)` on a model class. Validator that corresponds to `unique_together = (...)` on a model class.
@ -170,14 +167,14 @@ class UniqueTogetherValidator(object):
raise ValidationError(message, code='unique') raise ValidationError(message, code='unique')
def __repr__(self): def __repr__(self):
return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % ( return '<%s(queryset=%s, fields=%s)>' % (
self.__class__.__name__, self.__class__.__name__,
smart_repr(self.queryset), smart_repr(self.queryset),
smart_repr(self.fields) smart_repr(self.fields)
)) )
class BaseUniqueForValidator(object): class BaseUniqueForValidator:
message = None message = None
missing_message = _('This field is required.') missing_message = _('This field is required.')
@ -236,12 +233,12 @@ class BaseUniqueForValidator(object):
}, code='unique') }, code='unique')
def __repr__(self): def __repr__(self):
return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % ( return '<%s(queryset=%s, field=%s, date_field=%s)>' % (
self.__class__.__name__, self.__class__.__name__,
smart_repr(self.queryset), smart_repr(self.queryset),
smart_repr(self.field), smart_repr(self.field),
smart_repr(self.date_field) smart_repr(self.date_field)
)) )
class UniqueForDateValidator(BaseUniqueForValidator): class UniqueForDateValidator(BaseUniqueForValidator):

View File

@ -1,9 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import re import re
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.compat import unicode_http_header from rest_framework.compat import unicode_http_header
@ -13,7 +10,7 @@ from rest_framework.templatetags.rest_framework import replace_query_param
from rest_framework.utils.mediatypes import _MediaType from rest_framework.utils.mediatypes import _MediaType
class BaseVersioning(object): class BaseVersioning:
default_version = api_settings.DEFAULT_VERSION default_version = api_settings.DEFAULT_VERSION
allowed_versions = api_settings.ALLOWED_VERSIONS allowed_versions = api_settings.ALLOWED_VERSIONS
version_param = api_settings.VERSION_PARAM version_param = api_settings.VERSION_PARAM
@ -87,7 +84,7 @@ class URLPathVersioning(BaseVersioning):
kwargs = {} if (kwargs is None) else kwargs kwargs = {} if (kwargs is None) else kwargs
kwargs[self.version_param] = request.version kwargs[self.version_param] = request.version
return super(URLPathVersioning, self).reverse( return super().reverse(
viewname, args, kwargs, request, format, **extra viewname, args, kwargs, request, format, **extra
) )
@ -133,7 +130,7 @@ class NamespaceVersioning(BaseVersioning):
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
if request.version is not None: if request.version is not None:
viewname = self.get_versioned_viewname(viewname, request) viewname = self.get_versioned_viewname(viewname, request)
return super(NamespaceVersioning, self).reverse( return super().reverse(
viewname, args, kwargs, request, format, **extra viewname, args, kwargs, request, format, **extra
) )
@ -179,7 +176,7 @@ class QueryParameterVersioning(BaseVersioning):
return version return version
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
url = super(QueryParameterVersioning, self).reverse( url = super().reverse(
viewname, args, kwargs, request, format, **extra viewname, args, kwargs, request, format, **extra
) )
if request.version is not None: if request.version is not None:

View File

@ -1,8 +1,6 @@
""" """
Provides an APIView class that is the base of all views in REST framework. Provides an APIView class that is the base of all views in REST framework.
""" """
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import connection, models, transaction from django.db import connection, models, transaction
@ -137,7 +135,7 @@ class APIView(View):
) )
cls.queryset._fetch_all = force_evaluation cls.queryset._fetch_all = force_evaluation
view = super(APIView, cls).as_view(**initkwargs) view = super().as_view(**initkwargs)
view.cls = cls view.cls = cls
view.initkwargs = initkwargs view.initkwargs = initkwargs
@ -352,9 +350,13 @@ class APIView(View):
Check if request should be throttled. Check if request should be throttled.
Raises an appropriate exception if the request is throttled. Raises an appropriate exception if the request is throttled.
""" """
throttle_durations = []
for throttle in self.get_throttles(): for throttle in self.get_throttles():
if not throttle.allow_request(request, self): if not throttle.allow_request(request, self):
self.throttled(request, throttle.wait()) throttle_durations.append(throttle.wait())
if throttle_durations:
self.throttled(request, max(throttle_durations))
def determine_version(self, request, *args, **kwargs): def determine_version(self, request, *args, **kwargs):
""" """

View File

@ -16,8 +16,6 @@ automatically.
router.register(r'users', UserViewSet, 'user') router.register(r'users', UserViewSet, 'user')
urlpatterns = router.urls urlpatterns = router.urls
""" """
from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from functools import update_wrapper from functools import update_wrapper
from inspect import getmembers from inspect import getmembers
@ -34,7 +32,7 @@ def _is_extra_action(attr):
return hasattr(attr, 'mapping') return hasattr(attr, 'mapping')
class ViewSetMixin(object): class ViewSetMixin:
""" """
This is the magic. This is the magic.
@ -134,7 +132,7 @@ class ViewSetMixin(object):
""" """
Set the `.action` attribute on the view, depending on the request method. Set the `.action` attribute on the view, depending on the request method.
""" """
request = super(ViewSetMixin, self).initialize_request(request, *args, **kwargs) request = super().initialize_request(request, *args, **kwargs)
method = request.method.lower() method = request.method.lower()
if method == 'options': if method == 'options':
# This is a special case as we always provide handling for the # This is a special case as we always provide handling for the

View File

@ -1,6 +1,4 @@
#! /usr/bin/env python #! /usr/bin/env python3
from __future__ import print_function
import subprocess import subprocess
import sys import sys
@ -13,7 +11,7 @@ PYTEST_ARGS = {
FLAKE8_ARGS = ['rest_framework', 'tests'] FLAKE8_ARGS = ['rest_framework', 'tests']
ISORT_ARGS = ['--recursive', '--check-only', '--diff', '-o' 'uritemplate', '-p', 'tests', 'rest_framework', 'tests'] ISORT_ARGS = ['--recursive', '--check-only', '--diff', 'rest_framework', 'tests']
def exit_on_failure(ret, message=None): def exit_on_failure(ret, message=None):

View File

@ -1,6 +1,3 @@
[bdist_wheel]
universal = 1
[metadata] [metadata]
license_file = LICENSE.md license_file = LICENSE.md
@ -17,8 +14,8 @@ skip=.tox
atomic=true atomic=true
multi_line_output=5 multi_line_output=5
known_standard_library=types known_standard_library=types
known_third_party=pytest,_pytest,django,pytz known_third_party=pytest,_pytest,django,pytz,uritemplate
known_first_party=rest_framework known_first_party=rest_framework,tests
[coverage:run] [coverage:run]
# NOTE: source is ignored with pytest-cov (but uses the same). # NOTE: source is ignored with pytest-cov (but uses the same).

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os import os
import re import re
import shutil import shutil
@ -8,6 +7,34 @@ from io import open
from setuptools import find_packages, setup from setuptools import find_packages, setup
CURRENT_PYTHON = sys.version_info[:2]
REQUIRED_PYTHON = (3, 5)
# This check and everything above must remain compatible with Python 2.7.
if CURRENT_PYTHON < REQUIRED_PYTHON:
sys.stderr.write("""
==========================
Unsupported Python version
==========================
This version of Django REST Framework requires Python {}.{}, but you're trying
to install it on Python {}.{}.
This may be because you are using a version of pip that doesn't
understand the python_requires classifier. Make sure you
have pip >= 9.0 and setuptools >= 24.2, then try again:
$ python -m pip install --upgrade pip setuptools
$ python -m pip install djangorestframework
This will install the latest version of Django REST Framework which works on
your version of Python. If you can't upgrade your pip (or Python), request
an older version of Django REST Framework:
$ python -m pip install "django<3.10"
""".format(*(REQUIRED_PYTHON + CURRENT_PYTHON)))
sys.exit(1)
def read(f): def read(f):
return open(f, 'r', encoding='utf-8').read() return open(f, 'r', encoding='utf-8').read()
@ -52,7 +79,7 @@ setup(
packages=find_packages(exclude=['tests*']), packages=find_packages(exclude=['tests*']),
include_package_data=True, include_package_data=True,
install_requires=[], install_requires=[],
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", python_requires=">=3.5",
zip_safe=False, zip_safe=False,
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
@ -66,13 +93,11 @@ setup(
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP',
] ]
) )

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,6 +1,3 @@
# coding: utf-8
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models

View File

@ -1,7 +1,3 @@
# coding: utf-8
from __future__ import unicode_literals
import base64 import base64
import pytest import pytest
@ -10,7 +6,6 @@ from django.conf.urls import include, url
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponse from django.http import HttpResponse
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils import six
from rest_framework import ( from rest_framework import (
HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status
@ -188,7 +183,7 @@ class SessionAuthTests(TestCase):
cf. [#1810](https://github.com/encode/django-rest-framework/pull/1810) cf. [#1810](https://github.com/encode/django-rest-framework/pull/1810)
""" """
response = self.csrf_client.get('/auth/login/') response = self.csrf_client.get('/auth/login/')
content = response.content.decode('utf8') content = response.content.decode()
assert '<label for="id_username">Username:</label>' in content assert '<label for="id_username">Username:</label>' in content
def test_post_form_session_auth_failing_csrf(self): def test_post_form_session_auth_failing_csrf(self):
@ -253,7 +248,7 @@ class SessionAuthTests(TestCase):
assert response.status_code == status.HTTP_403_FORBIDDEN assert response.status_code == status.HTTP_403_FORBIDDEN
class BaseTokenAuthTests(object): class BaseTokenAuthTests:
"""Token authentication""" """Token authentication"""
model = None model = None
path = None path = None
@ -381,7 +376,7 @@ class TokenAuthTests(BaseTokenAuthTests, TestCase):
"""Ensure generate_key returns a string""" """Ensure generate_key returns a string"""
token = self.model() token = self.model()
key = token.generate_key() key = token.generate_key()
assert isinstance(key, six.string_types) assert isinstance(key, str)
def test_token_login_json(self): def test_token_login_json(self):
"""Ensure token login view using JSON POST works.""" """Ensure token login view using JSON POST works."""
@ -534,7 +529,7 @@ class BasicAuthenticationUnitTests(TestCase):
def test_basic_authentication_raises_error_if_user_not_active(self): def test_basic_authentication_raises_error_if_user_not_active(self):
from rest_framework import authentication from rest_framework import authentication
class MockUser(object): class MockUser:
is_active = False is_active = False
old_authenticate = authentication.authenticate old_authenticate = authentication.authenticate
authentication.authenticate = lambda **kwargs: MockUser() authentication.authenticate = lambda **kwargs: MockUser()

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.conf.urls import include, url from django.conf.urls import include, url
from .views import MockView from .views import MockView

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .views import MockView from .views import MockView

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
@ -26,18 +24,18 @@ class DropdownWithAuthTests(TestCase):
def test_name_shown_when_logged_in(self): def test_name_shown_when_logged_in(self):
self.client.login(username=self.username, password=self.password) self.client.login(username=self.username, password=self.password)
response = self.client.get('/') response = self.client.get('/')
content = response.content.decode('utf8') content = response.content.decode()
assert 'john' in content assert 'john' in content
def test_logout_shown_when_logged_in(self): def test_logout_shown_when_logged_in(self):
self.client.login(username=self.username, password=self.password) self.client.login(username=self.username, password=self.password)
response = self.client.get('/') response = self.client.get('/')
content = response.content.decode('utf8') content = response.content.decode()
assert '>Log out<' in content assert '>Log out<' in content
def test_login_shown_when_logged_out(self): def test_login_shown_when_logged_out(self):
response = self.client.get('/') response = self.client.get('/')
content = response.content.decode('utf8') content = response.content.decode()
assert '>Log in<' in content assert '>Log in<' in content
@ -61,16 +59,16 @@ class NoDropdownWithoutAuthTests(TestCase):
def test_name_shown_when_logged_in(self): def test_name_shown_when_logged_in(self):
self.client.login(username=self.username, password=self.password) self.client.login(username=self.username, password=self.password)
response = self.client.get('/') response = self.client.get('/')
content = response.content.decode('utf8') content = response.content.decode()
assert 'john' in content assert 'john' in content
def test_dropdown_not_shown_when_logged_in(self): def test_dropdown_not_shown_when_logged_in(self):
self.client.login(username=self.username, password=self.password) self.client.login(username=self.username, password=self.password)
response = self.client.get('/') response = self.client.get('/')
content = response.content.decode('utf8') content = response.content.decode()
assert '<li class="dropdown">' not in content assert '<li class="dropdown">' not in content
def test_dropdown_not_shown_when_logged_out(self): def test_dropdown_not_shown_when_logged_out(self):
response = self.client.get('/') response = self.client.get('/')
content = response.content.decode('utf8') content = response.content.decode()
assert '<li class="dropdown">' not in content assert '<li class="dropdown">' not in content

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
@ -36,7 +34,7 @@ class DropdownWithAuthTests(TestCase):
def test_login(self): def test_login(self):
response = self.client.get('/api/') response = self.client.get('/api/')
assert 200 == response.status_code assert 200 == response.status_code
content = response.content.decode('utf-8') content = response.content.decode()
assert 'form action="/api/"' in content assert 'form action="/api/"' in content
assert 'input name="nested.one"' in content assert 'input name="nested.one"' in content
assert 'input name="nested.two"' in content assert 'input name="nested.two"' in content

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from rest_framework import authentication, renderers from rest_framework import authentication, renderers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView

View File

@ -1,14 +1,10 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import ( from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation GenericForeignKey, GenericRelation
) )
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class Tag(models.Model): class Tag(models.Model):
""" """
Tags have a descriptive slug, and are attached to an arbitrary object. Tags have a descriptive slug, and are attached to an arbitrary object.
@ -22,7 +18,6 @@ class Tag(models.Model):
return self.tag return self.tag
@python_2_unicode_compatible
class Bookmark(models.Model): class Bookmark(models.Model):
""" """
A URL bookmark that may have multiple tags attached. A URL bookmark that may have multiple tags attached.
@ -34,7 +29,6 @@ class Bookmark(models.Model):
return 'Bookmark: %s' % self.url return 'Bookmark: %s' % self.url
@python_2_unicode_compatible
class Note(models.Model): class Note(models.Model):
""" """
A textual note that may have multiple tags attached. A textual note that may have multiple tags attached.

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers

View File

@ -1 +1,16 @@
from rest_framework import compat # noqa """
This test "app" exists to ensure that parts of Django REST Framework can be
imported/invoked before Django itself has been fully initialized.
"""
from rest_framework import compat, serializers # noqa
# test initializing fields with lazy translations
class ExampleSerializer(serializers.Serializer):
charfield = serializers.CharField(min_length=1, max_length=2)
integerfield = serializers.IntegerField(min_value=1, max_value=2)
floatfield = serializers.FloatField(min_value=1, max_value=2)
decimalfield = serializers.DecimalField(max_digits=10, decimal_places=1, min_value=1, max_value=2)
durationfield = serializers.DurationField(min_value=1, max_value=2)
listfield = serializers.ListField(min_length=1, max_length=2)

View File

@ -4,10 +4,21 @@ from tests import importable
def test_installed(): def test_installed():
# ensure that apps can freely import rest_framework.compat # ensure the test app hasn't been removed from the test suite
assert 'tests.importable' in settings.INSTALLED_APPS assert 'tests.importable' in settings.INSTALLED_APPS
def test_imported(): def test_compat():
# ensure that the __init__ hasn't been mucked with
assert hasattr(importable, 'compat') assert hasattr(importable, 'compat')
def test_serializer_fields_initialization():
assert hasattr(importable, 'ExampleSerializer')
serializer = importable.ExampleSerializer()
assert 'charfield' in serializer.fields
assert 'integerfield' in serializer.fields
assert 'floatfield' in serializer.fields
assert 'decimalfield' in serializer.fields
assert 'durationfield' in serializer.fields
assert 'listfield' in serializer.fields

View File

@ -1,9 +1,7 @@
from __future__ import unicode_literals
import uuid import uuid
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class RESTFrameworkModel(models.Model): class RESTFrameworkModel(models.Model):

Some files were not shown because too many files have changed in this diff Show More