Merge branch 'master' into sanitize-searchfield-input

This commit is contained in:
Tom Christie 2019-07-01 13:54:42 +01:00 committed by GitHub
commit b520e0a059
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
192 changed files with 3221 additions and 2159 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

@ -24,10 +24,10 @@ The initial aim is to provide a single full-time position on REST framework.
[![][rollbar-img]][rollbar-url] [![][rollbar-img]][rollbar-url]
[![][cadre-img]][cadre-url] [![][cadre-img]][cadre-url]
[![][kloudless-img]][kloudless-url] [![][kloudless-img]][kloudless-url]
[![][release-history-img]][release-history-url] [![][esg-img]][esg-url]
[![][lightson-img]][lightson-url] [![][lightson-img]][lightson-url]
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Kloudless][kloudless-url], [Release History][release-history-url], and [Lights On Software][lightson-url]. Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Kloudless][kloudless-url], [ESG][esg-url], and [Lights On Software][lightson-url].
--- ---
@ -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,17 +197,15 @@ 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.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
[rover-url]: http://jobs.rover.com/
[sentry-url]: https://getsentry.com/welcome/ [sentry-url]: https://getsentry.com/welcome/
[stream-url]: https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf [stream-url]: https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf
[rollbar-url]: https://rollbar.com/ [rollbar-url]: https://rollbar.com/
[cadre-url]: https://cadre.com/ [cadre-url]: https://cadre.com/
[load-impact-url]: https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf
[kloudless-url]: https://hubs.ly/H0f30Lf0 [kloudless-url]: https://hubs.ly/H0f30Lf0
[release-history-url]: https://releasehistory.io [esg-url]: https://software.esg-usa.com/
[lightson-url]: https://lightsonsoftware.com [lightson-url]: https://lightsonsoftware.com
[oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth [oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
@ -225,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

@ -1,4 +1,7 @@
source: authentication.py ---
source:
- authentication.py
---
# Authentication # Authentication
@ -327,7 +330,7 @@ If the `.authenticate_header()` method is not overridden, the authentication sch
## Example ## Example
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'. The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X-USERNAME'.
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import authentication from rest_framework import authentication
@ -335,7 +338,7 @@ The following example will authenticate any incoming request as the user given b
class ExampleAuthentication(authentication.BaseAuthentication): class ExampleAuthentication(authentication.BaseAuthentication):
def authenticate(self, request): def authenticate(self, request):
username = request.META.get('X_USERNAME') username = request.META.get('HTTP_X_USERNAME')
if not username: if not username:
return None return None
@ -354,7 +357,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

@ -1,6 +1,6 @@
# Caching # Caching
> A certain woman had a very sharp conciousness but almost no > A certain woman had a very sharp consciousness but almost no
> memory ... She remembered enough to work, and she worked hard. > memory ... She remembered enough to work, and she worked hard.
> - Lydia Davis > - Lydia Davis

View File

@ -1,4 +1,7 @@
source: negotiation.py ---
source:
- negotiation.py
---
# Content negotiation # Content negotiation

View File

@ -1,4 +1,7 @@
source: exceptions.py ---
source:
- exceptions.py
---
# Exceptions # Exceptions

View File

@ -1,4 +1,7 @@
source: fields.py ---
source:
- fields.py
---
# Serializer fields # Serializer fields
@ -209,7 +212,7 @@ A field that ensures the input is a valid UUID string. The `to_internal_value` m
**Signature:** `UUIDField(format='hex_verbose')` **Signature:** `UUIDField(format='hex_verbose')`
- `format`: Determines the representation format of the uuid value - `format`: Determines the representation format of the uuid value
- `'hex_verbose'` - The cannoncical hex representation, including hyphens: `"5ce0e9a5-5ffa-654b-cee0-1238041fb31a"` - `'hex_verbose'` - The canonical hex representation, including hyphens: `"5ce0e9a5-5ffa-654b-cee0-1238041fb31a"`
- `'hex'` - The compact hex representation of the UUID, not including hyphens: `"5ce0e9a55ffa654bcee01238041fb31a"` - `'hex'` - The compact hex representation of the UUID, not including hyphens: `"5ce0e9a55ffa654bcee01238041fb31a"`
- `'int'` - A 128 bit integer representation of the UUID: `"123456789012312313134124512351145145114"` - `'int'` - A 128 bit integer representation of the UUID: `"123456789012312313134124512351145145114"`
- `'urn'` - RFC 4122 URN representation of the UUID: `"urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a"` - `'urn'` - RFC 4122 URN representation of the UUID: `"urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a"`
@ -448,9 +451,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.
@ -471,9 +475,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:
@ -488,9 +493,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.
@ -498,9 +504,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`.
--- ---
@ -629,7 +636,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__)
@ -653,7 +660,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):

View File

@ -1,4 +1,7 @@
source: filters.py ---
source:
- filters.py
---
# Filtering # Filtering
@ -221,7 +224,7 @@ By default, the search parameter is named `'search`', but this may be overridden
To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request: To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request:
from rest_framework import filters from rest_framework import filters
class CustomSearchFilter(filters.SearchFilter): class CustomSearchFilter(filters.SearchFilter):
def get_search_fields(self, view, request): def get_search_fields(self, view, request):
if request.query_params.get('title_only'): if request.query_params.get('title_only'):
@ -291,53 +294,6 @@ The `ordering` attribute may be either a string or a list/tuple of strings.
--- ---
## DjangoObjectPermissionsFilter
The `DjangoObjectPermissionsFilter` is intended to be used together with the [`django-guardian`][guardian] package, with custom `'view'` permissions added. The filter will ensure that querysets only returns objects for which the user has the appropriate view permission.
---
**Note:** This filter has been deprecated as of version 3.9 and moved to the 3rd-party [`djangorestframework-guardian` package][django-rest-framework-guardian].
---
If you're using `DjangoObjectPermissionsFilter`, you'll probably also want to add an appropriate object permissions class, to ensure that users can only operate on instances if they have the appropriate object permissions. The easiest way to do this is to subclass `DjangoObjectPermissions` and add `'view'` permissions to the `perms_map` attribute.
A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectPermissions` might look something like this.
**permissions.py**:
class CustomObjectPermissions(permissions.DjangoObjectPermissions):
"""
Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
"""
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
**views.py**:
class EventViewSet(viewsets.ModelViewSet):
"""
Viewset that only lists events if user has 'view' permissions, and only
allows operations on individual events if user has appropriate 'view', 'add',
'change' or 'delete' permissions.
"""
queryset = Event.objects.all()
serializer_class = EventSerializer
filter_backends = (filters.DjangoObjectPermissionsFilter,)
permission_classes = (myapp.permissions.CustomObjectPermissions,)
For more information on adding `'view'` permissions for models, see the [relevant section][view-permissions] of the `django-guardian` documentation, and [this blogpost][view-permissions-blogpost].
---
# Custom generic filtering # Custom generic filtering
You can also provide your own generic filtering backend, or write an installable app for other developers to use. You can also provide your own generic filtering backend, or write an installable app for other developers to use.
@ -399,12 +355,8 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter]
[cite]: https://docs.djangoproject.com/en/stable/topics/db/queries/#retrieving-specific-objects-with-filters [cite]: https://docs.djangoproject.com/en/stable/topics/db/queries/#retrieving-specific-objects-with-filters
[django-filter-docs]: https://django-filter.readthedocs.io/en/latest/index.html [django-filter-docs]: https://django-filter.readthedocs.io/en/latest/index.html
[django-filter-drf-docs]: https://django-filter.readthedocs.io/en/latest/guide/rest_framework.html [django-filter-drf-docs]: https://django-filter.readthedocs.io/en/latest/guide/rest_framework.html
[guardian]: https://django-guardian.readthedocs.io/
[view-permissions]: https://django-guardian.readthedocs.io/en/latest/userguide/assign.html
[view-permissions-blogpost]: https://blog.nyaruka.com/adding-a-view-permission-to-django-models
[search-django-admin]: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields [search-django-admin]: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
[django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters [django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
[django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter
[django-url-filter]: https://github.com/miki725/django-url-filter [django-url-filter]: https://github.com/miki725/django-url-filter
[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters [drf-url-filter]: https://github.com/manjitkumar/drf-url-filters

View File

@ -1,4 +1,7 @@
source: urlpatterns.py ---
source:
- urlpatterns.py
---
# Format suffixes # Format suffixes

View File

@ -1,5 +1,8 @@
source: mixins.py ---
generics.py source:
- mixins.py
- generics.py
---
# Generic views # Generic views

View File

@ -1,4 +1,7 @@
source: metadata.py ---
source:
- metadata.py
---
# Metadata # Metadata

View File

@ -1,4 +1,7 @@
source: parsers.py ---
source:
- parsers.py
---
# Parsers # Parsers

View File

@ -1,4 +1,7 @@
source: permissions.py ---
source:
- permissions.py
---
# Permissions # Permissions
@ -281,6 +284,10 @@ Also note that the generic views will only check the object-level permissions fo
The following third party packages are also available. The following third party packages are also available.
## DRF - Access Policy
The [Django REST - Access Policy][drf-access-policy] package provides a way to define complex access rules in declaritive policy classes that are attached to view sets or function-based views. The policies are defined in JSON in a format similar to AWS' Identity & Access Management policies.
## Composed Permissions ## Composed Permissions
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components. The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
@ -299,7 +306,7 @@ The [Django Rest Framework Roles][django-rest-framework-roles] package makes it
## Django REST Framework API Key ## Django REST Framework API Key
The [Django REST Framework API Key][djangorestframework-api-key] package provides the ability to authorize clients based on customizable API key headers. This package is targeted at situations in which regular user-based authentication (e.g. `TokenAuthentication`) is not suitable, e.g. allowing non-human clients to safely use your API. API keys are generated and validated through cryptographic methods and can be created and revoked from the Django admin interface at anytime. The [Django REST Framework API Key][djangorestframework-api-key] package provides permissions classes, models and helpers to add API key authorization to your API. It can be used to authorize internal or third-party backends and services (i.e. _machines_) which do not have a user account. API keys are stored securely using Django's password hashing infrastructure, and they can be viewed, edited and revoked at anytime in the Django admin.
## Django Rest Framework Role Filters ## Django Rest Framework Role Filters
@ -317,6 +324,7 @@ The [Django Rest Framework Role Filters][django-rest-framework-role-filters] pac
[rest-condition]: https://github.com/caxap/rest_condition [rest-condition]: https://github.com/caxap/rest_condition
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions [dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles [django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
[djangorestframework-api-key]: https://github.com/florimondmanca/djangorestframework-api-key [djangorestframework-api-key]: https://florimondmanca.github.io/djangorestframework-api-key/
[django-rest-framework-role-filters]: https://github.com/allisson/django-rest-framework-role-filters [django-rest-framework-role-filters]: https://github.com/allisson/django-rest-framework-role-filters
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian [django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
[drf-access-policy]: https://github.com/rsinger86/drf-access-policy

View File

@ -1,4 +1,7 @@
source: relations.py ---
source:
- relations.py
---
# Serializer relations # Serializer relations
@ -576,6 +579,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 +601,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

@ -1,4 +1,7 @@
source: renderers.py ---
source:
- renderers.py
---
# Renderers # Renderers
@ -534,7 +537,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

@ -1,4 +1,7 @@
source: request.py ---
source:
- request.py
---
# Requests # Requests

View File

@ -1,4 +1,7 @@
source: response.py ---
source:
- response.py
---
# Responses # Responses

View File

@ -1,4 +1,7 @@
source: reverse.py ---
source:
- reverse.py
---
# Returning URLs # Returning URLs

View File

@ -1,4 +1,7 @@
source: routers.py ---
source:
- routers.py
---
# Routers # Routers

View File

@ -1,4 +1,7 @@
source: schemas.py ---
source:
- schemas.py
---
# Schemas # Schemas

View File

@ -1,4 +1,7 @@
source: serializers.py ---
source:
- serializers.py
---
# Serializers # Serializers
@ -572,6 +575,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 +629,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 +968,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

@ -1,4 +1,7 @@
source: settings.py ---
source:
- settings.py
---
# Settings # Settings
@ -404,7 +407,7 @@ This should be a function with the following signature:
If the view instance inherits `ViewSet`, it may have been initialized with several optional arguments: If the view instance inherits `ViewSet`, it may have been initialized with several optional arguments:
* `name`: A name expliticly provided to a view in the viewset. Typically, this value should be used as-is when provided. * `name`: A name explicitly provided to a view in the viewset. Typically, this value should be used as-is when provided.
* `suffix`: Text used when differentiating individual views in a viewset. This argument is mutually exclusive to `name`. * `suffix`: Text used when differentiating individual views in a viewset. This argument is mutually exclusive to `name`.
* `detail`: Boolean that differentiates an individual view in a viewset as either being a 'list' or 'detail' view. * `detail`: Boolean that differentiates an individual view in a viewset as either being a 'list' or 'detail' view.

View File

@ -1,4 +1,7 @@
source: status.py ---
source:
- status.py
---
# Status Codes # Status Codes
@ -20,13 +23,13 @@ The full set of HTTP status codes included in the `status` module is listed belo
The module also includes a set of helper functions for testing if a status code is in a given range. The module also includes a set of helper functions for testing if a status code is in a given range.
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
class ExampleTestCase(APITestCase): class ExampleTestCase(APITestCase):
def test_url_root(self): def test_url_root(self):
url = reverse('index') url = reverse('index')
response = self.client.get(url) response = self.client.get(url)
self.assertTrue(status.is_success(response.status_code)) self.assertTrue(status.is_success(response.status_code))
For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616] For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616]
@ -51,6 +54,8 @@ This class of status code indicates that the client's request was successfully r
HTTP_205_RESET_CONTENT HTTP_205_RESET_CONTENT
HTTP_206_PARTIAL_CONTENT HTTP_206_PARTIAL_CONTENT
HTTP_207_MULTI_STATUS HTTP_207_MULTI_STATUS
HTTP_208_ALREADY_REPORTED
HTTP_226_IM_USED
## Redirection - 3xx ## Redirection - 3xx
@ -64,6 +69,7 @@ This class of status code indicates that further action needs to be taken by the
HTTP_305_USE_PROXY HTTP_305_USE_PROXY
HTTP_306_RESERVED HTTP_306_RESERVED
HTTP_307_TEMPORARY_REDIRECT HTTP_307_TEMPORARY_REDIRECT
HTTP_308_PERMANENT_REDIRECT
## Client Error - 4xx ## Client Error - 4xx
@ -90,6 +96,7 @@ The 4xx class of status code is intended for cases in which the client seems to
HTTP_422_UNPROCESSABLE_ENTITY HTTP_422_UNPROCESSABLE_ENTITY
HTTP_423_LOCKED HTTP_423_LOCKED
HTTP_424_FAILED_DEPENDENCY HTTP_424_FAILED_DEPENDENCY
HTTP_426_UPGRADE_REQUIRED
HTTP_428_PRECONDITION_REQUIRED HTTP_428_PRECONDITION_REQUIRED
HTTP_429_TOO_MANY_REQUESTS HTTP_429_TOO_MANY_REQUESTS
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE
@ -105,7 +112,11 @@ Response status codes beginning with the digit "5" indicate cases in which the s
HTTP_503_SERVICE_UNAVAILABLE HTTP_503_SERVICE_UNAVAILABLE
HTTP_504_GATEWAY_TIMEOUT HTTP_504_GATEWAY_TIMEOUT
HTTP_505_HTTP_VERSION_NOT_SUPPORTED HTTP_505_HTTP_VERSION_NOT_SUPPORTED
HTTP_506_VARIANT_ALSO_NEGOTIATES
HTTP_507_INSUFFICIENT_STORAGE HTTP_507_INSUFFICIENT_STORAGE
HTTP_508_LOOP_DETECTED
HTTP_509_BANDWIDTH_LIMIT_EXCEEDED
HTTP_510_NOT_EXTENDED
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
## Helper functions ## Helper functions

View File

@ -1,4 +1,7 @@
source: test.py ---
source:
- test.py
---
# Testing # Testing

View File

@ -1,4 +1,7 @@
source: throttling.py ---
source:
- throttling.py
---
# Throttling # Throttling
@ -74,7 +77,7 @@ If you need to strictly identify unique client IP addresses, you'll need to firs
It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](https://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](https://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client.
Further context on how the `X-Forwarded-For` header works, and identifying a remote client IP can be [found here][identifing-clients]. Further context on how the `X-Forwarded-For` header works, and identifying a remote client IP can be [found here][identifying-clients].
## Setting up the cache ## Setting up the cache
@ -194,6 +197,6 @@ The following is an example of a rate throttle, that will randomly throttle 1 in
[cite]: https://developer.twitter.com/en/docs/basics/rate-limiting [cite]: https://developer.twitter.com/en/docs/basics/rate-limiting
[permissions]: permissions.md [permissions]: permissions.md
[identifing-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster [identifying-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster
[cache-setting]: https://docs.djangoproject.com/en/stable/ref/settings/#caches [cache-setting]: https://docs.djangoproject.com/en/stable/ref/settings/#caches
[cache-docs]: https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache [cache-docs]: https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache

View File

@ -1,4 +1,7 @@
source: validators.py ---
source:
- validators.py
---
# Validators # Validators
@ -100,7 +103,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 +162,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

@ -1,4 +1,7 @@
source: versioning.py ---
source:
- versioning.py
---
# Versioning # Versioning

View File

@ -1,5 +1,8 @@
source: decorators.py ---
views.py source:
- decorators.py
- views.py
---
# Class-based Views # Class-based Views

View File

@ -1,4 +1,7 @@
source: viewsets.py ---
source:
- viewsets.py
---
# ViewSets # ViewSets

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

@ -60,7 +60,7 @@ REST framework's new API documentation supports a number of features:
* Support for various authentication schemes. * Support for various authentication schemes.
* Code snippets for the Python, JavaScript, and Command Line clients. * Code snippets for the Python, JavaScript, and Command Line clients.
The `coreapi` library is required as a dependancy 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 (2.3.0 or above). The `pygments` and `markdown` to install the latest version (2.3.0 or above). The `pygments` and `markdown`
libraries are optional but recommended. libraries are optional but recommended.

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,38 @@ You can determine your currently installed version using `pip show`:
--- ---
## 3.10.x series
### 3.10.0
**Date**: [Unreleased][3.10.0-milestone]
* Updated PyYaml dependency for OpenAPI schema generation to `pyyaml>=5.1` [#6680][gh6680]
* Resolve DeprecationWarning with markdown. [#6317][gh6317]
* Add `generateschema --generator_class` CLI option
## 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]
@ -294,7 +321,7 @@ You can determine your currently installed version using `pip show`:
Note: `AutoSchema.__init__` now ensures `manual_fields` is a list. Note: `AutoSchema.__init__` now ensures `manual_fields` is a list.
Previously may have been stored internally as `None`. Previously may have been stored internally as `None`.
* Remove ulrparse compatability shim; use six instead [#5579][gh5579] * Remove ulrparse compatibility shim; use six instead [#5579][gh5579]
* Drop compat wrapper for `TimeDelta.total_seconds()` [#5577][gh5577] * Drop compat wrapper for `TimeDelta.total_seconds()` [#5577][gh5577]
* Clean up all whitespace throughout project [#5578][gh5578] * Clean up all whitespace throughout project [#5578][gh5578]
* Compat cleanup [#5581][gh5581] * Compat cleanup [#5581][gh5581]
@ -1165,7 +1192,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 +2134,10 @@ 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 -->
[gh6680]: https://github.com/encode/django-rest-framework/issues/6680
[gh6317]: https://github.com/encode/django-rest-framework/issues/6317

View File

@ -20,7 +20,7 @@ If you have an idea for a new feature please consider how it may be packaged as
You can use [this cookiecutter template][cookiecutter] for creating reusable Django REST Framework packages quickly. Cookiecutter creates projects from project templates. While optional, this cookiecutter template includes best practices from Django REST framework and other packages, as well as a Travis CI configuration, Tox configuration, and a sane setup.py for easy PyPI registration/distribution. You can use [this cookiecutter template][cookiecutter] for creating reusable Django REST Framework packages quickly. Cookiecutter creates projects from project templates. While optional, this cookiecutter template includes best practices from Django REST framework and other packages, as well as a Travis CI configuration, Tox configuration, and a sane setup.py for easy PyPI registration/distribution.
Note: Let us know if you have an alternate cookiecuter package so we can also link to it. Note: Let us know if you have an alternate cookiecutter package so we can also link to it.
#### Running the initial cookiecutter command #### Running the initial cookiecutter command
@ -197,6 +197,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [djangorestframework-composed-permissions][djangorestframework-composed-permissions] - Provides a simple way to define complex permissions. * [djangorestframework-composed-permissions][djangorestframework-composed-permissions] - Provides a simple way to define complex permissions.
* [rest_condition][rest-condition] - Another extension for building complex permissions in a simple and convenient way. * [rest_condition][rest-condition] - Another extension for building complex permissions in a simple and convenient way.
* [dry-rest-permissions][dry-rest-permissions] - Provides a simple way to define permissions for individual api actions. * [dry-rest-permissions][dry-rest-permissions] - Provides a simple way to define permissions for individual api actions.
* [drf-access-policy][drf-access-policy] - Declarative and flexible permissions inspired by AWS' IAM policies.
### Serializers ### Serializers
@ -208,6 +209,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-rest-framework-serializer-extensions][drf-serializer-extensions] - * [django-rest-framework-serializer-extensions][drf-serializer-extensions] -
Enables black/whitelisting fields, and conditionally expanding child serializers on a per-view/request basis. Enables black/whitelisting fields, and conditionally expanding child serializers on a per-view/request basis.
* [djangorestframework-queryfields][djangorestframework-queryfields] - Serializer mixin allowing clients to control which fields will be sent in the API response. * [djangorestframework-queryfields][djangorestframework-queryfields] - Serializer mixin allowing clients to control which fields will be sent in the API response.
* [drf-flex-fields][drf-flex-fields] - Serializer providing dynamic field expansion and sparse field sets via URL parameters.
### Serializer fields ### Serializer fields
@ -244,6 +246,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [djangorestframework-chain][djangorestframework-chain] - Allows arbitrary chaining of both relations and lookup filters. * [djangorestframework-chain][djangorestframework-chain] - Allows arbitrary chaining of both relations and lookup filters.
* [django-url-filter][django-url-filter] - Allows a safe way to filter data via human-friendly URLs. It is a generic library which is not tied to DRF but it provides easy integration with DRF. * [django-url-filter][django-url-filter] - Allows a safe way to filter data via human-friendly URLs. It is a generic library which is not tied to DRF but it provides easy integration with DRF.
* [drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. * [drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values.
* [django-rest-framework-guardian][django-rest-framework-guardian] - Provides integration with django-guardian, including the `DjangoObjectPermissionsFilter` previously found in DRF.
### Misc ### Misc
@ -264,6 +267,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [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-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
* [djangorestframework-mvt][djangorestframework-mvt] - An extension for creating views that serve Postgres data as Map Box Vector Tiles.
[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
@ -338,3 +343,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[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-framework-condition]: https://github.com/jozo/django-rest-framework-condition
[django-rest-witchcraft]: https://github.com/shosca/django-rest-witchcraft
[drf-access-policy]: https://github.com/rsinger86/drf-access-policy
[drf-flex-fields]: https://github.com/rsinger86/drf-flex-fields
[djangorestframework-mvt]: https://github.com/corteva/djangorestframework-mvt
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -68,7 +68,7 @@ continued development by **[signing up for a paid plan][funding]**.
<ul class="premium-promo promo"> <ul class="premium-promo promo">
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li> <li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li> <li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://releasehistory.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/release-history.png)">Release History</a></li> <li><a href="https://software.esg-usa.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/esg-new-logo.png)">ESG</a></li>
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li> <li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
<li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li> <li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li>
<li><a href="https://hubs.ly/H0f30Lf0" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/kloudless-plus-text.png)">Kloudless</a></li> <li><a href="https://hubs.ly/H0f30Lf0" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/kloudless-plus-text.png)">Kloudless</a></li>
@ -76,7 +76,7 @@ continued development by **[signing up for a paid plan][funding]**.
</ul> </ul>
<div style="clear: both; padding-bottom: 20px;"></div> <div style="clear: both; padding-bottom: 20px;"></div>
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Release History](https://releasehistory.io), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).* *Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [ESG](https://software.esg-usa.com/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).*
--- ---
@ -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

@ -14,18 +14,18 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
## 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
@ -372,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

@ -4,13 +4,15 @@ site_description: Django REST framework - Web APIs for Django
repo_url: https://github.com/encode/django-rest-framework repo_url: https://github.com/encode/django-rest-framework
theme_dir: docs_theme theme:
name: mkdocs
custom_dir: docs_theme
markdown_extensions: markdown_extensions:
- toc: - toc:
anchorlink: True anchorlink: True
pages: nav:
- Home: 'index.md' - Home: 'index.md'
- Tutorial: - Tutorial:
- 'Quickstart': 'tutorial/quickstart.md' - 'Quickstart': 'tutorial/quickstart.md'

View File

@ -1,2 +1,2 @@
# MkDocs to build our documentation. # MkDocs to build our documentation.
mkdocs==0.16.3 mkdocs==1.0.4

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>=5.1

View File

@ -1,4 +1,4 @@
# Pytest for running the tests. # Pytest for running the tests.
pytest==4.3.0 pytest>=5.0,<5.1
pytest-django==3.4.8 pytest-django>=3.5.1,<3.6
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

@ -7,6 +7,7 @@ class TokenAdmin(admin.ModelAdmin):
list_display = ('key', 'user', 'created') list_display = ('key', 'user', 'created')
fields = ('user',) fields = ('user',)
ordering = ('-created',) ordering = ('-created',)
autocomplete_fields = ('user',)
admin.site.register(Token, TokenAdmin) admin.site.register(Token, TokenAdmin)

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

@ -3,18 +3,13 @@ The most important decorator in this module is `@api_view`, which is used
for writing function-based views with REST framework. for writing function-based views with REST framework.
There are also various decorators for setting the API policies on function 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 `@action` decorator, which is used to annotate
used to annotate methods on viewsets that should be included by routers. 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,35 @@ class empty:
pass pass
if six.PY3: class BuiltinSignatureError(Exception):
def is_simple_callable(obj): """
""" Built-in function signatures are not inspectable. This exception is raised
True if the object is a callable that takes no arguments. so the serializer can raise a helpful error message.
""" """
if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)): pass
return False
sig = inspect.signature(obj)
params = sig.parameters.values()
return all(
param.kind == param.VAR_POSITIONAL or
param.kind == param.VAR_KEYWORD or
param.default != param.empty
for param in params
)
else: def is_simple_callable(obj):
def is_simple_callable(obj): """
function = inspect.isfunction(obj) True if the object is a callable that takes no arguments.
method = inspect.ismethod(obj) """
# Bail early since we cannot inspect built-in function signatures.
if inspect.isbuiltin(obj):
raise BuiltinSignatureError(
'Built-in function signatures are not inspectable. '
'Wrap the function call in a simple, pure Python function.')
if not (function or method): if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)):
return False return False
if method: sig = inspect.signature(obj)
is_unbound = obj.im_self is None params = sig.parameters.values()
return all(
args, _, _, defaults = inspect.getargspec(obj) param.kind == param.VAR_POSITIONAL or
param.kind == param.VAR_KEYWORD or
len_args = len(args) if function or is_unbound else len(args) - 1 param.default != param.empty
len_defaults = len(defaults) if defaults else 0 for param in params
return len_args <= len_defaults )
def get_attribute(instance, attrs): def get_attribute(instance, attrs):
@ -108,7 +100,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 +177,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
@ -239,19 +231,19 @@ def get_error_detail(exc_info):
error_dict = exc_info.error_dict error_dict = exc_info.error_dict
except AttributeError: except AttributeError:
return [ return [
ErrorDetail(error.message % (error.params or ()), ErrorDetail((error.message % error.params) if error.params else error.message,
code=error.code if error.code else code) code=error.code if error.code else code)
for error in exc_info.error_list] for error in exc_info.error_list]
return { return {
k: [ k: [
ErrorDetail(error.message % (error.params or ()), ErrorDetail((error.message % error.params) if error.params else error.message,
code=error.code if error.code else code) code=error.code if error.code else code)
for error in errors for error in errors
] for k, errors in error_dict.items() ] for k, errors in error_dict.items()
} }
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 +265,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 +276,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 +295,7 @@ MISSING_ERROR_MESSAGE = (
) )
class Field(object): class Field:
_creation_counter = 0 _creation_counter = 0
default_error_messages = { default_error_messages = {
@ -451,6 +441,18 @@ class Field(object):
""" """
try: try:
return get_attribute(instance, self.source_attrs) return get_attribute(instance, self.source_attrs)
except BuiltinSignatureError as exc:
msg = (
'Field source for `{serializer}.{field}` maps to a built-in '
'function type and is invalid. Define a property or method on '
'the `{instance}` instance that wraps the call to the built-in '
'function.'.format(
serializer=self.parent.__class__.__name__,
field=self.field_name,
instance=instance.__class__.__name__,
)
)
raise type(exc)(msg)
except (KeyError, AttributeError) as exc: except (KeyError, AttributeError) as exc:
if self.default is not empty: if self.default is not empty:
return self.get_default() return self.get_default()
@ -515,6 +517,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 +625,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 +643,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 +654,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 +731,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 +771,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 +789,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 +814,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 +825,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 +837,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 +852,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 +869,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 +903,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 +918,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 +936,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 +972,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 +1026,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 +1112,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 +1121,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 +1158,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 +1217,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 +1242,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 +1279,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 +1308,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 +1342,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 +1369,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 +1407,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 +1444,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 +1460,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 +1473,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 +1485,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 +1503,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 +1522,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 +1568,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 +1584,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 +1617,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 +1647,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 +1678,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 +1692,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 +1710,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 +1726,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 +1742,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 +1756,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 +1775,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 +1807,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 +1824,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,25 +1854,19 @@ 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
# 'method_name' argument has been used. For example: # 'method_name' argument has been used. For example:
# my_field = serializer.SerializerMethodField(method_name='get_my_field') # my_field = serializer.SerializerMethodField(method_name='get_my_field')
default_method_name = 'get_{field_name}'.format(field_name=field_name) default_method_name = 'get_{field_name}'.format(field_name=field_name)
assert self.method_name != default_method_name, (
"It is redundant to specify `%s` on SerializerMethodField '%s' in "
"serializer '%s', because it is the same as the default method name. "
"Remove the `method_name` argument." %
(self.method_name, field_name, parent.__class__.__name__)
)
# The method name should default to `get_{field_name}`. # The method name should default to `get_{field_name}`.
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 +1889,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 import forms from django import forms
@ -14,14 +11,10 @@ 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
@ -37,7 +30,7 @@ class SearchFilterForm(forms.Form):
self.fields[search_field] = forms.CharField() self.fields[search_field] = forms.CharField()
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.
""" """
@ -53,6 +46,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.
@ -127,7 +123,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
] ]
@ -177,6 +173,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.
@ -206,7 +215,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
@ -255,7 +264,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
] ]
@ -308,40 +317,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,63 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.module_loading import import_string
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)
parser.add_argument('--urlconf', dest="urlconf", default=None, type=str)
parser.add_argument('--generator_class', dest="generator_class", default=None, type=str)
def handle(self, *args, **options): def handle(self, *args, **options):
assert coreapi is not None, 'coreapi must be installed.' if options['generator_class']:
generator_class = import_string(options['generator_class'])
generator = SchemaGenerator( else:
generator_class = self.get_generator_class()
generator = generator_class(
url=options['url'], url=options['url'],
title=options['title'], title=options['title'],
description=options['description'] description=options['description'],
urlconf=options['urlconf'],
) )
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, sort_keys=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 = {
@ -132,7 +54,7 @@ def endpoint_ordering(endpoint):
'PATCH': 3, 'PATCH': 3,
'DELETE': 4 'DELETE': 4
}.get(method, 5) }.get(method, 5)
return (path, method_priority) return (method_priority,)
_PATH_PARAMETER_COMPONENT_RE = re.compile( _PATH_PARAMETER_COMPONENT_RE = re.compile(
@ -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,53 +193,7 @@ 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:
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
# Methods used when we generate a view instance from the raw callback...
def determine_path_prefix(self, paths):
"""
Given a list of all paths, return the common prefix which should be
discounted when generating a schema structure.
This will be the longest common string that does not include that last
component of the URL, or the last component before a path parameter.
For example:
/api/v1/users/
/api/v1/users/{pk}/
The path prefix is '/api/v1/'
"""
prefixes = []
for path in paths:
components = path.strip('/').split('/')
initial_components = []
for component in components:
if '{' in component:
break
initial_components.append(component)
prefix = '/'.join(initial_components[:-1])
if not prefix:
# We can just break early in the case that there's at least
# one URL that doesn't have a path prefix.
return '/'
prefixes.append('/' + prefix + '/')
return common_path(prefixes)
def create_view(self, callback, method, request=None): def create_view(self, callback, method, request=None):
""" """
@ -379,19 +218,6 @@ class SchemaGenerator(object):
return view return view
def has_view_permissions(self, path, method, view):
"""
Return `True` if the incoming request has the correct view permissions.
"""
if view.request is None:
return True
try:
view.check_permissions(view.request)
except (exceptions.APIException, Http404, PermissionDenied):
return False
return True
def coerce_path(self, path, method, view): def coerce_path(self, path, method, view):
""" """
Coerce {pk} path arguments into the name of the model field, Coerce {pk} path arguments into the name of the model field,
@ -407,48 +233,49 @@ class SchemaGenerator(object):
field_name = 'id' field_name = 'id'
return path.replace('{pk}', '{%s}' % field_name) return path.replace('{pk}', '{%s}' % field_name)
# Method for generating the link layout.... def get_schema(self, request=None, public=False):
raise NotImplementedError(".get_schema() must be implemented in subclasses.")
def get_keys(self, subpath, method, view): def determine_path_prefix(self, paths):
""" """
Return a list of keys that should be used to layout a link within Given a list of all paths, return the common prefix which should be
the schema document. discounted when generating a schema structure.
/users/ ("users", "list"), ("users", "create") This will be the longest common string that does not include that last
/users/{pk}/ ("users", "read"), ("users", "update"), ("users", "delete") component of the URL, or the last component before a path parameter.
/users/enabled/ ("users", "enabled") # custom viewset list action
/users/{pk}/star/ ("users", "star") # custom viewset detail action For example:
/users/{pk}/groups/ ("users", "groups", "list"), ("users", "groups", "create")
/users/{pk}/groups/{pk}/ ("users", "groups", "read"), ("users", "groups", "update"), ("users", "groups", "delete") /api/v1/users/
/api/v1/users/{pk}/
The path prefix is '/api/v1'
""" """
if hasattr(view, 'action'): prefixes = []
# Viewsets have explicitly named actions. for path in paths:
action = view.action components = path.strip('/').split('/')
else: initial_components = []
# Views have no associated action, so we determine one from the method. for component in components:
if is_list_view(subpath, method, view): if '{' in component:
action = 'list' break
else: initial_components.append(component)
action = self.default_mapping[method.lower()] prefix = '/'.join(initial_components[:-1])
if not prefix:
# We can just break early in the case that there's at least
# one URL that doesn't have a path prefix.
return '/'
prefixes.append('/' + prefix + '/')
return common_path(prefixes)
named_path_components = [ def has_view_permissions(self, path, method, view):
component for component """
in subpath.strip('/').split('/') Return `True` if the incoming request has the correct view permissions.
if '{' not in component """
] if view.request is None:
return True
if is_custom_action(action): try:
# Custom action, eg "/users/{pk}/activate/", "/users/active/" view.check_permissions(view.request)
if len(view.action_map) > 1: except (exceptions.APIException, Http404, PermissionDenied):
action = self.default_mapping[method.lower()] return False
if action in self.coerce_method_names: return True
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,131 +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):
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)
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.
@ -179,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,480 @@
import warnings
from django.core.validators import (
DecimalValidator, EmailValidator, MaxLengthValidator, MaxValueValidator,
MinLengthValidator, MinValueValidator, RegexValidator, URLValidator
)
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 rest_framework.fields import empty
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)
if prefix == '/': # no prefix
prefix = ''
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',
}
# "Formats such as "email", "uuid", and so on, MAY be used even though undefined by this specification."
# see: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types
# see also: https://swagger.io/docs/specification/data-models/data-types/#string
if isinstance(field, serializers.EmailField):
return {
'type': 'string',
'format': 'email'
}
if isinstance(field, serializers.URLField):
return {
'type': 'string',
'format': 'uri'
}
if isinstance(field, serializers.UUIDField):
return {
'type': 'string',
'format': 'uuid'
}
if isinstance(field, serializers.IPAddressField):
content = {
'type': 'string',
}
if field.protocol != 'both':
content['format'] = field.protocol
return content
# DecimalField has multipleOf based on decimal_places
if isinstance(field, serializers.DecimalField):
content = {
'type': 'number'
}
if field.decimal_places:
content['multipleOf'] = float('.' + (field.decimal_places - 1) * '0' + '1')
if field.max_whole_digits:
content['maximum'] = int(field.max_whole_digits * '9') + 1
content['minimum'] = -content['maximum']
self._map_min_max(field, content)
return content
if isinstance(field, serializers.FloatField):
content = {
'type': 'number'
}
self._map_min_max(field, content)
return content
if isinstance(field, serializers.IntegerField):
content = {
'type': 'integer'
}
self._map_min_max(field, content)
return content
# Simplest cases, default to 'string' type:
FIELD_CLASS_SCHEMA_TYPE = {
serializers.BooleanField: 'boolean',
serializers.JSONField: 'object',
serializers.DictField: 'object',
}
return {'type': FIELD_CLASS_SCHEMA_TYPE.get(field.__class__, 'string')}
def _map_min_max(self, field, content):
if field.max_value:
content['maximum'] = field.max_value
if field.min_value:
content['minimum'] = field.min_value
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
if field.default and field.default != empty: # why don't they use None?!
schema['default'] = field.default
if field.help_text:
schema['description'] = field.help_text
self._map_field_validators(field.validators, schema)
properties[field.field_name] = schema
return {
'required': required,
'properties': properties,
}
def _map_field_validators(self, validators, schema):
"""
map field validators
:param list:validators: list of field validators
:param dict:schema: schema that the validators get added to
"""
for v in validators:
# "Formats such as "email", "uuid", and so on, MAY be used even though undefined by this specification."
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types
if isinstance(v, EmailValidator):
schema['format'] = 'email'
if isinstance(v, URLValidator):
schema['format'] = 'uri'
if isinstance(v, RegexValidator):
schema['pattern'] = v.regex.pattern
elif isinstance(v, MaxLengthValidator):
schema['maxLength'] = v.limit_value
elif isinstance(v, MinLengthValidator):
schema['minLength'] = v.limit_value
elif isinstance(v, MaxValueValidator):
schema['maximum'] = v.limit_value
elif isinstance(v, MinValueValidator):
schema['minimum'] = v.limit_value
elif isinstance(v, DecimalValidator):
if v.decimal_places:
schema['multipleOf'] = float('.' + (v.decimal_places - 1) * '0' + '1')
if v.max_digits:
digits = v.max_digits
if v.decimal_places is not None and v.decimal_places > 0:
digits -= v.decimal_places
schema['maximum'] = int(digits * '9') + 1
schema['minimum'] = -schema['maximum']
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,13 +587,9 @@ 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):
super(ListSerializer, self).bind(field_name, parent)
self.partial = self.parent.partial
def get_initial(self): def get_initial(self):
if hasattr(self, 'initial_data'): if hasattr(self, 'initial_data'):
return self.to_representation(self.initial_data) return self.to_representation(self.initial_data)
@ -649,9 +641,6 @@ class ListSerializer(BaseSerializer):
}, code='not_a_list') }, code='not_a_list')
if not self.allow_empty and len(data) == 0: if not self.allow_empty and len(data) == 0:
if self.parent and self.partial:
raise SkipField()
message = self.error_messages['empty'] message = self.error_messages['empty']
raise ValidationError({ raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message] api_settings.NON_FIELD_ERRORS_KEY: [message]
@ -758,19 +747,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.
@ -977,14 +966,22 @@ class ModelSerializer(Serializer):
# Note that unlike `.create()` we don't need to treat many-to-many # Note that unlike `.create()` we don't need to treat many-to-many
# relationships as being a special case. During updates we already # relationships as being a special case. During updates we already
# have an instance pk for the relationships to be associated with. # have an instance pk for the relationships to be associated with.
m2m_fields = []
for attr, value in validated_data.items(): for attr, value in validated_data.items():
if attr in info.relations and info.relations[attr].to_many: if attr in info.relations and info.relations[attr].to_many:
field = getattr(instance, attr) m2m_fields.append((attr, value))
field.set(value)
else: else:
setattr(instance, attr, value) setattr(instance, attr, value)
instance.save() instance.save()
# Note that many-to-many fields are set after updating instance.
# Setting m2m fields triggers signals which could potentialy change
# updated instance and we do not want it to collide with .update()
for attr, value in m2m_fields:
field = getattr(instance, attr)
field.set(value)
return instance return instance
# Determine the fields to apply... # Determine the fields to apply...
@ -1236,6 +1233,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:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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