mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-29 01:20:02 +03:00
Merge branch 'master' into sponsor-update
This commit is contained in:
commit
78c37f3b00
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
custom: https://fund.django-rest-framework.org/topics/funding/
|
|
@ -4,10 +4,6 @@ dist: xenial
|
|||
matrix:
|
||||
fast_finish: true
|
||||
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=2.0 }
|
||||
|
@ -26,8 +22,8 @@ matrix:
|
|||
- { python: "3.7", env: DJANGO=master }
|
||||
|
||||
- { python: "3.7", env: TOXENV=base }
|
||||
- { python: "2.7", env: TOXENV=lint }
|
||||
- { python: "2.7", env: TOXENV=docs }
|
||||
- { python: "3.7", env: TOXENV=lint }
|
||||
- { python: "3.7", env: TOXENV=docs }
|
||||
|
||||
- python: "3.7"
|
||||
env: TOXENV=dist
|
||||
|
|
1
CHANGELOG.md
Symbolic link
1
CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
docs/community/release-notes.md
|
|
@ -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:
|
||||
|
||||
# Setup the virtual environment
|
||||
virtualenv env
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip install django
|
||||
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].
|
||||
|
||||
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.
|
||||
|
||||
|
|
10
README.md
10
README.md
|
@ -53,7 +53,7 @@ There is a live example API for testing purposes, [available here][sandbox].
|
|||
|
||||
# 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)
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
Please see the [security policy][security-policy].
|
||||
|
||||
[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
|
||||
|
@ -199,7 +197,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
[cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png
|
||||
[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
|
||||
[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history-readme.png
|
||||
[esg-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/esg-readme.png
|
||||
[lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png
|
||||
|
||||
[sentry-url]: https://getsentry.com/welcome/
|
||||
|
@ -223,4 +221,4 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
[image]: https://www.django-rest-framework.org/img/quickstart.png
|
||||
|
||||
[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
9
SECURITY.md
Normal 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
|
|
@ -354,7 +354,7 @@ The following third party packages are also available.
|
|||
|
||||
## 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
|
||||
|
||||
|
|
|
@ -306,10 +306,11 @@ A date and time representation.
|
|||
|
||||
Corresponds to `django.db.models.fields.DateTimeField`.
|
||||
|
||||
**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)`
|
||||
**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None, default_timezone=None)`
|
||||
|
||||
* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer.
|
||||
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||
* `default_timezone` - A `pytz.timezone` representing the timezone. If not specified and the `USE_TZ` setting is enabled, this defaults to the [current timezone][django-current-timezone]. If `USE_TZ` is disabled, then datetime objects will be naive.
|
||||
|
||||
#### `DateTimeField` format strings.
|
||||
|
||||
|
@ -447,9 +448,10 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is
|
|||
|
||||
A field class that validates a list of objects.
|
||||
|
||||
**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.
|
||||
- `allow_empty` - Designates if empty lists are allowed.
|
||||
- `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.
|
||||
|
||||
|
@ -470,9 +472,10 @@ We can now reuse our custom `StringListField` class throughout our application,
|
|||
|
||||
A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.
|
||||
|
||||
**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.
|
||||
- `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:
|
||||
|
||||
|
@ -487,9 +490,10 @@ You can also use the declarative style, as with `ListField`. For example:
|
|||
|
||||
A preconfigured `DictField` that is compatible with Django's postgres `HStoreField`.
|
||||
|
||||
**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.
|
||||
- `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.
|
||||
|
||||
|
@ -497,9 +501,10 @@ Note that the child field **must** be an instance of `CharField`, as the hstore
|
|||
|
||||
A field class that validates that the incoming data structure consists of valid JSON primitives. In its alternate binary mode, it will represent and validate JSON-encoded binary strings.
|
||||
|
||||
**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`.
|
||||
- `encoder` - Use this JSON encoder to serialize input object. Defaults to `None`.
|
||||
|
||||
---
|
||||
|
||||
|
@ -628,7 +633,7 @@ Our `ColorField` class above currently does not perform any data validation.
|
|||
To indicate invalid data, we should raise a `serializers.ValidationError`, like so:
|
||||
|
||||
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'
|
||||
raise ValidationError(msg % type(data).__name__)
|
||||
|
||||
|
@ -652,7 +657,7 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me
|
|||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, six.text_type):
|
||||
if not isinstance(data, str):
|
||||
self.fail('incorrect_type', input_type=type(data).__name__)
|
||||
|
||||
if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data):
|
||||
|
@ -835,3 +840,4 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide
|
|||
[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
|
||||
[django-hstore]: https://github.com/djangonauts/django-hstore
|
||||
[python-decimal-rounding-modes]: https://docs.python.org/3/library/decimal.html#rounding-modes
|
||||
[django-current-timezone]: https://docs.djangoproject.com/en/stable/topics/i18n/timezones/#default-time-zone-and-current-time-zone
|
||||
|
|
|
@ -51,7 +51,7 @@ For example:
|
|||
---
|
||||
|
||||
**Note**: With the exception of `DjangoObjectPermissions`, the provided
|
||||
permission classes in `rest_framework.permssions` **do not** implement the
|
||||
permission classes in `rest_framework.permissions` **do not** implement the
|
||||
methods necessary to check object permissions.
|
||||
|
||||
If you wish to use the provided permission classes in order to check object
|
||||
|
|
|
@ -576,6 +576,8 @@ If you explicitly specify a relational field pointing to a
|
|||
``ManyToManyField`` with a through model, be sure to set ``read_only``
|
||||
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
|
||||
|
@ -596,3 +598,5 @@ The [rest-framework-generic-relations][drf-nested-relations] library provides re
|
|||
[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1
|
||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||
[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
|
||||
|
|
|
@ -534,7 +534,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
|||
[messagepack]: https://msgpack.org/
|
||||
[juanriaza]: https://github.com/juanriaza
|
||||
[mjumbewu]: https://github.com/mjumbewu
|
||||
[flipperpa]: https://githuc.com/flipperpa
|
||||
[flipperpa]: https://github.com/flipperpa
|
||||
[wharton]: https://github.com/wharton
|
||||
[drf-renderer-xlsx]: https://github.com/wharton/drf-renderer-xlsx
|
||||
[vbabiy]: https://github.com/vbabiy
|
||||
|
|
|
@ -20,7 +20,7 @@ can render the schema into the commonly used YAML-based OpenAPI format.
|
|||
|
||||
## Quickstart
|
||||
|
||||
There are two different ways you can serve a schema description for you API.
|
||||
There are two different ways you can serve a schema description for your API.
|
||||
|
||||
### Generating a schema with the `generateschema` management command
|
||||
|
||||
|
|
|
@ -572,6 +572,8 @@ This option is a dictionary, mapping field names to a dictionary of keyword argu
|
|||
user.save()
|
||||
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
|
||||
|
||||
When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for `ModelSerializer` is to use the primary keys of the related instances.
|
||||
|
@ -624,7 +626,7 @@ The default implementation returns a serializer class based on the `serializer_f
|
|||
|
||||
Called to generate a serializer field that maps to a relational model field.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -963,7 +965,7 @@ The following class is an example of a generic serializer that can handle coerci
|
|||
def to_representation(self, obj):
|
||||
for attribute_name in dir(obj):
|
||||
attribute = getattr(obj, attribute_name)
|
||||
if attribute_name('_'):
|
||||
if attribute_name.startswith('_'):
|
||||
# Ignore private attributes.
|
||||
pass
|
||||
elif hasattr(attribute, '__call__'):
|
||||
|
|
|
@ -100,7 +100,7 @@ The validator should be applied to *serializer classes*, like so:
|
|||
|
||||
---
|
||||
|
||||
**Note**: The `UniqueTogetherValidation` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
|
||||
**Note**: The `UniqueTogetherValidator` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
|
||||
|
||||
---
|
||||
|
||||
|
@ -159,7 +159,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden
|
|||
|
||||
---
|
||||
|
||||
**Note**: The `UniqueFor<Range>Validation` classes impose an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
|
||||
**Note**: The `UniqueFor<Range>Validator` classes impose an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -523,7 +523,7 @@ The following class is an example of a generic serializer that can handle coerci
|
|||
def to_representation(self, obj):
|
||||
for attribute_name in dir(obj):
|
||||
attribute = getattr(obj, attribute_name)
|
||||
if attribute_name('_'):
|
||||
if attribute_name.startswith('_'):
|
||||
# Ignore private attributes.
|
||||
pass
|
||||
elif hasattr(attribute, '__call__'):
|
||||
|
|
|
@ -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:
|
||||
|
||||
# Setup the virtual environment
|
||||
virtualenv env
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
pip install django
|
||||
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].
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -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://djangogigs.com][django-gigs-com]
|
||||
* [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://stackoverflow.com/jobs/developer-jobs-using-django][stackoverflow-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/
|
||||
[django-gigs-com]: https://djangogigs.com
|
||||
[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
|
||||
[stackoverflow-com]: https://stackoverflow.com/jobs/developer-jobs-using-django
|
||||
[upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/
|
||||
|
|
|
@ -38,11 +38,36 @@ You can determine your currently installed version using `pip show`:
|
|||
|
||||
---
|
||||
|
||||
## 3.10.x series
|
||||
|
||||
### 3.10.0
|
||||
|
||||
**Date**: [Unreleased][3.10.0-milestone]
|
||||
|
||||
* Resolve DeprecationWarning with markdown. [#6317][gh6317]
|
||||
|
||||
|
||||
## 3.9.x series
|
||||
|
||||
### 3.9.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
|
||||
|
||||
**Date**: [3rd March 2019][3.9.1-milestone]
|
||||
**Date**: [3rd March 2019][3.9.2-milestone]
|
||||
|
||||
* Routers: invalidate `_urls` cache on `register()` [#6407][gh6407]
|
||||
* Deferred schema renderer creation to avoid requiring pyyaml. [#6416][gh6416]
|
||||
|
@ -62,7 +87,7 @@ You can determine your currently installed version using `pip show`:
|
|||
|
||||
### 3.9.1
|
||||
|
||||
**Date**: [16th Janurary 2019][3.9.1-milestone]
|
||||
**Date**: [16th January 2019][3.9.1-milestone]
|
||||
|
||||
* Resolve XSS issue in browsable API. [#6330][gh6330]
|
||||
* Upgrade Bootstrap to 3.4.0 to resolve XSS issue.
|
||||
|
@ -1165,7 +1190,8 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1
|
||||
[3.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/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 -->
|
||||
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013
|
||||
|
@ -2106,3 +2132,9 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[gh6340]: https://github.com/encode/django-rest-framework/issues/6340
|
||||
[gh6416]: https://github.com/encode/django-rest-framework/issues/6416
|
||||
[gh6407]: https://github.com/encode/django-rest-framework/issues/6407
|
||||
|
||||
<!-- 3.9.3 -->
|
||||
[gh6613]: https://github.com/encode/django-rest-framework/issues/6613
|
||||
|
||||
<!-- 3.10.0 -->
|
||||
[gh6317]: https://github.com/encode/django-rest-framework/issues/6317
|
||||
|
|
|
@ -263,6 +263,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM.
|
||||
* [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework.
|
||||
* [djangorestframework-datatables][djangorestframework-datatables] - Seamless integration between Django REST framework and [Datatables](https://datatables.net).
|
||||
* [django-rest-framework-condition][django-rest-framework-condition] - Decorators for managing HTTP cache headers for Django REST framework (ETag and Last-modified).
|
||||
* [django-rest-witchcraft][django-rest-witchcraft] - Provides DRF integration with SQLAlchemy with SQLAlchemy model serializers/viewsets and a bunch of other goodies
|
||||
|
||||
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
|
||||
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
|
||||
|
@ -336,3 +338,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
||||
[djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy
|
||||
[djangorestframework-datatables]: https://github.com/izimobil/django-rest-framework-datatables
|
||||
[django-rest-framework-condition]: https://github.com/jozo/django-rest-framework-condition
|
||||
[django-rest-witchcraft]: https://github.com/shosca/django-rest-witchcraft
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 567 KiB |
BIN
docs/img/premium/release-history.png
Normal file
BIN
docs/img/premium/release-history.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -84,7 +84,7 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
|
||||
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)
|
||||
|
||||
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:
|
||||
|
||||
* [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-crispy-forms][django-crispy-forms] - Improved HTML display for filtering.
|
||||
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
|
||||
|
||||
## Installation
|
||||
|
@ -238,8 +238,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[eventbrite]: https://www.eventbrite.co.uk/about/
|
||||
[coreapi]: https://pypi.org/project/coreapi/
|
||||
[markdown]: https://pypi.org/project/Markdown/
|
||||
[pygments]: https://pypi.org/project/Pygments/
|
||||
[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
|
||||
[index]: .
|
||||
[oauth1-section]: api-guide/authentication/#django-rest-framework-oauth
|
||||
|
|
|
@ -17,7 +17,7 @@ The built-in API documentation includes:
|
|||
### Installation
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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]
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -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
|
||||
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
|
||||
[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
|
||||
[django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger
|
||||
[swagger]: https://swagger.io/
|
||||
[open-api]: https://openapis.org/
|
||||
[rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
|
||||
[apiary]: https://apiary.io/
|
||||
[markdown]: https://daringfireball.net/projects/markdown/
|
||||
[markdown]: https://daringfireball.net/projects/markdown/syntax
|
||||
[hypermedia-docs]: rest-hypermedia-hateoas.md
|
||||
[image-drf-docs]: ../img/drfdocs.png
|
||||
[image-django-rest-swagger]: ../img/django-rest-swagger.png
|
||||
[image-apiary]: ../img/apiary.png
|
||||
[image-self-describing-api]: ../img/self-describing.png
|
||||
|
|
|
@ -8,24 +8,24 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
|
|||
|
||||
---
|
||||
|
||||
**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
|
||||
**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
|
||||
|
||||
---
|
||||
|
||||
## Setting up a new environment
|
||||
|
||||
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
|
||||
|
||||
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 djangorestframework
|
||||
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
|
||||
|
||||
|
@ -218,7 +218,6 @@ Edit the `snippets/views.py` file, and add the following.
|
|||
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.parsers import JSONParser
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
|
@ -373,7 +372,7 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
|
|||
[quickstart]: quickstart.md
|
||||
[repo]: https://github.com/encode/rest-framework-tutorial
|
||||
[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
|
||||
[httpie]: https://github.com/jakubroztocil/httpie#installation
|
||||
[curl]: https://curl.haxx.se/
|
||||
|
|
|
@ -10,11 +10,11 @@ Create a new Django project named `tutorial`, then start a new app called `quick
|
|||
mkdir tutorial
|
||||
cd tutorial
|
||||
|
||||
# Create a virtualenv to isolate our package dependencies locally
|
||||
virtualenv env
|
||||
# Create a virtual environment to isolate our package dependencies locally
|
||||
python3 -m venv env
|
||||
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 djangorestframework
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
<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/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 src="{{ base_url }}/mkdocs/js/require.js"></script>
|
||||
<script src="{{ base_url }}/js/theme.js"></script>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# Optional packages which may be used with REST framework.
|
||||
psycopg2-binary==2.7.5
|
||||
markdown==2.6.11
|
||||
psycopg2-binary>=2.8.2, <2.9
|
||||
markdown==3.1.1
|
||||
pygments==2.4.2
|
||||
django-guardian==1.5.0
|
||||
django-filter==1.1.0
|
||||
django-filter>=2.1.0, <2.2
|
||||
coreapi==2.3.1
|
||||
coreschema==0.0.4
|
||||
pyyaml
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Pytest for running the tests.
|
||||
pytest==4.3.0
|
||||
pytest-django==3.4.8
|
||||
pytest-cov==2.6.1
|
||||
pytest>=4.5.0,<4.6
|
||||
pytest-django>=3.4.8,<3.5
|
||||
pytest-cov>=2.7.1
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '3.9.2'
|
||||
__version__ = '3.9.3'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'
|
||||
|
@ -25,9 +25,9 @@ ISO_8601 = 'iso-8601'
|
|||
default_app_config = 'rest_framework.apps.RestFrameworkConfig'
|
||||
|
||||
|
||||
class RemovedInDRF310Warning(DeprecationWarning):
|
||||
class RemovedInDRF311Warning(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
class RemovedInDRF311Warning(PendingDeprecationWarning):
|
||||
class RemovedInDRF312Warning(PendingDeprecationWarning):
|
||||
pass
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
"""
|
||||
Provides various authentication policies.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from django.contrib.auth import authenticate, get_user_model
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
from django.utils.six import text_type
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
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.
|
||||
"""
|
||||
auth = request.META.get('HTTP_AUTHORIZATION', b'')
|
||||
if isinstance(auth, text_type):
|
||||
if isinstance(auth, str):
|
||||
# Work around django test client oddness
|
||||
auth = auth.encode(HTTP_HEADER_ENCODING)
|
||||
return auth
|
||||
|
@ -33,7 +30,7 @@ class CSRFCheck(CsrfViewMiddleware):
|
|||
return reason
|
||||
|
||||
|
||||
class BaseAuthentication(object):
|
||||
class BaseAuthentication:
|
||||
"""
|
||||
All authentication classes should extend BaseAuthentication.
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AuthTokenConfig(AppConfig):
|
||||
|
|
|
@ -38,8 +38,8 @@ class Command(BaseCommand):
|
|||
token = self.create_user_token(username, reset_token)
|
||||
except UserModel.DoesNotExist:
|
||||
raise CommandError(
|
||||
'Cannot create the Token: user {0} does not exist'.format(
|
||||
'Cannot create the Token: user {} does not exist'.format(
|
||||
username)
|
||||
)
|
||||
self.stdout.write(
|
||||
'Generated token {0} for user {1}'.format(token.key, username))
|
||||
'Generated token {} for user {}'.format(token.key, username))
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -3,11 +3,9 @@ import os
|
|||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Token(models.Model):
|
||||
"""
|
||||
The default authorization token model.
|
||||
|
@ -32,7 +30,7 @@ class Token(models.Model):
|
|||
def save(self, *args, **kwargs):
|
||||
if not self.key:
|
||||
self.key = self.generate_key()
|
||||
return super(Token, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def generate_key(self):
|
||||
return binascii.hexlify(os.urandom(20)).decode()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
|
|
|
@ -2,23 +2,11 @@
|
|||
The `compat` module provides support for backwards compatibility with older
|
||||
versions of Django/Python, and compatibility wrappers around optional packages.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
from django.utils import six
|
||||
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:
|
||||
from django.urls import ( # noqa
|
||||
URLPattern,
|
||||
|
@ -36,11 +24,6 @@ try:
|
|||
except ImportError:
|
||||
ProhibitNullCharactersValidator = None
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
mock = None
|
||||
|
||||
|
||||
def get_original_route(urlpattern):
|
||||
"""
|
||||
|
@ -89,23 +72,6 @@ def make_url_resolver(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):
|
||||
# Coerce HTTP header value to unicode.
|
||||
if isinstance(value, bytes):
|
||||
|
@ -150,13 +116,6 @@ except ImportError:
|
|||
yaml = None
|
||||
|
||||
|
||||
# django-crispy-forms is optional
|
||||
try:
|
||||
import crispy_forms
|
||||
except ImportError:
|
||||
crispy_forms = None
|
||||
|
||||
|
||||
# requests is optional
|
||||
try:
|
||||
import requests
|
||||
|
@ -164,35 +123,17 @@ except ImportError:
|
|||
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
|
||||
if 'patch' not in View.http_method_names:
|
||||
View.http_method_names = View.http_method_names + ['patch']
|
||||
|
||||
|
||||
# Markdown is optional
|
||||
# Markdown is optional (version 3.0+ required)
|
||||
try:
|
||||
import markdown
|
||||
|
||||
if markdown.version <= '2.2':
|
||||
HEADERID_EXT_PATH = 'headerid'
|
||||
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'
|
||||
HEADERID_EXT_PATH = 'markdown.extensions.toc'
|
||||
LEVEL_PARAM = 'baselevel'
|
||||
|
||||
def apply_markdown(text):
|
||||
"""
|
||||
|
@ -265,7 +206,7 @@ if markdown is not None and pygments is not None:
|
|||
return ret.split("\n")
|
||||
|
||||
def md_filter_add_syntax_highlight(md):
|
||||
md.preprocessors.add('highlight', CodeBlockPreprocessor(), "_begin")
|
||||
md.preprocessors.register(CodeBlockPreprocessor(), 'highlight', 40)
|
||||
return True
|
||||
else:
|
||||
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
|
||||
# See: https://bugs.python.org/issue22767
|
||||
if six.PY3:
|
||||
SHORT_SEPARATORS = (',', ':')
|
||||
LONG_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
|
||||
SHORT_SEPARATORS = (',', ':')
|
||||
LONG_SEPARATORS = (', ', ': ')
|
||||
INDENT_SEPARATORS = (',', ': ')
|
||||
|
||||
|
||||
# Version Constants.
|
||||
|
|
|
@ -6,15 +6,10 @@ There are also various decorators for setting the API policies on function
|
|||
based views, as well as the `@detail_route` and `@list_route` decorators, which are
|
||||
used to annotate methods on viewsets that should be included by routers.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import types
|
||||
import warnings
|
||||
|
||||
from django.forms.utils import pretty_name
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import RemovedInDRF310Warning
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
|
@ -28,7 +23,7 @@ def api_view(http_method_names=None):
|
|||
def decorator(func):
|
||||
|
||||
WrappedAPIView = type(
|
||||
six.PY3 and 'WrappedAPIView' or b'WrappedAPIView',
|
||||
'WrappedAPIView',
|
||||
(APIView,),
|
||||
{'__doc__': func.__doc__}
|
||||
)
|
||||
|
@ -217,39 +212,3 @@ class MethodMapper(dict):
|
|||
|
||||
def trace(self, 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
|
||||
|
|
|
@ -4,18 +4,14 @@ Handled exceptions raised by REST framework.
|
|||
In addition Django's built in 403 and 404 exceptions are handled.
|
||||
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import math
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.compat import unicode_to_repr
|
||||
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.
|
||||
"""
|
||||
code = None
|
||||
|
||||
def __new__(cls, string, code=None):
|
||||
self = super(ErrorDetail, cls).__new__(cls, string)
|
||||
self = super().__new__(cls, string)
|
||||
self.code = code
|
||||
return self
|
||||
|
||||
def __eq__(self, other):
|
||||
r = super(ErrorDetail, self).__eq__(other)
|
||||
r = super().__eq__(other)
|
||||
try:
|
||||
return r and self.code == other.code
|
||||
except AttributeError:
|
||||
|
@ -86,10 +82,10 @@ class ErrorDetail(six.text_type):
|
|||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('ErrorDetail(string=%r, code=%r)' % (
|
||||
six.text_type(self),
|
||||
return 'ErrorDetail(string=%r, code=%r)' % (
|
||||
str(self),
|
||||
self.code,
|
||||
))
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(str(self))
|
||||
|
@ -113,7 +109,7 @@ class APIException(Exception):
|
|||
self.detail = _get_error_details(detail, code)
|
||||
|
||||
def __str__(self):
|
||||
return six.text_type(self.detail)
|
||||
return str(self.detail)
|
||||
|
||||
def get_codes(self):
|
||||
"""
|
||||
|
@ -196,7 +192,7 @@ class MethodNotAllowed(APIException):
|
|||
def __init__(self, method, detail=None, code=None):
|
||||
if detail is None:
|
||||
detail = force_text(self.default_detail).format(method=method)
|
||||
super(MethodNotAllowed, self).__init__(detail, code)
|
||||
super().__init__(detail, code)
|
||||
|
||||
|
||||
class NotAcceptable(APIException):
|
||||
|
@ -206,7 +202,7 @@ class NotAcceptable(APIException):
|
|||
|
||||
def __init__(self, detail=None, code=None, available_renderers=None):
|
||||
self.available_renderers = available_renderers
|
||||
super(NotAcceptable, self).__init__(detail, code)
|
||||
super().__init__(detail, code)
|
||||
|
||||
|
||||
class UnsupportedMediaType(APIException):
|
||||
|
@ -217,7 +213,7 @@ class UnsupportedMediaType(APIException):
|
|||
def __init__(self, media_type, detail=None, code=None):
|
||||
if detail is None:
|
||||
detail = force_text(self.default_detail).format(media_type=media_type)
|
||||
super(UnsupportedMediaType, self).__init__(detail, code)
|
||||
super().__init__(detail, code)
|
||||
|
||||
|
||||
class Throttled(APIException):
|
||||
|
@ -234,11 +230,11 @@ class Throttled(APIException):
|
|||
wait = math.ceil(wait)
|
||||
detail = ' '.join((
|
||||
detail,
|
||||
force_text(ungettext(self.extra_detail_singular.format(wait=wait),
|
||||
self.extra_detail_plural.format(wait=wait),
|
||||
wait))))
|
||||
force_text(ngettext(self.extra_detail_singular.format(wait=wait),
|
||||
self.extra_detail_plural.format(wait=wait),
|
||||
wait))))
|
||||
self.wait = wait
|
||||
super(Throttled, self).__init__(detail, code)
|
||||
super().__init__(detail, code)
|
||||
|
||||
|
||||
def server_error(request, *args, **kwargs):
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import decimal
|
||||
|
@ -8,37 +6,35 @@ import inspect
|
|||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
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 ImageField as DjangoImageField
|
||||
from django.utils import six, timezone
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import (
|
||||
parse_date, parse_datetime, parse_duration, parse_time
|
||||
)
|
||||
from django.utils.duration import duration_string
|
||||
from django.utils.encoding import is_protected_type, smart_text
|
||||
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.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 rest_framework import ISO_8601
|
||||
from rest_framework.compat import (
|
||||
Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
||||
MinValueValidator, ProhibitNullCharactersValidator, unicode_repr,
|
||||
unicode_to_repr
|
||||
)
|
||||
from rest_framework.compat import ProhibitNullCharactersValidator
|
||||
from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import html, humanize_datetime, json, representation
|
||||
from rest_framework.utils.formatting import lazy_format
|
||||
|
||||
|
||||
class empty:
|
||||
|
@ -51,39 +47,21 @@ class empty:
|
|||
pass
|
||||
|
||||
|
||||
if six.PY3:
|
||||
def is_simple_callable(obj):
|
||||
"""
|
||||
True if the object is a callable that takes no arguments.
|
||||
"""
|
||||
if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)):
|
||||
return False
|
||||
def is_simple_callable(obj):
|
||||
"""
|
||||
True if the object is a callable that takes no arguments.
|
||||
"""
|
||||
if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)):
|
||||
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):
|
||||
function = inspect.isfunction(obj)
|
||||
method = inspect.ismethod(obj)
|
||||
|
||||
if not (function or method):
|
||||
return False
|
||||
|
||||
if method:
|
||||
is_unbound = obj.im_self is None
|
||||
|
||||
args, _, _, defaults = inspect.getargspec(obj)
|
||||
|
||||
len_args = len(args) if function or is_unbound else len(args) - 1
|
||||
len_defaults = len(defaults) if defaults else 0
|
||||
return len_args <= len_defaults
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
def get_attribute(instance, attrs):
|
||||
|
@ -108,7 +86,7 @@ def get_attribute(instance, attrs):
|
|||
# If we raised an Attribute or KeyError here it'd get treated
|
||||
# as an omitted field in `Field.get_attribute()`. Instead we
|
||||
# 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
|
||||
|
||||
|
@ -185,18 +163,18 @@ def iter_options(grouped_choices, cutoff=None, cutoff_text=None):
|
|||
"""
|
||||
Helper function for options and option groups in templates.
|
||||
"""
|
||||
class StartOptionGroup(object):
|
||||
class StartOptionGroup:
|
||||
start_option_group = True
|
||||
end_option_group = False
|
||||
|
||||
def __init__(self, label):
|
||||
self.label = label
|
||||
|
||||
class EndOptionGroup(object):
|
||||
class EndOptionGroup:
|
||||
start_option_group = False
|
||||
end_option_group = True
|
||||
|
||||
class Option(object):
|
||||
class Option:
|
||||
start_option_group = False
|
||||
end_option_group = False
|
||||
|
||||
|
@ -251,7 +229,7 @@ def get_error_detail(exc_info):
|
|||
}
|
||||
|
||||
|
||||
class CreateOnlyDefault(object):
|
||||
class CreateOnlyDefault:
|
||||
"""
|
||||
This class may be used to provide default values that are only used
|
||||
for create operations, but that do not return any value for update
|
||||
|
@ -273,12 +251,10 @@ class CreateOnlyDefault(object):
|
|||
return self.default
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr(
|
||||
'%s(%s)' % (self.__class__.__name__, unicode_repr(self.default))
|
||||
)
|
||||
return '%s(%s)' % (self.__class__.__name__, repr(self.default))
|
||||
|
||||
|
||||
class CurrentUserDefault(object):
|
||||
class CurrentUserDefault:
|
||||
def set_context(self, serializer_field):
|
||||
self.user = serializer_field.context['request'].user
|
||||
|
||||
|
@ -286,7 +262,7 @@ class CurrentUserDefault(object):
|
|||
return self.user
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('%s()' % self.__class__.__name__)
|
||||
return '%s()' % self.__class__.__name__
|
||||
|
||||
|
||||
class SkipField(Exception):
|
||||
|
@ -305,7 +281,7 @@ MISSING_ERROR_MESSAGE = (
|
|||
)
|
||||
|
||||
|
||||
class Field(object):
|
||||
class Field:
|
||||
_creation_counter = 0
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -515,6 +491,11 @@ class Field(object):
|
|||
if data is None:
|
||||
if not self.allow_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 (False, data)
|
||||
|
@ -618,7 +599,7 @@ class Field(object):
|
|||
When a field is instantiated, we store the arguments that were used,
|
||||
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._kwargs = kwargs
|
||||
return instance
|
||||
|
@ -636,7 +617,7 @@ class Field(object):
|
|||
for item in self._args
|
||||
]
|
||||
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()
|
||||
}
|
||||
return self.__class__(*args, **kwargs)
|
||||
|
@ -647,7 +628,7 @@ class Field(object):
|
|||
This allows us to create descriptive representations for 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...
|
||||
|
@ -724,7 +705,7 @@ class NullBooleanField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
|
||||
kwargs['allow_null'] = True
|
||||
super(NullBooleanField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
|
@ -764,17 +745,13 @@ class CharField(Field):
|
|||
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
|
||||
self.max_length = kwargs.pop('max_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:
|
||||
message = lazy(
|
||||
self.error_messages['max_length'].format,
|
||||
six.text_type)(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))
|
||||
if self.min_length is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_length'].format,
|
||||
six.text_type)(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))
|
||||
|
||||
|
@ -786,23 +763,23 @@ class CharField(Field):
|
|||
# Test for the empty string here so that it does not get validated,
|
||||
# and so that subclasses do not need to handle it explicitly
|
||||
# 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:
|
||||
self.fail('blank')
|
||||
return ''
|
||||
return super(CharField, self).run_validation(data)
|
||||
return super().run_validation(data)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# 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`,
|
||||
# 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')
|
||||
value = six.text_type(data)
|
||||
value = str(data)
|
||||
return value.strip() if self.trim_whitespace else value
|
||||
|
||||
def to_representation(self, value):
|
||||
return six.text_type(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
class EmailField(CharField):
|
||||
|
@ -811,7 +788,7 @@ class EmailField(CharField):
|
|||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(EmailField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
validator = EmailValidator(message=self.error_messages['invalid'])
|
||||
self.validators.append(validator)
|
||||
|
||||
|
@ -822,7 +799,7 @@ class RegexField(CharField):
|
|||
}
|
||||
|
||||
def __init__(self, regex, **kwargs):
|
||||
super(RegexField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
validator = RegexValidator(regex, message=self.error_messages['invalid'])
|
||||
self.validators.append(validator)
|
||||
|
||||
|
@ -834,7 +811,7 @@ class SlugField(CharField):
|
|||
}
|
||||
|
||||
def __init__(self, allow_unicode=False, **kwargs):
|
||||
super(SlugField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.allow_unicode = allow_unicode
|
||||
if self.allow_unicode:
|
||||
validator = RegexValidator(re.compile(r'^[-\w]+\Z', re.UNICODE), message=self.error_messages['invalid_unicode'])
|
||||
|
@ -849,7 +826,7 @@ class URLField(CharField):
|
|||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(URLField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
validator = URLValidator(message=self.error_messages['invalid'])
|
||||
self.validators.append(validator)
|
||||
|
||||
|
@ -866,16 +843,16 @@ class UUIDField(Field):
|
|||
if self.uuid_format not in self.valid_formats:
|
||||
raise ValueError(
|
||||
'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):
|
||||
if not isinstance(data, uuid.UUID):
|
||||
try:
|
||||
if isinstance(data, six.integer_types):
|
||||
if isinstance(data, int):
|
||||
return uuid.UUID(int=data)
|
||||
elif isinstance(data, six.string_types):
|
||||
elif isinstance(data, str):
|
||||
return uuid.UUID(hex=data)
|
||||
else:
|
||||
self.fail('invalid', value=data)
|
||||
|
@ -900,12 +877,12 @@ class IPAddressField(CharField):
|
|||
def __init__(self, protocol='both', **kwargs):
|
||||
self.protocol = protocol.lower()
|
||||
self.unpack_ipv4 = (self.protocol == 'both')
|
||||
super(IPAddressField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
validators, error_message = ip_address_validators(protocol, self.unpack_ipv4)
|
||||
self.validators.extend(validators)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, six.string_types):
|
||||
if not isinstance(data, str):
|
||||
self.fail('invalid', value=data)
|
||||
|
||||
if ':' in data:
|
||||
|
@ -915,7 +892,7 @@ class IPAddressField(CharField):
|
|||
except DjangoValidationError:
|
||||
self.fail('invalid', value=data)
|
||||
|
||||
return super(IPAddressField, self).to_internal_value(data)
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
# Number types...
|
||||
|
@ -933,22 +910,18 @@ class IntegerField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
self.max_value = kwargs.pop('max_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:
|
||||
message = lazy(
|
||||
self.error_messages['max_value'].format,
|
||||
six.text_type)(max_value=self.max_value)
|
||||
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
|
||||
self.validators.append(
|
||||
MaxValueValidator(self.max_value, message=message))
|
||||
if self.min_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_value'].format,
|
||||
six.text_type)(min_value=self.min_value)
|
||||
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
|
||||
self.validators.append(
|
||||
MinValueValidator(self.min_value, message=message))
|
||||
|
||||
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')
|
||||
|
||||
try:
|
||||
|
@ -973,23 +946,19 @@ class FloatField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
self.max_value = kwargs.pop('max_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:
|
||||
message = lazy(
|
||||
self.error_messages['max_value'].format,
|
||||
six.text_type)(max_value=self.max_value)
|
||||
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
|
||||
self.validators.append(
|
||||
MaxValueValidator(self.max_value, message=message))
|
||||
if self.min_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_value'].format,
|
||||
six.text_type)(min_value=self.min_value)
|
||||
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
|
||||
self.validators.append(
|
||||
MinValueValidator(self.min_value, message=message))
|
||||
|
||||
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')
|
||||
|
||||
try:
|
||||
|
@ -1031,18 +1000,14 @@ class DecimalField(Field):
|
|||
else:
|
||||
self.max_whole_digits = None
|
||||
|
||||
super(DecimalField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if self.max_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['max_value'].format,
|
||||
six.text_type)(max_value=self.max_value)
|
||||
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
|
||||
self.validators.append(
|
||||
MaxValueValidator(self.max_value, message=message))
|
||||
if self.min_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_value'].format,
|
||||
six.text_type)(min_value=self.min_value)
|
||||
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
|
||||
self.validators.append(
|
||||
MinValueValidator(self.min_value, message=message))
|
||||
|
||||
|
@ -1121,7 +1086,7 @@ class DecimalField(Field):
|
|||
coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING)
|
||||
|
||||
if not isinstance(value, decimal.Decimal):
|
||||
value = decimal.Decimal(six.text_type(value).strip())
|
||||
value = decimal.Decimal(str(value).strip())
|
||||
|
||||
quantized = self.quantize(value)
|
||||
|
||||
|
@ -1130,7 +1095,7 @@ class DecimalField(Field):
|
|||
if self.localize:
|
||||
return localize_input(quantized)
|
||||
|
||||
return '{0:f}'.format(quantized)
|
||||
return '{:f}'.format(quantized)
|
||||
|
||||
def quantize(self, value):
|
||||
"""
|
||||
|
@ -1167,7 +1132,7 @@ class DateTimeField(Field):
|
|||
self.input_formats = input_formats
|
||||
if default_timezone is not None:
|
||||
self.timezone = default_timezone
|
||||
super(DateTimeField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def enforce_timezone(self, value):
|
||||
"""
|
||||
|
@ -1226,7 +1191,7 @@ class DateTimeField(Field):
|
|||
|
||||
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
|
||||
|
||||
value = self.enforce_timezone(value)
|
||||
|
@ -1251,7 +1216,7 @@ class DateField(Field):
|
|||
self.format = format
|
||||
if input_formats is not None:
|
||||
self.input_formats = input_formats
|
||||
super(DateField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, value):
|
||||
input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS)
|
||||
|
@ -1288,7 +1253,7 @@ class DateField(Field):
|
|||
|
||||
output_format = getattr(self, 'format', api_settings.DATE_FORMAT)
|
||||
|
||||
if output_format is None or isinstance(value, six.string_types):
|
||||
if output_format is None or isinstance(value, str):
|
||||
return value
|
||||
|
||||
# Applying a `DateField` to a datetime value is almost always
|
||||
|
@ -1317,7 +1282,7 @@ class TimeField(Field):
|
|||
self.format = format
|
||||
if input_formats is not None:
|
||||
self.input_formats = input_formats
|
||||
super(TimeField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, value):
|
||||
input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS)
|
||||
|
@ -1351,7 +1316,7 @@ class TimeField(Field):
|
|||
|
||||
output_format = getattr(self, 'format', api_settings.TIME_FORMAT)
|
||||
|
||||
if output_format is None or isinstance(value, six.string_types):
|
||||
if output_format is None or isinstance(value, str):
|
||||
return value
|
||||
|
||||
# Applying a `TimeField` to a datetime value is almost always
|
||||
|
@ -1378,24 +1343,20 @@ class DurationField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
self.max_value = kwargs.pop('max_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:
|
||||
message = lazy(
|
||||
self.error_messages['max_value'].format,
|
||||
six.text_type)(max_value=self.max_value)
|
||||
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
|
||||
self.validators.append(
|
||||
MaxValueValidator(self.max_value, message=message))
|
||||
if self.min_value is not None:
|
||||
message = lazy(
|
||||
self.error_messages['min_value'].format,
|
||||
six.text_type)(min_value=self.min_value)
|
||||
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
|
||||
self.validators.append(
|
||||
MinValueValidator(self.min_value, message=message))
|
||||
|
||||
def to_internal_value(self, value):
|
||||
if isinstance(value, datetime.timedelta):
|
||||
return value
|
||||
parsed = parse_duration(six.text_type(value))
|
||||
parsed = parse_duration(str(value))
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]')
|
||||
|
@ -1420,21 +1381,21 @@ class ChoiceField(Field):
|
|||
|
||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
||||
|
||||
super(ChoiceField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data == '' and self.allow_blank:
|
||||
return ''
|
||||
|
||||
try:
|
||||
return self.choice_strings_to_values[six.text_type(data)]
|
||||
return self.choice_strings_to_values[str(data)]
|
||||
except KeyError:
|
||||
self.fail('invalid_choice', input=data)
|
||||
|
||||
def to_representation(self, value):
|
||||
if value in ('', None):
|
||||
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):
|
||||
"""
|
||||
|
@ -1457,7 +1418,7 @@ class ChoiceField(Field):
|
|||
# Allows us to deal with eg. integer choices while supporting either
|
||||
# integer or string input, but still get the correct datatype out.
|
||||
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)
|
||||
|
@ -1473,7 +1434,7 @@ class MultipleChoiceField(ChoiceField):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||
super(MultipleChoiceField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
if self.field_name not in dictionary:
|
||||
|
@ -1486,7 +1447,7 @@ class MultipleChoiceField(ChoiceField):
|
|||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
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__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
@ -1498,7 +1459,7 @@ class MultipleChoiceField(ChoiceField):
|
|||
|
||||
def to_representation(self, value):
|
||||
return {
|
||||
self.choice_strings_to_values.get(six.text_type(item), item) for item in value
|
||||
self.choice_strings_to_values.get(str(item), item) for item in value
|
||||
}
|
||||
|
||||
|
||||
|
@ -1516,7 +1477,7 @@ class FilePathField(ChoiceField):
|
|||
allow_folders=allow_folders, required=required
|
||||
)
|
||||
kwargs['choices'] = field.choices
|
||||
super(FilePathField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
# File types...
|
||||
|
@ -1535,7 +1496,7 @@ class FileField(Field):
|
|||
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
|
||||
if 'use_url' in kwargs:
|
||||
self.use_url = kwargs.pop('use_url')
|
||||
super(FileField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
|
@ -1581,13 +1542,13 @@ class ImageField(FileField):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField)
|
||||
super(ImageField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# Image validation is a bit grungy, so we'll just outright
|
||||
# defer to Django's implementation so we don't need to
|
||||
# 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.error_messages = self.error_messages
|
||||
return django_field.clean(file_object)
|
||||
|
@ -1597,7 +1558,7 @@ class ImageField(FileField):
|
|||
|
||||
class _UnvalidatedField(Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(_UnvalidatedField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.allow_blank = True
|
||||
self.allow_null = True
|
||||
|
||||
|
@ -1630,13 +1591,13 @@ class ListField(Field):
|
|||
"Remove `source=` from the field declaration."
|
||||
)
|
||||
|
||||
super(ListField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.child.bind(field_name='', parent=self)
|
||||
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))
|
||||
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))
|
||||
|
||||
def get_value(self, dictionary):
|
||||
|
@ -1660,7 +1621,7 @@ class ListField(Field):
|
|||
"""
|
||||
if html.is_html_input(data):
|
||||
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__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
@ -1691,11 +1652,13 @@ class DictField(Field):
|
|||
child = _UnvalidatedField()
|
||||
initial = {}
|
||||
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):
|
||||
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 self.child.source is None, (
|
||||
|
@ -1703,7 +1666,7 @@ class DictField(Field):
|
|||
"Remove `source=` from the field declaration."
|
||||
)
|
||||
|
||||
super(DictField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.child.bind(field_name='', parent=self)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
|
@ -1721,11 +1684,14 @@ class DictField(Field):
|
|||
data = html.parse_html_dict(data)
|
||||
if not isinstance(data, dict):
|
||||
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)
|
||||
|
||||
def to_representation(self, value):
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -1734,7 +1700,7 @@ class DictField(Field):
|
|||
errors = OrderedDict()
|
||||
|
||||
for key, value in data.items():
|
||||
key = six.text_type(key)
|
||||
key = str(key)
|
||||
|
||||
try:
|
||||
result[key] = self.child.run_validation(value)
|
||||
|
@ -1750,7 +1716,7 @@ class HStoreField(DictField):
|
|||
child = CharField(allow_blank=True, allow_null=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HStoreField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
assert isinstance(self.child, CharField), (
|
||||
"The `child` argument must be an instance of `CharField`, "
|
||||
"as the hstore extension stores values as strings."
|
||||
|
@ -1764,15 +1730,16 @@ class JSONField(Field):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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):
|
||||
if html.is_html_input(dictionary) and self.field_name in dictionary:
|
||||
# When HTML form input is used, mark up the input
|
||||
# as being a JSON string, rather than a JSON primitive.
|
||||
class JSONString(six.text_type):
|
||||
class JSONString(str):
|
||||
def __new__(self, value):
|
||||
ret = six.text_type.__new__(self, value)
|
||||
ret = str.__new__(self, value)
|
||||
ret.is_json_string = True
|
||||
return ret
|
||||
return JSONString(dictionary[self.field_name])
|
||||
|
@ -1782,21 +1749,18 @@ class JSONField(Field):
|
|||
try:
|
||||
if self.binary or getattr(data, 'is_json_string', False):
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
data = data.decode()
|
||||
return json.loads(data)
|
||||
else:
|
||||
json.dumps(data)
|
||||
json.dumps(data, cls=self.encoder)
|
||||
except (TypeError, ValueError):
|
||||
self.fail('invalid')
|
||||
return data
|
||||
|
||||
def to_representation(self, value):
|
||||
if self.binary:
|
||||
value = json.dumps(value)
|
||||
# On python 2.x the return type for json.dumps() is underspecified.
|
||||
# On python 3.x json.dumps() returns unicode strings.
|
||||
if isinstance(value, six.text_type):
|
||||
value = bytes(value.encode('utf-8'))
|
||||
value = json.dumps(value, cls=self.encoder)
|
||||
value = value.encode()
|
||||
return value
|
||||
|
||||
|
||||
|
@ -1817,7 +1781,7 @@ class ReadOnlyField(Field):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['read_only'] = True
|
||||
super(ReadOnlyField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
|
@ -1834,7 +1798,7 @@ class HiddenField(Field):
|
|||
def __init__(self, **kwargs):
|
||||
assert 'default' in kwargs, 'default is a required argument.'
|
||||
kwargs['write_only'] = True
|
||||
super(HiddenField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
# We always use the default value for `HiddenField`.
|
||||
|
@ -1864,7 +1828,7 @@ class SerializerMethodField(Field):
|
|||
self.method_name = method_name
|
||||
kwargs['source'] = '*'
|
||||
kwargs['read_only'] = True
|
||||
super(SerializerMethodField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
# In order to enforce a consistent style, we error if a redundant
|
||||
|
@ -1882,7 +1846,7 @@ class SerializerMethodField(Field):
|
|||
if self.method_name is None:
|
||||
self.method_name = default_method_name
|
||||
|
||||
super(SerializerMethodField, self).bind(field_name, parent)
|
||||
super().bind(field_name, parent)
|
||||
|
||||
def to_representation(self, value):
|
||||
method = getattr(self.parent, self.method_name)
|
||||
|
@ -1905,11 +1869,9 @@ class ModelField(Field):
|
|||
# The `max_length` option is supported by Django's base `Field` class,
|
||||
# so we'd better support it here.
|
||||
max_length = kwargs.pop('max_length', None)
|
||||
super(ModelField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
if max_length is not None:
|
||||
message = lazy(
|
||||
self.error_messages['max_length'].format,
|
||||
six.text_type)(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))
|
||||
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
Provides generic filtering backends that can be used to filter the results
|
||||
returned by list views.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import operator
|
||||
import warnings
|
||||
from functools import reduce
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
@ -13,18 +10,14 @@ from django.db import models
|
|||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.sql.constants import ORDER_PATTERN
|
||||
from django.template import loader
|
||||
from django.utils import six
|
||||
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, is_guardian_installed
|
||||
)
|
||||
from rest_framework.compat import coreapi, coreschema, distinct
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
class BaseFilterBackend(object):
|
||||
class BaseFilterBackend:
|
||||
"""
|
||||
A base class from which all filter backend classes should inherit.
|
||||
"""
|
||||
|
@ -40,6 +33,9 @@ class BaseFilterBackend(object):
|
|||
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
|
||||
return []
|
||||
|
||||
def get_schema_operation_parameters(self, view):
|
||||
return []
|
||||
|
||||
|
||||
class SearchFilter(BaseFilterBackend):
|
||||
# The URL query parameter used for the search.
|
||||
|
@ -109,7 +105,7 @@ class SearchFilter(BaseFilterBackend):
|
|||
return queryset
|
||||
|
||||
orm_lookups = [
|
||||
self.construct_search(six.text_type(search_field))
|
||||
self.construct_search(str(search_field))
|
||||
for search_field in search_fields
|
||||
]
|
||||
|
||||
|
@ -159,6 +155,19 @@ class SearchFilter(BaseFilterBackend):
|
|||
)
|
||||
]
|
||||
|
||||
def get_schema_operation_parameters(self, view):
|
||||
return [
|
||||
{
|
||||
'name': self.search_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.search_description),
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class OrderingFilter(BaseFilterBackend):
|
||||
# The URL query parameter used for the ordering.
|
||||
|
@ -188,7 +197,7 @@ class OrderingFilter(BaseFilterBackend):
|
|||
|
||||
def get_default_ordering(self, view):
|
||||
ordering = getattr(view, 'ordering', None)
|
||||
if isinstance(ordering, six.string_types):
|
||||
if isinstance(ordering, str):
|
||||
return (ordering,)
|
||||
return ordering
|
||||
|
||||
|
@ -237,7 +246,7 @@ class OrderingFilter(BaseFilterBackend):
|
|||
]
|
||||
else:
|
||||
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
|
||||
]
|
||||
|
||||
|
@ -290,40 +299,15 @@ class OrderingFilter(BaseFilterBackend):
|
|||
)
|
||||
]
|
||||
|
||||
|
||||
class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
||||
"""
|
||||
A filter backend that limits results to those where the requesting user
|
||||
has read object level permissions.
|
||||
"""
|
||||
def __init__(self):
|
||||
warnings.warn(
|
||||
"`DjangoObjectPermissionsFilter` has been deprecated and moved to "
|
||||
"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)
|
||||
def get_schema_operation_parameters(self, view):
|
||||
return [
|
||||
{
|
||||
'name': self.ordering_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.ordering_description),
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
Generic views that provide commonly needed behaviour.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import Http404
|
||||
|
|
|
@ -1,41 +1,56 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from rest_framework.compat import coreapi
|
||||
from rest_framework.renderers import (
|
||||
CoreJSONRenderer, JSONOpenAPIRenderer, OpenAPIRenderer
|
||||
)
|
||||
from rest_framework.schemas.generators import SchemaGenerator
|
||||
from rest_framework import renderers
|
||||
from rest_framework.schemas import coreapi
|
||||
from rest_framework.schemas.openapi import SchemaGenerator
|
||||
|
||||
OPENAPI_MODE = 'openapi'
|
||||
COREAPI_MODE = 'coreapi'
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
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):
|
||||
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('--description', dest="description", default=None, type=str)
|
||||
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
|
||||
if self.get_mode() == COREAPI_MODE:
|
||||
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
|
||||
else:
|
||||
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json'], default='openapi', type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
assert coreapi is not None, 'coreapi must be installed.'
|
||||
|
||||
generator = SchemaGenerator(
|
||||
generator_class = self.get_generator_class()
|
||||
generator = generator_class(
|
||||
url=options['url'],
|
||||
title=options['title'],
|
||||
description=options['description']
|
||||
)
|
||||
|
||||
schema = generator.get_schema(request=None, public=True)
|
||||
|
||||
renderer = self.get_renderer(options['format'])
|
||||
output = renderer.render(schema, renderer_context={})
|
||||
self.stdout.write(output.decode('utf-8'))
|
||||
self.stdout.write(output.decode())
|
||||
|
||||
def get_renderer(self, format):
|
||||
renderer_cls = {
|
||||
'corejson': CoreJSONRenderer,
|
||||
'openapi': OpenAPIRenderer,
|
||||
'openapi-json': JSONOpenAPIRenderer,
|
||||
}[format]
|
||||
if self.get_mode() == COREAPI_MODE:
|
||||
renderer_cls = {
|
||||
'corejson': renderers.CoreJSONRenderer,
|
||||
'openapi': renderers.CoreAPIOpenAPIRenderer,
|
||||
'openapi-json': renderers.CoreAPIJSONOpenAPIRenderer,
|
||||
}[format]
|
||||
return renderer_cls()
|
||||
|
||||
renderer_cls = {
|
||||
'openapi': renderers.OpenAPIRenderer,
|
||||
'openapi-json': renderers.JSONOpenAPIRenderer,
|
||||
}[format]
|
||||
return renderer_cls()
|
||||
|
||||
def get_generator_class(self):
|
||||
if self.get_mode() == COREAPI_MODE:
|
||||
return coreapi.SchemaGenerator
|
||||
return SchemaGenerator
|
||||
|
|
|
@ -6,8 +6,6 @@ some fairly ad-hoc information about the view.
|
|||
Future implementations might use JSON schema or other definitions in order
|
||||
to return this information in a more standardized way.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
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
|
||||
|
||||
|
||||
class BaseMetadata(object):
|
||||
class BaseMetadata:
|
||||
def determine_metadata(self, request, view):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
|
|
|
@ -4,14 +4,12 @@ Basic building blocks for generic class based views.
|
|||
We don't bind behaviour to http method handlers yet,
|
||||
which allows mixin classes to be composed in interesting ways.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
class CreateModelMixin(object):
|
||||
class CreateModelMixin:
|
||||
"""
|
||||
Create a model instance.
|
||||
"""
|
||||
|
@ -32,7 +30,7 @@ class CreateModelMixin(object):
|
|||
return {}
|
||||
|
||||
|
||||
class ListModelMixin(object):
|
||||
class ListModelMixin:
|
||||
"""
|
||||
List a queryset.
|
||||
"""
|
||||
|
@ -48,7 +46,7 @@ class ListModelMixin(object):
|
|||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RetrieveModelMixin(object):
|
||||
class RetrieveModelMixin:
|
||||
"""
|
||||
Retrieve a model instance.
|
||||
"""
|
||||
|
@ -58,7 +56,7 @@ class RetrieveModelMixin(object):
|
|||
return Response(serializer.data)
|
||||
|
||||
|
||||
class UpdateModelMixin(object):
|
||||
class UpdateModelMixin:
|
||||
"""
|
||||
Update a model instance.
|
||||
"""
|
||||
|
@ -84,7 +82,7 @@ class UpdateModelMixin(object):
|
|||
return self.update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class DestroyModelMixin(object):
|
||||
class DestroyModelMixin:
|
||||
"""
|
||||
Destroy a model instance.
|
||||
"""
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
Content negotiation deals with selecting an appropriate renderer given the
|
||||
incoming request. Typically this will be based on the request's Accept header.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
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):
|
||||
raise NotImplementedError('.select_parser() must be implemented')
|
||||
|
||||
|
@ -66,7 +64,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
|
|||
# Accepted media type is 'application/json'
|
||||
full_media_type = ';'.join(
|
||||
(renderer.media_type,) +
|
||||
tuple('{0}={1}'.format(
|
||||
tuple('{}={}'.format(
|
||||
key, value.decode(HTTP_HEADER_ENCODING))
|
||||
for key, value in media_type_wrapper.params.items()))
|
||||
return renderer, full_media_type
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
# coding: utf-8
|
||||
"""
|
||||
Pagination serializers determine the structure of the output that should
|
||||
be used for paginated responses.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from collections import OrderedDict, namedtuple
|
||||
from urllib import parse
|
||||
|
||||
from django.core.paginator import InvalidPage
|
||||
from django.core.paginator import Paginator as DjangoPaginator
|
||||
from django.template import loader
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
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)
|
||||
|
||||
|
||||
class BasePagination(object):
|
||||
class BasePagination:
|
||||
display_page_controls = False
|
||||
|
||||
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()`'
|
||||
return []
|
||||
|
||||
def get_schema_operation_parameters(self, view):
|
||||
return []
|
||||
|
||||
|
||||
class PageNumberPagination(BasePagination):
|
||||
"""
|
||||
|
@ -204,7 +203,7 @@ class PageNumberPagination(BasePagination):
|
|||
self.page = paginator.page(page_number)
|
||||
except InvalidPage as exc:
|
||||
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)
|
||||
|
||||
|
@ -305,6 +304,32 @@ class PageNumberPagination(BasePagination):
|
|||
)
|
||||
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):
|
||||
"""
|
||||
|
@ -434,6 +459,15 @@ class LimitOffsetPagination(BasePagination):
|
|||
context = self.get_html_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):
|
||||
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()`'
|
||||
|
@ -458,14 +492,28 @@ class LimitOffsetPagination(BasePagination):
|
|||
)
|
||||
]
|
||||
|
||||
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_operation_parameters(self, view):
|
||||
parameters = [
|
||||
{
|
||||
'name': self.limit_query_param,
|
||||
'required': False,
|
||||
'in': 'query',
|
||||
'description': force_text(self.limit_query_description),
|
||||
'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):
|
||||
|
@ -589,7 +637,7 @@ class CursorPagination(BasePagination):
|
|||
if not self.has_next:
|
||||
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
|
||||
# then we cannot use the first position we find as a marker.
|
||||
compare = self._get_position_from_instance(self.page[-1], self.ordering)
|
||||
|
@ -597,12 +645,14 @@ class CursorPagination(BasePagination):
|
|||
compare = self.next_position
|
||||
offset = 0
|
||||
|
||||
has_item_with_unique_position = False
|
||||
for item in reversed(self.page):
|
||||
position = self._get_position_from_instance(item, self.ordering)
|
||||
if position != compare:
|
||||
# The item in this position and the item following it
|
||||
# have different positions. We can use this position as
|
||||
# our marker.
|
||||
has_item_with_unique_position = True
|
||||
break
|
||||
|
||||
# The item in this position has the same position as the item
|
||||
|
@ -611,7 +661,7 @@ class CursorPagination(BasePagination):
|
|||
compare = position
|
||||
offset += 1
|
||||
|
||||
else:
|
||||
if self.page and not has_item_with_unique_position:
|
||||
# There were no unique positions in the page.
|
||||
if not self.has_previous:
|
||||
# We are on the first page.
|
||||
|
@ -630,6 +680,9 @@ class CursorPagination(BasePagination):
|
|||
offset = self.cursor.offset + self.page_size
|
||||
position = self.previous_position
|
||||
|
||||
if not self.page:
|
||||
position = self.next_position
|
||||
|
||||
cursor = Cursor(offset=offset, reverse=False, position=position)
|
||||
return self.encode_cursor(cursor)
|
||||
|
||||
|
@ -637,7 +690,7 @@ class CursorPagination(BasePagination):
|
|||
if not self.has_previous:
|
||||
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
|
||||
# then we cannot use the first position we find as a marker.
|
||||
compare = self._get_position_from_instance(self.page[0], self.ordering)
|
||||
|
@ -645,12 +698,14 @@ class CursorPagination(BasePagination):
|
|||
compare = self.previous_position
|
||||
offset = 0
|
||||
|
||||
has_item_with_unique_position = False
|
||||
for item in self.page:
|
||||
position = self._get_position_from_instance(item, self.ordering)
|
||||
if position != compare:
|
||||
# The item in this position and the item following it
|
||||
# have different positions. We can use this position as
|
||||
# our marker.
|
||||
has_item_with_unique_position = True
|
||||
break
|
||||
|
||||
# The item in this position has the same position as the item
|
||||
|
@ -659,7 +714,7 @@ class CursorPagination(BasePagination):
|
|||
compare = position
|
||||
offset += 1
|
||||
|
||||
else:
|
||||
if self.page and not has_item_with_unique_position:
|
||||
# There were no unique positions in the page.
|
||||
if not self.has_next:
|
||||
# We are on the final page.
|
||||
|
@ -678,6 +733,9 @@ class CursorPagination(BasePagination):
|
|||
offset = 0
|
||||
position = self.next_position
|
||||
|
||||
if not self.page:
|
||||
position = self.previous_position
|
||||
|
||||
cursor = Cursor(offset=offset, reverse=True, position=position)
|
||||
return self.encode_cursor(cursor)
|
||||
|
||||
|
@ -716,13 +774,13 @@ class CursorPagination(BasePagination):
|
|||
'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(
|
||||
type=type(ordering).__name__
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(ordering, six.string_types):
|
||||
if isinstance(ordering, str):
|
||||
return (ordering,)
|
||||
return tuple(ordering)
|
||||
|
||||
|
@ -737,7 +795,7 @@ class CursorPagination(BasePagination):
|
|||
|
||||
try:
|
||||
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 = _positive_int(offset, cutoff=self.offset_cutoff)
|
||||
|
@ -763,7 +821,7 @@ class CursorPagination(BasePagination):
|
|||
if cursor.position is not None:
|
||||
tokens['p'] = cursor.position
|
||||
|
||||
querystring = urlparse.urlencode(tokens, doseq=True)
|
||||
querystring = parse.urlencode(tokens, doseq=True)
|
||||
encoded = b64encode(querystring.encode('ascii')).decode('ascii')
|
||||
return replace_query_param(self.base_url, self.cursor_query_param, encoded)
|
||||
|
||||
|
@ -773,7 +831,7 @@ class CursorPagination(BasePagination):
|
|||
attr = instance[field_name]
|
||||
else:
|
||||
attr = getattr(instance, field_name)
|
||||
return six.text_type(attr)
|
||||
return str(attr)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return Response(OrderedDict([
|
||||
|
@ -820,3 +878,29 @@ class CursorPagination(BasePagination):
|
|||
)
|
||||
)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
on the request, such as form content or json encoded data.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import codecs
|
||||
from urllib import parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadhandler import StopFutureHandlers
|
||||
|
@ -15,9 +14,7 @@ from django.http.multipartparser import ChunkIter
|
|||
from django.http.multipartparser import \
|
||||
MultiPartParser as DjangoMultiPartParser
|
||||
from django.http.multipartparser import MultiPartParserError, parse_header
|
||||
from django.utils import six
|
||||
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.exceptions import ParseError
|
||||
|
@ -25,13 +22,13 @@ from rest_framework.settings import api_settings
|
|||
from rest_framework.utils import json
|
||||
|
||||
|
||||
class DataAndFiles(object):
|
||||
class DataAndFiles:
|
||||
def __init__(self, data, files):
|
||||
self.data = data
|
||||
self.files = files
|
||||
|
||||
|
||||
class BaseParser(object):
|
||||
class BaseParser:
|
||||
"""
|
||||
All parsers should extend `BaseParser`, specifying a `media_type`
|
||||
attribute, and overriding the `.parse()` method.
|
||||
|
@ -67,7 +64,7 @@ class JSONParser(BaseParser):
|
|||
parse_constant = json.strict_constant if self.strict else None
|
||||
return json.load(decoded_stream, parse_constant=parse_constant)
|
||||
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):
|
||||
|
@ -83,8 +80,7 @@ class FormParser(BaseParser):
|
|||
"""
|
||||
parser_context = parser_context or {}
|
||||
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
|
||||
data = QueryDict(stream.read(), encoding=encoding)
|
||||
return data
|
||||
return QueryDict(stream.read(), encoding=encoding)
|
||||
|
||||
|
||||
class MultiPartParser(BaseParser):
|
||||
|
@ -113,7 +109,7 @@ class MultiPartParser(BaseParser):
|
|||
data, files = parser.parse()
|
||||
return DataAndFiles(data, files)
|
||||
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):
|
||||
|
@ -205,7 +201,7 @@ class FileUploadParser(BaseParser):
|
|||
|
||||
try:
|
||||
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]
|
||||
if 'filename*' in filename_parm:
|
||||
return self.get_encoded_filename(filename_parm)
|
||||
|
@ -221,7 +217,7 @@ class FileUploadParser(BaseParser):
|
|||
encoded_filename = force_text(filename_parm['filename*'])
|
||||
try:
|
||||
charset, lang, filename = encoded_filename.split('\'', 2)
|
||||
filename = urlparse.unquote(filename)
|
||||
filename = parse.unquote(filename)
|
||||
except (ValueError, LookupError):
|
||||
filename = force_text(filename_parm['filename'])
|
||||
return filename
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
"""
|
||||
Provides a set of pluggable permission policies.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.http import Http404
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import exceptions
|
||||
|
||||
|
@ -101,8 +98,7 @@ class BasePermissionMetaclass(OperationHolderMixin, type):
|
|||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(BasePermissionMetaclass)
|
||||
class BasePermission(object):
|
||||
class BasePermission(metaclass=BasePermissionMetaclass):
|
||||
"""
|
||||
A base class from which all permission classes should inherit.
|
||||
"""
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from urllib import parse
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django.db.models import Manager
|
||||
from django.db.models.query import QuerySet
|
||||
from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve
|
||||
from django.utils import six
|
||||
from django.utils.encoding import (
|
||||
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 django.utils.encoding import smart_text, uri_to_iri
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.fields import (
|
||||
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.
|
||||
We use this for hyperlinked URLs that may render as a named link
|
||||
in some contexts, or render as a plain URL in others.
|
||||
"""
|
||||
def __new__(self, url, obj):
|
||||
ret = six.text_type.__new__(self, url)
|
||||
ret = str.__new__(self, url)
|
||||
ret.obj = obj
|
||||
return ret
|
||||
|
||||
|
@ -65,13 +59,12 @@ class Hyperlink(six.text_type):
|
|||
# This ensures that we only called `__str__` lazily,
|
||||
# as in some cases calling __str__ on a model instances *might*
|
||||
# involve a database lookup.
|
||||
return six.text_type(self.obj)
|
||||
return str(self.obj)
|
||||
|
||||
is_hyperlink = True
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PKOnlyObject(object):
|
||||
class PKOnlyObject:
|
||||
"""
|
||||
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,
|
||||
|
@ -121,14 +114,14 @@ class RelatedField(Field):
|
|||
)
|
||||
kwargs.pop('many', None)
|
||||
kwargs.pop('allow_empty', None)
|
||||
super(RelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We override this method in order to automagically create
|
||||
# `ManyRelatedField` classes instead when `many=True` is set.
|
||||
if kwargs.pop('many', False):
|
||||
return cls.many_init(*args, **kwargs)
|
||||
return super(RelatedField, cls).__new__(cls, *args, **kwargs)
|
||||
return super().__new__(cls, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
|
@ -157,7 +150,7 @@ class RelatedField(Field):
|
|||
# We force empty strings to None values for relational fields.
|
||||
if data == '':
|
||||
data = None
|
||||
return super(RelatedField, self).run_validation(data)
|
||||
return super().run_validation(data)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset
|
||||
|
@ -189,7 +182,7 @@ class RelatedField(Field):
|
|||
pass
|
||||
|
||||
# Standard case, return the object instance.
|
||||
return super(RelatedField, self).get_attribute(instance)
|
||||
return super().get_attribute(instance)
|
||||
|
||||
def get_choices(self, cutoff=None):
|
||||
queryset = self.get_queryset()
|
||||
|
@ -225,7 +218,7 @@ class RelatedField(Field):
|
|||
)
|
||||
|
||||
def display_value(self, instance):
|
||||
return six.text_type(instance)
|
||||
return str(instance)
|
||||
|
||||
|
||||
class StringRelatedField(RelatedField):
|
||||
|
@ -236,10 +229,10 @@ class StringRelatedField(RelatedField):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['read_only'] = True
|
||||
super(StringRelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
return six.text_type(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
class PrimaryKeyRelatedField(RelatedField):
|
||||
|
@ -251,7 +244,7 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
self.pk_field = kwargs.pop('pk_field', None)
|
||||
super(PrimaryKeyRelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
return True
|
||||
|
@ -297,7 +290,7 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
# implicit `self` argument to be passed.
|
||||
self.reverse = reverse
|
||||
|
||||
super(HyperlinkedRelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
return self.lookup_field == 'pk'
|
||||
|
@ -317,10 +310,10 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
return queryset.get(**lookup_kwargs)
|
||||
except ValueError:
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
|
@ -346,7 +339,7 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
|
||||
if http_prefix:
|
||||
# If needed convert absolute URLs to relative path
|
||||
data = urlparse.urlparse(data).path
|
||||
data = parse.urlparse(data).path
|
||||
prefix = get_script_prefix()
|
||||
if data.startswith(prefix):
|
||||
data = '/' + data[len(prefix):]
|
||||
|
@ -432,7 +425,7 @@ class HyperlinkedIdentityField(HyperlinkedRelatedField):
|
|||
assert view_name is not None, 'The `view_name` argument is required.'
|
||||
kwargs['read_only'] = True
|
||||
kwargs['source'] = '*'
|
||||
super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
|
||||
super().__init__(view_name, **kwargs)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
# 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):
|
||||
assert slug_field is not None, 'The `slug_field` argument is required.'
|
||||
self.slug_field = slug_field
|
||||
super(SlugRelatedField, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
|
@ -502,7 +495,7 @@ class ManyRelatedField(Field):
|
|||
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
|
||||
)
|
||||
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)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
|
@ -518,7 +511,7 @@ class ManyRelatedField(Field):
|
|||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
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__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
from urllib import parse
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
@ -19,9 +18,7 @@ from django.http.multipartparser import parse_header
|
|||
from django.template import engines, loader
|
||||
from django.test.client import encode_multipart
|
||||
from django.urls import NoReverseMatch
|
||||
from django.utils import six
|
||||
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.compat import (
|
||||
|
@ -40,7 +37,7 @@ def zero_as_none(value):
|
|||
return None if value == 0 else value
|
||||
|
||||
|
||||
class BaseRenderer(object):
|
||||
class BaseRenderer:
|
||||
"""
|
||||
All renderers should extend this class, setting the `media_type`
|
||||
and `format` attributes, and override the `.render()` method.
|
||||
|
@ -91,7 +88,7 @@ class JSONRenderer(BaseRenderer):
|
|||
Render `data` into JSON, returning a bytestring.
|
||||
"""
|
||||
if data is None:
|
||||
return bytes()
|
||||
return b''
|
||||
|
||||
renderer_context = renderer_context or {}
|
||||
indent = self.get_indent(accepted_media_type, renderer_context)
|
||||
|
@ -107,18 +104,11 @@ class JSONRenderer(BaseRenderer):
|
|||
allow_nan=not self.strict, separators=separators
|
||||
)
|
||||
|
||||
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
|
||||
# but if ensure_ascii=False, the return type is underspecified,
|
||||
# and may (or may not) be unicode.
|
||||
# On python 3.x json.dumps() returns unicode strings.
|
||||
if isinstance(ret, six.text_type):
|
||||
# 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
|
||||
# We always fully escape \u2028 and \u2029 to ensure we output JSON
|
||||
# that is a strict javascript subset.
|
||||
# See: http://timelessrepo.com/json-isnt-a-javascript-subset
|
||||
ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
|
||||
return ret.encode()
|
||||
|
||||
|
||||
class TemplateHTMLRenderer(BaseRenderer):
|
||||
|
@ -349,7 +339,7 @@ class HTMLFormRenderer(BaseRenderer):
|
|||
# Get a clone of the field with text-only value representation.
|
||||
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')
|
||||
|
||||
if 'template' in style:
|
||||
|
@ -577,7 +567,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
data.pop(name, None)
|
||||
content = renderer.render(data, accepted, context)
|
||||
# Renders returns bytes, but CharField expects a str.
|
||||
content = content.decode('utf-8')
|
||||
content = content.decode()
|
||||
else:
|
||||
content = None
|
||||
|
||||
|
@ -684,7 +674,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
csrf_header_name = csrf_header_name[5:]
|
||||
csrf_header_name = csrf_header_name.replace('_', '-')
|
||||
|
||||
context = {
|
||||
return {
|
||||
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||
'code_style': pygments_css(self.code_style),
|
||||
'view': view,
|
||||
|
@ -720,7 +710,6 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
'csrf_cookie_name': csrf_cookie_name,
|
||||
'csrf_header_name': csrf_header_name
|
||||
}
|
||||
return context
|
||||
|
||||
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.
|
||||
"""
|
||||
context = super(AdminRenderer, self).get_context(
|
||||
context = super().get_context(
|
||||
data, accepted_media_type, renderer_context
|
||||
)
|
||||
|
||||
|
@ -995,14 +984,14 @@ class _BaseOpenAPIRenderer:
|
|||
|
||||
tag = None
|
||||
for name, link in document.links.items():
|
||||
path = urlparse.urlparse(link.url).path
|
||||
path = parse.urlparse(link.url).path
|
||||
method = link.action.lower()
|
||||
paths.setdefault(path, {})
|
||||
paths[path][method] = self.get_operation(link, name, tag=tag)
|
||||
|
||||
for tag, section in document.data.items():
|
||||
for name, link in section.links.items():
|
||||
path = urlparse.urlparse(link.url).path
|
||||
path = parse.urlparse(link.url).path
|
||||
method = link.action.lower()
|
||||
paths.setdefault(path, {})
|
||||
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'
|
||||
charset = None
|
||||
format = 'openapi'
|
||||
|
||||
def __init__(self):
|
||||
assert coreapi, 'Using OpenAPIRenderer, but `coreapi` is not installed.'
|
||||
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
|
||||
assert coreapi, 'Using CoreAPIOpenAPIRenderer, but `coreapi` is not installed.'
|
||||
assert yaml, 'Using CoreAPIOpenAPIRenderer, but `pyyaml` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
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'
|
||||
charset = None
|
||||
format = 'openapi-json'
|
||||
|
||||
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):
|
||||
structure = self.get_structure(data)
|
||||
return json.dumps(structure, indent=4).encode('utf-8')
|
||||
|
||||
|
||||
class OpenAPIRenderer(BaseRenderer):
|
||||
media_type = 'application/vnd.oai.openapi'
|
||||
charset = None
|
||||
format = 'openapi'
|
||||
|
||||
def __init__(self):
|
||||
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
return yaml.dump(data, default_flow_style=False).encode('utf-8')
|
||||
|
||||
|
||||
class JSONOpenAPIRenderer(BaseRenderer):
|
||||
media_type = 'application/vnd.oai.openapi+json'
|
||||
charset = None
|
||||
format = 'openapi-json'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
return json.dumps(data, indent=2).encode('utf-8')
|
||||
|
|
|
@ -8,8 +8,6 @@ The wrapped request then offers a richer API, in particular :
|
|||
- full support of PUT method, including support for file uploads
|
||||
- form overloading of HTTP method, content type and content
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import io
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
@ -18,7 +16,6 @@ from django.conf import settings
|
|||
from django.http import HttpRequest, QueryDict
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.http.request import RawPostDataException
|
||||
from django.utils import six
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
|
||||
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')
|
||||
|
||||
|
||||
class override_method(object):
|
||||
class override_method:
|
||||
"""
|
||||
A context manager that temporarily overrides the method on a request,
|
||||
additionally setting the `view.request` attribute.
|
||||
|
@ -78,10 +75,10 @@ def wrap_attributeerrors():
|
|||
except AttributeError:
|
||||
info = sys.exc_info()
|
||||
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.
|
||||
Cannot use `None`, as that may be a valid value.
|
||||
|
@ -126,7 +123,7 @@ def clone_request(request, method):
|
|||
return ret
|
||||
|
||||
|
||||
class ForcedAuthentication(object):
|
||||
class ForcedAuthentication:
|
||||
"""
|
||||
This authentication class is used if the test client or request factory
|
||||
forcibly authenticated the request.
|
||||
|
@ -140,7 +137,7 @@ class ForcedAuthentication(object):
|
|||
return (self.force_user, self.force_token)
|
||||
|
||||
|
||||
class Request(object):
|
||||
class Request:
|
||||
"""
|
||||
Wrapper allowing to enhance a standard `HttpRequest` instance.
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from http.client import responses
|
||||
|
||||
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
|
||||
|
||||
|
@ -29,7 +27,7 @@ class Response(SimpleTemplateResponse):
|
|||
Setting 'renderer' and 'media_type' will typically be deferred,
|
||||
For example being set automatically by the `APIView`.
|
||||
"""
|
||||
super(Response, self).__init__(None, status=status)
|
||||
super().__init__(None, status=status)
|
||||
|
||||
if isinstance(data, Serializer):
|
||||
msg = (
|
||||
|
@ -45,7 +43,7 @@ class Response(SimpleTemplateResponse):
|
|||
self.content_type = content_type
|
||||
|
||||
if headers:
|
||||
for name, value in six.iteritems(headers):
|
||||
for name, value in headers.items():
|
||||
self[name] = value
|
||||
|
||||
@property
|
||||
|
@ -64,18 +62,18 @@ class Response(SimpleTemplateResponse):
|
|||
content_type = self.content_type
|
||||
|
||||
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:
|
||||
content_type = media_type
|
||||
self['Content-Type'] = content_type
|
||||
|
||||
ret = renderer.render(self.data, accepted_media_type, context)
|
||||
if isinstance(ret, six.text_type):
|
||||
if isinstance(ret, str):
|
||||
assert charset, (
|
||||
'renderer returned unicode, and did not specify '
|
||||
'a charset value.'
|
||||
)
|
||||
return bytes(ret.encode(charset))
|
||||
return ret.encode(charset)
|
||||
|
||||
if not ret:
|
||||
del self['Content-Type']
|
||||
|
@ -94,7 +92,7 @@ class Response(SimpleTemplateResponse):
|
|||
"""
|
||||
Remove attributes from the response that shouldn't be cached.
|
||||
"""
|
||||
state = super(Response, self).__getstate__()
|
||||
state = super().__getstate__()
|
||||
for key in (
|
||||
'accepted_renderer', 'renderer_context', 'resolver_match',
|
||||
'client', 'request', 'json', 'wsgi_request'
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
"""
|
||||
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 reverse as django_reverse
|
||||
from django.utils import six
|
||||
from django.utils.functional import lazy
|
||||
|
||||
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
|
||||
|
||||
|
||||
reverse_lazy = lazy(reverse, six.text_type)
|
||||
reverse_lazy = lazy(reverse, str)
|
||||
|
|
|
@ -13,8 +13,6 @@ For example, you might have a `urls.py` that looks something like this:
|
|||
|
||||
urlpatterns = router.urls
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import warnings
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
@ -22,12 +20,9 @@ from collections import OrderedDict, namedtuple
|
|||
from django.conf.urls import url
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.urls import NoReverseMatch
|
||||
from django.utils import six
|
||||
from django.utils.deprecation import RenameMethodsBase
|
||||
|
||||
from rest_framework import (
|
||||
RemovedInDRF310Warning, RemovedInDRF311Warning, views
|
||||
)
|
||||
from rest_framework import RemovedInDRF311Warning, views
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
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'])
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
self.registry = []
|
||||
|
||||
|
@ -173,7 +146,7 @@ class SimpleRouter(BaseRouter):
|
|||
|
||||
def __init__(self, trailing_slash=True):
|
||||
self.trailing_slash = '/' if trailing_slash else ''
|
||||
super(SimpleRouter, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
def get_default_basename(self, viewset):
|
||||
"""
|
||||
|
@ -365,7 +338,7 @@ class DefaultRouter(SimpleRouter):
|
|||
self.root_renderers = kwargs.pop('root_renderers')
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
|
@ -383,7 +356,7 @@ class DefaultRouter(SimpleRouter):
|
|||
Generate the list of URL patterns, including a default root view
|
||||
for the API, and appending `.json` style format suffixes.
|
||||
"""
|
||||
urls = super(DefaultRouter, self).get_urls()
|
||||
urls = super().get_urls()
|
||||
|
||||
if self.include_root_view:
|
||||
view = self.get_api_root_view(api_urls=urls)
|
||||
|
|
|
@ -22,24 +22,32 @@ Other access should target the submodules directly
|
|||
"""
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
from .generators import SchemaGenerator
|
||||
from .inspectors import AutoSchema, DefaultSchema, ManualSchema # noqa
|
||||
from . import coreapi, openapi
|
||||
from .inspectors import DefaultSchema # noqa
|
||||
from .coreapi import AutoSchema, ManualSchema, SchemaGenerator # noqa
|
||||
|
||||
|
||||
def get_schema_view(
|
||||
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,
|
||||
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
|
||||
"""
|
||||
Return a schema view.
|
||||
"""
|
||||
# Avoid import cycle on APIView
|
||||
from .views import SchemaView
|
||||
if generator_class is None:
|
||||
if coreapi.is_enabled():
|
||||
generator_class = coreapi.SchemaGenerator
|
||||
else:
|
||||
generator_class = openapi.SchemaGenerator
|
||||
|
||||
generator = generator_class(
|
||||
title=title, url=url, description=description,
|
||||
urlconf=urlconf, patterns=patterns,
|
||||
)
|
||||
|
||||
# Avoid import cycle on APIView
|
||||
from .views import SchemaView
|
||||
return SchemaView.as_view(
|
||||
renderer_classes=renderer_classes,
|
||||
schema_generator=generator,
|
||||
|
|
616
rest_framework/schemas/coreapi.py
Normal file
616
rest_framework/schemas/coreapi.py
Normal 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)
|
|
@ -4,25 +4,19 @@ generators.py # Top-down schema generation
|
|||
See schemas.__init__.py for package overview.
|
||||
"""
|
||||
import re
|
||||
from collections import Counter, OrderedDict
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.admindocs.views import simplify_regex
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.compat import (
|
||||
URLPattern, URLResolver, coreapi, coreschema, get_original_route
|
||||
)
|
||||
from rest_framework.compat import URLPattern, URLResolver, get_original_route
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils.model_meta import _get_pk
|
||||
|
||||
from .utils import is_list_view
|
||||
|
||||
|
||||
def common_path(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)
|
||||
|
||||
|
||||
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):
|
||||
path, method, callback = endpoint
|
||||
method_priority = {
|
||||
|
@ -140,7 +62,7 @@ _PATH_PARAMETER_COMPONENT_RE = re.compile(
|
|||
)
|
||||
|
||||
|
||||
class EndpointEnumerator(object):
|
||||
class EndpointEnumerator:
|
||||
"""
|
||||
A class to determine the available API endpoints that a project exposes.
|
||||
"""
|
||||
|
@ -151,7 +73,7 @@ class EndpointEnumerator(object):
|
|||
urlconf = settings.ROOT_URLCONF
|
||||
|
||||
# Load the given URLconf module
|
||||
if isinstance(urlconf, six.string_types):
|
||||
if isinstance(urlconf, str):
|
||||
urls = import_module(urlconf)
|
||||
else:
|
||||
urls = urlconf
|
||||
|
@ -185,19 +107,20 @@ class EndpointEnumerator(object):
|
|||
)
|
||||
api_endpoints.extend(nested_endpoints)
|
||||
|
||||
api_endpoints = sorted(api_endpoints, key=endpoint_ordering)
|
||||
|
||||
return api_endpoints
|
||||
return sorted(api_endpoints, key=endpoint_ordering)
|
||||
|
||||
def get_path_from_regex(self, path_regex):
|
||||
"""
|
||||
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)
|
||||
|
||||
# Strip Django 2.0 convertors as they are incompatible with uritemplate format
|
||||
path = re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
|
||||
return path
|
||||
return re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
|
||||
|
||||
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')]
|
||||
|
||||
|
||||
class SchemaGenerator(object):
|
||||
# Map HTTP methods onto actions.
|
||||
default_mapping = {
|
||||
'get': 'retrieve',
|
||||
'post': 'create',
|
||||
'put': 'update',
|
||||
'patch': 'partial_update',
|
||||
'delete': 'destroy',
|
||||
}
|
||||
class BaseSchemaGenerator(object):
|
||||
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,
|
||||
# so by default we prefer to use the actual model field name for schemas.
|
||||
# Set by 'SCHEMA_COERCE_PATH_PK'.
|
||||
coerce_path_pk = 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('/'):
|
||||
url += '/'
|
||||
|
||||
self.coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
|
||||
self.coerce_path_pk = api_settings.SCHEMA_COERCE_PATH_PK
|
||||
|
||||
self.patterns = patterns
|
||||
|
@ -270,36 +176,15 @@ class SchemaGenerator(object):
|
|||
self.url = url
|
||||
self.endpoints = None
|
||||
|
||||
def get_schema(self, request=None, public=False):
|
||||
"""
|
||||
Generate a `coreapi.Document` representing the API schema.
|
||||
"""
|
||||
def _initialise_endpoints(self):
|
||||
if self.endpoints is None:
|
||||
inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf)
|
||||
self.endpoints = inspector.get_api_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
|
||||
)
|
||||
|
||||
def get_links(self, request=None):
|
||||
def _get_paths_and_endpoints(self, request):
|
||||
"""
|
||||
Return a dictionary containing all the links that should be
|
||||
included in the API schema.
|
||||
Generate (path, method, view) given (path, method, callback) for paths.
|
||||
"""
|
||||
links = LinkNode()
|
||||
|
||||
# Generate (path, method, view) given (path, method, callback).
|
||||
paths = []
|
||||
view_endpoints = []
|
||||
for path, method, callback in self.endpoints:
|
||||
|
@ -308,22 +193,48 @@ class SchemaGenerator(object):
|
|||
paths.append(path)
|
||||
view_endpoints.append((path, method, view))
|
||||
|
||||
# Only generate the path prefix for paths that will be included
|
||||
if not paths:
|
||||
return None
|
||||
prefix = self.determine_path_prefix(paths)
|
||||
return paths, view_endpoints
|
||||
|
||||
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)
|
||||
def create_view(self, callback, method, request=None):
|
||||
"""
|
||||
Given a callback, return an actual view instance.
|
||||
"""
|
||||
view = callback.cls(**getattr(callback, 'initkwargs', {}))
|
||||
view.args = ()
|
||||
view.kwargs = {}
|
||||
view.format_kwarg = None
|
||||
view.request = None
|
||||
view.action_map = getattr(callback, 'actions', None)
|
||||
|
||||
return links
|
||||
actions = getattr(callback, 'actions', None)
|
||||
if actions is not None:
|
||||
if method == 'OPTIONS':
|
||||
view.action = 'metadata'
|
||||
else:
|
||||
view.action = actions.get(method.lower())
|
||||
|
||||
# Methods used when we generate a view instance from the raw callback...
|
||||
if request is not None:
|
||||
view.request = clone_request(request, method)
|
||||
|
||||
return view
|
||||
|
||||
def coerce_path(self, path, method, view):
|
||||
"""
|
||||
Coerce {pk} path arguments into the name of the model field,
|
||||
where possible. This is cleaner for an external representation.
|
||||
(Ie. "this is an identifier", not "this is a database primary key")
|
||||
"""
|
||||
if not self.coerce_path_pk or '{pk}' not in path:
|
||||
return path
|
||||
model = getattr(getattr(view, 'queryset', None), 'model', None)
|
||||
if model:
|
||||
field_name = get_pk_name(model)
|
||||
else:
|
||||
field_name = 'id'
|
||||
return path.replace('{pk}', '{%s}' % field_name)
|
||||
|
||||
def get_schema(self, request=None, public=False):
|
||||
raise NotImplementedError(".get_schema() must be implemented in subclasses.")
|
||||
|
||||
def determine_path_prefix(self, paths):
|
||||
"""
|
||||
|
@ -356,29 +267,6 @@ class SchemaGenerator(object):
|
|||
prefixes.append('/' + prefix + '/')
|
||||
return common_path(prefixes)
|
||||
|
||||
def create_view(self, callback, method, request=None):
|
||||
"""
|
||||
Given a callback, return an actual view instance.
|
||||
"""
|
||||
view = callback.cls(**getattr(callback, 'initkwargs', {}))
|
||||
view.args = ()
|
||||
view.kwargs = {}
|
||||
view.format_kwarg = None
|
||||
view.request = None
|
||||
view.action_map = getattr(callback, 'actions', None)
|
||||
|
||||
actions = getattr(callback, 'actions', None)
|
||||
if actions is not None:
|
||||
if method == 'OPTIONS':
|
||||
view.action = 'metadata'
|
||||
else:
|
||||
view.action = actions.get(method.lower())
|
||||
|
||||
if request is not None:
|
||||
view.request = clone_request(request, method)
|
||||
|
||||
return view
|
||||
|
||||
def has_view_permissions(self, path, method, view):
|
||||
"""
|
||||
Return `True` if the incoming request has the correct view permissions.
|
||||
|
@ -391,64 +279,3 @@ class SchemaGenerator(object):
|
|||
except (exceptions.APIException, Http404, PermissionDenied):
|
||||
return False
|
||||
return True
|
||||
|
||||
def coerce_path(self, path, method, view):
|
||||
"""
|
||||
Coerce {pk} path arguments into the name of the model field,
|
||||
where possible. This is cleaner for an external representation.
|
||||
(Ie. "this is an identifier", not "this is a database primary key")
|
||||
"""
|
||||
if not self.coerce_path_pk or '{pk}' not in path:
|
||||
return path
|
||||
model = getattr(getattr(view, 'queryset', None), 'model', None)
|
||||
if model:
|
||||
field_name = get_pk_name(model)
|
||||
else:
|
||||
field_name = 'id'
|
||||
return path.replace('{pk}', '{%s}' % field_name)
|
||||
|
||||
# Method for generating the link layout....
|
||||
|
||||
def get_keys(self, subpath, method, view):
|
||||
"""
|
||||
Return a list of keys that should be used to layout a link within
|
||||
the schema document.
|
||||
|
||||
/users/ ("users", "list"), ("users", "create")
|
||||
/users/{pk}/ ("users", "read"), ("users", "update"), ("users", "delete")
|
||||
/users/enabled/ ("users", "enabled") # custom viewset list action
|
||||
/users/{pk}/star/ ("users", "star") # custom viewset detail action
|
||||
/users/{pk}/groups/ ("users", "groups", "list"), ("users", "groups", "create")
|
||||
/users/{pk}/groups/{pk}/ ("users", "groups", "read"), ("users", "groups", "update"), ("users", "groups", "delete")
|
||||
"""
|
||||
if hasattr(view, 'action'):
|
||||
# Viewsets have explicitly named actions.
|
||||
action = view.action
|
||||
else:
|
||||
# Views have no associated action, so we determine one from the method.
|
||||
if is_list_view(subpath, method, view):
|
||||
action = 'list'
|
||||
else:
|
||||
action = self.default_mapping[method.lower()]
|
||||
|
||||
named_path_components = [
|
||||
component for component
|
||||
in subpath.strip('/').split('/')
|
||||
if '{' not in component
|
||||
]
|
||||
|
||||
if is_custom_action(action):
|
||||
# Custom action, eg "/users/{pk}/activate/", "/users/active/"
|
||||
if len(view.action_map) > 1:
|
||||
action = self.default_mapping[method.lower()]
|
||||
if action in self.coerce_method_names:
|
||||
action = self.coerce_method_names[action]
|
||||
return named_path_components + [action]
|
||||
else:
|
||||
return named_path_components[:-1] + [action]
|
||||
|
||||
if action in self.coerce_method_names:
|
||||
action = self.coerce_method_names[action]
|
||||
|
||||
# Default action, eg "/users/", "/users/{pk}/"
|
||||
return named_path_components + [action]
|
||||
|
|
|
@ -1,129 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
inspectors.py # Per-endpoint view introspection
|
||||
|
||||
See schemas.__init__.py for package overview.
|
||||
"""
|
||||
import re
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
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.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):
|
||||
title = force_text(field.label) if field.label else ''
|
||||
description = force_text(field.help_text) if field.help_text else ''
|
||||
|
||||
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
|
||||
child_schema = field_to_schema(field.child)
|
||||
return coreschema.Array(
|
||||
items=child_schema,
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.DictField):
|
||||
return coreschema.Object(
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.Serializer):
|
||||
return coreschema.Object(
|
||||
properties=OrderedDict([
|
||||
(key, field_to_schema(value))
|
||||
for key, value
|
||||
in field.fields.items()
|
||||
]),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.ManyRelatedField):
|
||||
return coreschema.Array(
|
||||
items=coreschema.String(),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
schema_cls = coreschema.String
|
||||
model = getattr(field.queryset, 'model', None)
|
||||
if model is not None:
|
||||
model_field = model._meta.pk
|
||||
if isinstance(model_field, models.AutoField):
|
||||
schema_cls = coreschema.Integer
|
||||
return schema_cls(title=title, description=description)
|
||||
elif isinstance(field, serializers.RelatedField):
|
||||
return coreschema.String(title=title, description=description)
|
||||
elif isinstance(field, serializers.MultipleChoiceField):
|
||||
return coreschema.Array(
|
||||
items=coreschema.Enum(enum=list(field.choices)),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.ChoiceField):
|
||||
return coreschema.Enum(
|
||||
enum=list(field.choices),
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.BooleanField):
|
||||
return coreschema.Boolean(title=title, description=description)
|
||||
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
|
||||
return coreschema.Number(title=title, description=description)
|
||||
elif isinstance(field, serializers.IntegerField):
|
||||
return coreschema.Integer(title=title, description=description)
|
||||
elif isinstance(field, serializers.DateField):
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='date'
|
||||
)
|
||||
elif isinstance(field, serializers.DateTimeField):
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='date-time'
|
||||
)
|
||||
elif isinstance(field, serializers.JSONField):
|
||||
return coreschema.Object(title=title, description=description)
|
||||
|
||||
if field.style.get('base_template') == 'textarea.html':
|
||||
return coreschema.String(
|
||||
title=title,
|
||||
description=description,
|
||||
format='textarea'
|
||||
)
|
||||
|
||||
return coreschema.String(title=title, description=description)
|
||||
|
||||
|
||||
def get_pk_description(model, model_field):
|
||||
if isinstance(model_field, models.AutoField):
|
||||
value_type = _('unique integer value')
|
||||
elif isinstance(model_field, models.UUIDField):
|
||||
value_type = _('UUID string')
|
||||
else:
|
||||
value_type = _('unique value')
|
||||
|
||||
return _('A {value_type} identifying this {name}.').format(
|
||||
value_type=value_type,
|
||||
name=model._meta.verbose_name,
|
||||
)
|
||||
|
||||
|
||||
class ViewInspector(object):
|
||||
class ViewInspector:
|
||||
"""
|
||||
Descriptor class on APIView.
|
||||
|
||||
|
@ -177,326 +62,11 @@ class ViewInspector(object):
|
|||
def view(self):
|
||||
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):
|
||||
"""Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting"""
|
||||
def __get__(self, instance, owner):
|
||||
result = super(DefaultSchema, self).__get__(instance, owner)
|
||||
result = super().__get__(instance, owner)
|
||||
if not isinstance(result, DefaultSchema):
|
||||
return result
|
||||
|
||||
|
|
377
rest_framework/schemas/openapi.py
Normal file
377
rest_framework/schemas/openapi.py
Normal file
|
@ -0,0 +1,377 @@
|
|||
import warnings
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import uritemplate
|
||||
|
||||
from .generators import BaseSchemaGenerator
|
||||
from .inspectors import ViewInspector
|
||||
from .utils import get_pk_description, is_list_view
|
||||
|
||||
# Generator
|
||||
|
||||
|
||||
class SchemaGenerator(BaseSchemaGenerator):
|
||||
|
||||
def get_info(self):
|
||||
info = {
|
||||
'title': self.title,
|
||||
'version': 'TODO',
|
||||
}
|
||||
|
||||
if self.description is not None:
|
||||
info['description'] = self.description
|
||||
|
||||
return info
|
||||
|
||||
def get_paths(self, request=None):
|
||||
result = {}
|
||||
|
||||
paths, view_endpoints = self._get_paths_and_endpoints(request)
|
||||
|
||||
# Only generate the path prefix for paths that will be included
|
||||
if not paths:
|
||||
return None
|
||||
prefix = self.determine_path_prefix(paths)
|
||||
|
||||
for path, method, view in view_endpoints:
|
||||
if not self.has_view_permissions(path, method, view):
|
||||
continue
|
||||
operation = view.schema.get_operation(path, method)
|
||||
subpath = '/' + path[len(prefix):]
|
||||
result.setdefault(subpath, {})
|
||||
result[subpath][method.lower()] = operation
|
||||
|
||||
return result
|
||||
|
||||
def get_schema(self, request=None, public=False):
|
||||
"""
|
||||
Generate a OpenAPI schema.
|
||||
"""
|
||||
self._initialise_endpoints()
|
||||
|
||||
paths = self.get_paths(None if public else request)
|
||||
if not paths:
|
||||
return None
|
||||
|
||||
schema = {
|
||||
'openapi': '3.0.2',
|
||||
'info': self.get_info(),
|
||||
'paths': paths,
|
||||
}
|
||||
|
||||
return schema
|
||||
|
||||
# View Inspectors
|
||||
|
||||
|
||||
class AutoSchema(ViewInspector):
|
||||
|
||||
content_types = ['application/json']
|
||||
method_mapping = {
|
||||
'get': 'Retrieve',
|
||||
'post': 'Create',
|
||||
'put': 'Update',
|
||||
'patch': 'PartialUpdate',
|
||||
'delete': 'Destroy',
|
||||
}
|
||||
|
||||
def get_operation(self, path, method):
|
||||
operation = {}
|
||||
|
||||
operation['operationId'] = self._get_operation_id(path, method)
|
||||
|
||||
parameters = []
|
||||
parameters += self._get_path_parameters(path, method)
|
||||
parameters += self._get_pagination_parameters(path, method)
|
||||
parameters += self._get_filter_parameters(path, method)
|
||||
operation['parameters'] = parameters
|
||||
|
||||
request_body = self._get_request_body(path, method)
|
||||
if request_body:
|
||||
operation['requestBody'] = request_body
|
||||
operation['responses'] = self._get_responses(path, method)
|
||||
|
||||
return operation
|
||||
|
||||
def _get_operation_id(self, path, method):
|
||||
"""
|
||||
Compute an operation ID from the model, serializer or view name.
|
||||
"""
|
||||
method_name = getattr(self.view, 'action', method.lower())
|
||||
if is_list_view(path, method, self.view):
|
||||
action = 'List'
|
||||
elif method_name not in self.method_mapping:
|
||||
action = method_name
|
||||
else:
|
||||
action = self.method_mapping[method.lower()]
|
||||
|
||||
# Try to deduce the ID from the view's model
|
||||
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
|
||||
if model is not None:
|
||||
name = model.__name__
|
||||
|
||||
# Try with the serializer class name
|
||||
elif hasattr(self.view, 'get_serializer_class'):
|
||||
name = self.view.get_serializer_class().__name__
|
||||
if name.endswith('Serializer'):
|
||||
name = name[:-10]
|
||||
|
||||
# Fallback to the view name
|
||||
else:
|
||||
name = self.view.__class__.__name__
|
||||
if name.endswith('APIView'):
|
||||
name = name[:-7]
|
||||
elif name.endswith('View'):
|
||||
name = name[:-4]
|
||||
if name.endswith(action): # ListView, UpdateAPIView, ThingDelete ...
|
||||
name = name[:-len(action)]
|
||||
|
||||
if action == 'List' and not name.endswith('s'): # ListThings instead of ListThing
|
||||
name += 's'
|
||||
|
||||
return action + name
|
||||
|
||||
def _get_path_parameters(self, path, method):
|
||||
"""
|
||||
Return a list of parameters from templated path variables.
|
||||
"""
|
||||
assert uritemplate, '`uritemplate` must be installed for OpenAPI schema support.'
|
||||
|
||||
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
|
||||
parameters = []
|
||||
|
||||
for variable in uritemplate.variables(path):
|
||||
description = ''
|
||||
if model is not None: # TODO: test this.
|
||||
# Attempt to infer a field description if possible.
|
||||
try:
|
||||
model_field = model._meta.get_field(variable)
|
||||
except Exception:
|
||||
model_field = None
|
||||
|
||||
if model_field is not None and model_field.help_text:
|
||||
description = force_text(model_field.help_text)
|
||||
elif model_field is not None and model_field.primary_key:
|
||||
description = get_pk_description(model, model_field)
|
||||
|
||||
parameter = {
|
||||
"name": variable,
|
||||
"in": "path",
|
||||
"required": True,
|
||||
"description": description,
|
||||
'schema': {
|
||||
'type': 'string', # TODO: integer, pattern, ...
|
||||
},
|
||||
}
|
||||
parameters.append(parameter)
|
||||
|
||||
return parameters
|
||||
|
||||
def _get_filter_parameters(self, path, method):
|
||||
if not self._allows_filters(path, method):
|
||||
return []
|
||||
parameters = []
|
||||
for filter_backend in self.view.filter_backends:
|
||||
parameters += filter_backend().get_schema_operation_parameters(self.view)
|
||||
return parameters
|
||||
|
||||
def _allows_filters(self, path, method):
|
||||
"""
|
||||
Determine whether to include filter Fields in schema.
|
||||
|
||||
Default implementation looks for ModelViewSet or GenericAPIView
|
||||
actions/methods that cause filtering on the default implementation.
|
||||
"""
|
||||
if getattr(self.view, 'filter_backends', None) is None:
|
||||
return False
|
||||
if hasattr(self.view, 'action'):
|
||||
return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
|
||||
return method.lower() in ["get", "put", "patch", "delete"]
|
||||
|
||||
def _get_pagination_parameters(self, path, method):
|
||||
view = self.view
|
||||
|
||||
if not is_list_view(path, method, view):
|
||||
return []
|
||||
|
||||
pagination = getattr(view, 'pagination_class', None)
|
||||
if not pagination:
|
||||
return []
|
||||
|
||||
paginator = view.pagination_class()
|
||||
return paginator.get_schema_operation_parameters(view)
|
||||
|
||||
def _map_field(self, field):
|
||||
|
||||
# Nested Serializers, `many` or not.
|
||||
if isinstance(field, serializers.ListSerializer):
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': self._map_serializer(field.child)
|
||||
}
|
||||
if isinstance(field, serializers.Serializer):
|
||||
data = self._map_serializer(field)
|
||||
data['type'] = 'object'
|
||||
return data
|
||||
|
||||
# Related fields.
|
||||
if isinstance(field, serializers.ManyRelatedField):
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': self._map_field(field.child_relation)
|
||||
}
|
||||
if isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
model = getattr(field.queryset, 'model', None)
|
||||
if model is not None:
|
||||
model_field = model._meta.pk
|
||||
if isinstance(model_field, models.AutoField):
|
||||
return {'type': 'integer'}
|
||||
|
||||
# ChoiceFields (single and multiple).
|
||||
# Q:
|
||||
# - Is 'type' required?
|
||||
# - can we determine the TYPE of a choicefield?
|
||||
if isinstance(field, serializers.MultipleChoiceField):
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'enum': list(field.choices)
|
||||
},
|
||||
}
|
||||
|
||||
if isinstance(field, serializers.ChoiceField):
|
||||
return {
|
||||
'enum': list(field.choices),
|
||||
}
|
||||
|
||||
# ListField.
|
||||
if isinstance(field, serializers.ListField):
|
||||
return {
|
||||
'type': 'array',
|
||||
}
|
||||
|
||||
# DateField and DateTimeField type is string
|
||||
if isinstance(field, serializers.DateField):
|
||||
return {
|
||||
'type': 'string',
|
||||
'format': 'date',
|
||||
}
|
||||
|
||||
if isinstance(field, serializers.DateTimeField):
|
||||
return {
|
||||
'type': 'string',
|
||||
'format': 'date-time',
|
||||
}
|
||||
|
||||
# Simplest cases, default to 'string' type:
|
||||
FIELD_CLASS_SCHEMA_TYPE = {
|
||||
serializers.BooleanField: 'boolean',
|
||||
serializers.DecimalField: 'number',
|
||||
serializers.FloatField: 'number',
|
||||
serializers.IntegerField: 'integer',
|
||||
|
||||
serializers.JSONField: 'object',
|
||||
serializers.DictField: 'object',
|
||||
}
|
||||
return {'type': FIELD_CLASS_SCHEMA_TYPE.get(field.__class__, 'string')}
|
||||
|
||||
def _map_serializer(self, serializer):
|
||||
# Assuming we have a valid serializer instance.
|
||||
# TODO:
|
||||
# - field is Nested or List serializer.
|
||||
# - Handle read_only/write_only for request/response differences.
|
||||
# - could do this with readOnly/writeOnly and then filter dict.
|
||||
required = []
|
||||
properties = {}
|
||||
|
||||
for field in serializer.fields.values():
|
||||
if isinstance(field, serializers.HiddenField):
|
||||
continue
|
||||
|
||||
if field.required:
|
||||
required.append(field.field_name)
|
||||
|
||||
schema = self._map_field(field)
|
||||
if field.read_only:
|
||||
schema['readOnly'] = True
|
||||
if field.write_only:
|
||||
schema['writeOnly'] = True
|
||||
if field.allow_null:
|
||||
schema['nullable'] = True
|
||||
|
||||
properties[field.field_name] = schema
|
||||
return {
|
||||
'required': required,
|
||||
'properties': properties,
|
||||
}
|
||||
|
||||
def _get_request_body(self, path, method):
|
||||
view = self.view
|
||||
|
||||
if method not in ('PUT', 'PATCH', 'POST'):
|
||||
return {}
|
||||
|
||||
if not hasattr(view, 'get_serializer'):
|
||||
return {}
|
||||
|
||||
try:
|
||||
serializer = view.get_serializer()
|
||||
except exceptions.APIException:
|
||||
serializer = None
|
||||
warnings.warn('{}.get_serializer() raised an exception during '
|
||||
'schema generation. Serializer fields will not be '
|
||||
'generated for {} {}.'
|
||||
.format(view.__class__.__name__, method, path))
|
||||
|
||||
if not isinstance(serializer, serializers.Serializer):
|
||||
return {}
|
||||
|
||||
content = self._map_serializer(serializer)
|
||||
# No required fields for PATCH
|
||||
if method == 'PATCH':
|
||||
del content['required']
|
||||
# No read_only fields for request.
|
||||
for name, schema in content['properties'].copy().items():
|
||||
if 'readOnly' in schema:
|
||||
del content['properties'][name]
|
||||
|
||||
return {
|
||||
'content': {
|
||||
ct: {'schema': content}
|
||||
for ct in self.content_types
|
||||
}
|
||||
}
|
||||
|
||||
def _get_responses(self, path, method):
|
||||
# TODO: Handle multiple codes.
|
||||
content = {}
|
||||
view = self.view
|
||||
if hasattr(view, 'get_serializer'):
|
||||
try:
|
||||
serializer = view.get_serializer()
|
||||
except exceptions.APIException:
|
||||
serializer = None
|
||||
warnings.warn('{}.get_serializer() raised an exception during '
|
||||
'schema generation. Serializer fields will not be '
|
||||
'generated for {} {}.'
|
||||
.format(view.__class__.__name__, method, path))
|
||||
|
||||
if isinstance(serializer, serializers.Serializer):
|
||||
content = self._map_serializer(serializer)
|
||||
# No write_only fields for response.
|
||||
for name, schema in content['properties'].copy().items():
|
||||
if 'writeOnly' in schema:
|
||||
del content['properties'][name]
|
||||
content['required'] = [f for f in content['required'] if f != name]
|
||||
|
||||
return {
|
||||
'200': {
|
||||
'content': {
|
||||
ct: {'schema': content}
|
||||
for ct in self.content_types
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@ utils.py # Shared helper functions
|
|||
|
||||
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
|
||||
|
||||
|
||||
|
@ -22,3 +25,17 @@ def is_list_view(path, method, view):
|
|||
if path_components and '{' in path_components[-1]:
|
||||
return False
|
||||
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,
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ See schemas.__init__.py for package overview.
|
|||
"""
|
||||
from rest_framework import exceptions, renderers
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.schemas import coreapi
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
@ -17,12 +18,18 @@ class SchemaView(APIView):
|
|||
public = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SchemaView, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.renderer_classes is None:
|
||||
self.renderer_classes = [
|
||||
renderers.OpenAPIRenderer,
|
||||
renderers.CoreJSONRenderer
|
||||
]
|
||||
if coreapi.is_enabled():
|
||||
self.renderer_classes = [
|
||||
renderers.CoreAPIOpenAPIRenderer,
|
||||
renderers.CoreJSONRenderer
|
||||
]
|
||||
else:
|
||||
self.renderer_classes = [
|
||||
renderers.OpenAPIRenderer,
|
||||
renderers.JSONOpenAPIRenderer,
|
||||
]
|
||||
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
|
||||
self.renderer_classes += [renderers.BrowsableAPIRenderer]
|
||||
|
||||
|
@ -38,4 +45,4 @@ class SchemaView(APIView):
|
|||
self.renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||
neg = self.perform_content_negotiation(self.request, force=True)
|
||||
self.request.accepted_renderer, self.request.accepted_media_type = neg
|
||||
return super(SchemaView, self).handle_exception(exc)
|
||||
return super().handle_exception(exc)
|
||||
|
|
|
@ -10,12 +10,11 @@ python primitives.
|
|||
2. The process of marshalling between python primitives and request and
|
||||
response content is handled by parsers and renderers.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
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.fields import Field as DjangoModelField
|
||||
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.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.fields import get_error_detail, set_value
|
||||
from rest_framework.settings import api_settings
|
||||
|
@ -115,14 +114,14 @@ class BaseSerializer(Field):
|
|||
self.partial = kwargs.pop('partial', False)
|
||||
self._context = kwargs.pop('context', {})
|
||||
kwargs.pop('many', None)
|
||||
super(BaseSerializer, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We override this method in order to automagically create
|
||||
# `ListSerializer` classes instead when `many=True` is set.
|
||||
if kwargs.pop('many', False):
|
||||
return cls.many_init(*args, **kwargs)
|
||||
return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
|
||||
return super().__new__(cls, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
|
@ -315,7 +314,7 @@ class SerializerMetaclass(type):
|
|||
|
||||
def __new__(cls, name, 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):
|
||||
|
@ -344,13 +343,12 @@ def as_serializer_error(exc):
|
|||
}
|
||||
|
||||
|
||||
@six.add_metaclass(SerializerMetaclass)
|
||||
class Serializer(BaseSerializer):
|
||||
class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
|
||||
default_error_messages = {
|
||||
'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
|
||||
}
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def fields(self):
|
||||
"""
|
||||
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
|
||||
# have issues importing modules that use ModelSerializers as fields,
|
||||
# even if Django's app-loading stage has not yet run.
|
||||
if not hasattr(self, '_fields'):
|
||||
self._fields = BindingDict(self)
|
||||
for key, value in self.get_fields().items():
|
||||
self._fields[key] = value
|
||||
return self._fields
|
||||
fields = BindingDict(self)
|
||||
for key, value in self.get_fields().items():
|
||||
fields[key] = value
|
||||
return fields
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def _writable_fields(self):
|
||||
return [
|
||||
field for field in self.fields.values() if not field.read_only
|
||||
]
|
||||
for field in self.fields.values():
|
||||
if not field.read_only:
|
||||
yield field
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def _readable_fields(self):
|
||||
return [
|
||||
field for field in self.fields.values()
|
||||
if not field.write_only
|
||||
]
|
||||
for field in self.fields.values():
|
||||
if not field.write_only:
|
||||
yield field
|
||||
|
||||
def get_fields(self):
|
||||
"""
|
||||
|
@ -466,7 +462,7 @@ class Serializer(BaseSerializer):
|
|||
to_validate.update(value)
|
||||
else:
|
||||
to_validate = value
|
||||
super(Serializer, self).run_validators(to_validate)
|
||||
super().run_validators(to_validate)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
|
@ -535,7 +531,7 @@ class Serializer(BaseSerializer):
|
|||
return attrs
|
||||
|
||||
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
|
||||
# serializer, for the purposes of presenting a form-like API onto the
|
||||
|
@ -560,12 +556,12 @@ class Serializer(BaseSerializer):
|
|||
|
||||
@property
|
||||
def data(self):
|
||||
ret = super(Serializer, self).data
|
||||
ret = super().data
|
||||
return ReturnDict(ret, serializer=self)
|
||||
|
||||
@property
|
||||
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':
|
||||
# Edge case. Provide a more descriptive error than
|
||||
# "this field may not be null", when no data is passed.
|
||||
|
@ -591,11 +587,11 @@ class ListSerializer(BaseSerializer):
|
|||
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||
assert self.child is not None, '`child` is a required argument.'
|
||||
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)
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
super(ListSerializer, self).bind(field_name, parent)
|
||||
super().bind(field_name, parent)
|
||||
self.partial = self.parent.partial
|
||||
|
||||
def get_initial(self):
|
||||
|
@ -758,19 +754,19 @@ class ListSerializer(BaseSerializer):
|
|||
return not bool(self._errors)
|
||||
|
||||
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.
|
||||
# Allows renderers such as HTMLFormRenderer to get the full field info.
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
ret = super(ListSerializer, self).data
|
||||
ret = super().data
|
||||
return ReturnList(ret, serializer=self)
|
||||
|
||||
@property
|
||||
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':
|
||||
# Edge case. Provide a more descriptive error than
|
||||
# "this field may not be null", when no data is passed.
|
||||
|
@ -1236,6 +1232,11 @@ class ModelSerializer(Serializer):
|
|||
# `allow_blank` is only valid for textual fields.
|
||||
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):
|
||||
# Populate the `child` argument on `ListField` instances generated
|
||||
# for the PostgreSQL specific `ArrayField`.
|
||||
|
|
|
@ -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
|
||||
back to the defaults.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
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
|
||||
|
||||
|
@ -56,7 +52,7 @@ DEFAULTS = {
|
|||
'DEFAULT_FILTER_BACKENDS': (),
|
||||
|
||||
# Schema
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema',
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema',
|
||||
|
||||
# Throttling
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
|
@ -166,7 +162,7 @@ def perform_import(val, setting_name):
|
|||
"""
|
||||
if val is None:
|
||||
return None
|
||||
elif isinstance(val, six.string_types):
|
||||
elif isinstance(val, str):
|
||||
return import_from_string(val, setting_name)
|
||||
elif isinstance(val, (list, tuple)):
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
# Nod to tastypie's use of importlib.
|
||||
module_path, class_name = val.rsplit('.', 1)
|
||||
module = import_module(module_path)
|
||||
return getattr(module, class_name)
|
||||
except (ImportError, AttributeError) as e:
|
||||
return import_string(val)
|
||||
except ImportError as e:
|
||||
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
|
||||
raise ImportError(msg)
|
||||
|
||||
|
||||
class APISettings(object):
|
||||
class APISettings:
|
||||
"""
|
||||
A settings object, that allows API settings to be accessed as properties.
|
||||
For example:
|
||||
|
|
|
@ -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 4918 - https://tools.ietf.org/html/rfc4918
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
def is_informational(code):
|
||||
|
@ -38,6 +37,8 @@ HTTP_204_NO_CONTENT = 204
|
|||
HTTP_205_RESET_CONTENT = 205
|
||||
HTTP_206_PARTIAL_CONTENT = 206
|
||||
HTTP_207_MULTI_STATUS = 207
|
||||
HTTP_208_ALREADY_REPORTED = 208
|
||||
HTTP_226_IM_USED = 226
|
||||
HTTP_300_MULTIPLE_CHOICES = 300
|
||||
HTTP_301_MOVED_PERMANENTLY = 301
|
||||
HTTP_302_FOUND = 302
|
||||
|
@ -46,6 +47,7 @@ HTTP_304_NOT_MODIFIED = 304
|
|||
HTTP_305_USE_PROXY = 305
|
||||
HTTP_306_RESERVED = 306
|
||||
HTTP_307_TEMPORARY_REDIRECT = 307
|
||||
HTTP_308_PERMANENT_REDIRECT = 308
|
||||
HTTP_400_BAD_REQUEST = 400
|
||||
HTTP_401_UNAUTHORIZED = 401
|
||||
HTTP_402_PAYMENT_REQUIRED = 402
|
||||
|
@ -67,6 +69,7 @@ HTTP_417_EXPECTATION_FAILED = 417
|
|||
HTTP_422_UNPROCESSABLE_ENTITY = 422
|
||||
HTTP_423_LOCKED = 423
|
||||
HTTP_424_FAILED_DEPENDENCY = 424
|
||||
HTTP_426_UPGRADE_REQUIRED = 426
|
||||
HTTP_428_PRECONDITION_REQUIRED = 428
|
||||
HTTP_429_TOO_MANY_REQUESTS = 429
|
||||
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
||||
|
@ -77,5 +80,9 @@ HTTP_502_BAD_GATEWAY = 502
|
|||
HTTP_503_SERVICE_UNAVAILABLE = 503
|
||||
HTTP_504_GATEWAY_TIMEOUT = 504
|
||||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
|
||||
HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
|
||||
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
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<div class="modal-body">
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<h4 class="text-center">You are logged in as {{ user.username }}.</h4>
|
||||
<h4 class="text-center">You are logged in as {{ user.get_username }}.</h4>
|
||||
{% else %}
|
||||
|
||||
<div class="text-center">
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import template
|
||||
from django.template import loader
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text, iri_to_uri
|
||||
from django.utils.html import escape, format_html, smart_urlquote
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
|
@ -187,7 +184,7 @@ def add_class(value, css_class):
|
|||
In the case of REST Framework, the filter is used to add Bootstrap-specific
|
||||
classes to the forms.
|
||||
"""
|
||||
html = six.text_type(value)
|
||||
html = str(value)
|
||||
match = class_re.search(html)
|
||||
if match:
|
||||
m = re.search(r'^%s$|^%s\s|\s%s\s|\s%s$' % (css_class, css_class,
|
||||
|
@ -204,7 +201,7 @@ def add_class(value, css_class):
|
|||
@register.filter
|
||||
def format_value(value):
|
||||
if getattr(value, 'is_hyperlink', False):
|
||||
name = six.text_type(value.obj)
|
||||
name = str(value.obj)
|
||||
return mark_safe('<a href=%s>%s</a>' % (value, escape(name)))
|
||||
if value is None or isinstance(value, bool):
|
||||
return mark_safe('<code>%s</code>' % {True: 'true', False: 'false', None: 'null'}[value])
|
||||
|
@ -219,7 +216,7 @@ def format_value(value):
|
|||
template = loader.get_template('rest_framework/admin/dict_value.html')
|
||||
context = {'value': value}
|
||||
return template.render(context)
|
||||
elif isinstance(value, six.string_types):
|
||||
elif isinstance(value, str):
|
||||
if (
|
||||
(value.startswith('http:') or value.startswith('https:')) and not
|
||||
re.search(r'\s', value)
|
||||
|
@ -229,7 +226,7 @@ def format_value(value):
|
|||
return mark_safe('<a href="mailto:{value}">{value}</a>'.format(value=escape(value)))
|
||||
elif '\n' in value:
|
||||
return mark_safe('<pre>%s</pre>' % escape(value))
|
||||
return six.text_type(value)
|
||||
return str(value)
|
||||
|
||||
|
||||
@register.filter
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
# -- coding: utf-8 --
|
||||
|
||||
# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order
|
||||
# to make it harder for the user to import the wrong thing without realizing.
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import io
|
||||
from importlib import import_module
|
||||
|
||||
|
@ -14,7 +10,6 @@ from django.test import override_settings, testcases
|
|||
from django.test.client import Client as DjangoClient
|
||||
from django.test.client import ClientHandler
|
||||
from django.test.client import RequestFactory as DjangoRequestFactory
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlencode
|
||||
|
||||
|
@ -32,7 +27,7 @@ if requests is not None:
|
|||
def get_all(self, key, default):
|
||||
return self.getheaders(key)
|
||||
|
||||
class MockOriginalResponse(object):
|
||||
class MockOriginalResponse:
|
||||
def __init__(self, headers):
|
||||
self.msg = HeaderDict(headers)
|
||||
self.closed = False
|
||||
|
@ -109,7 +104,7 @@ if requests is not None:
|
|||
|
||||
class RequestsClient(requests.Session):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RequestsClient, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
adapter = DjangoTestAdapter()
|
||||
self.mount('http://', adapter)
|
||||
self.mount('https://', adapter)
|
||||
|
@ -117,7 +112,7 @@ if requests is not None:
|
|||
def request(self, method, url, *args, **kwargs):
|
||||
if not url.startswith('http'):
|
||||
raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url)
|
||||
return super(RequestsClient, self).request(method, url, *args, **kwargs)
|
||||
return super().request(method, url, *args, **kwargs)
|
||||
|
||||
else:
|
||||
def RequestsClient(*args, **kwargs):
|
||||
|
@ -129,7 +124,7 @@ if coreapi is not None:
|
|||
def __init__(self, *args, **kwargs):
|
||||
self._session = RequestsClient()
|
||||
kwargs['transports'] = [coreapi.transports.HTTPTransport(session=self.session)]
|
||||
return super(CoreAPIClient, self).__init__(*args, **kwargs)
|
||||
return super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
|
@ -149,7 +144,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
self.renderer_classes = {}
|
||||
for cls in self.renderer_classes_list:
|
||||
self.renderer_classes[cls.format] = cls
|
||||
super(APIRequestFactory, self).__init__(**defaults)
|
||||
super().__init__(**defaults)
|
||||
|
||||
def _encode_data(self, data, format=None, content_type=None):
|
||||
"""
|
||||
|
@ -171,7 +166,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
format = format or self.default_format
|
||||
|
||||
assert format in self.renderer_classes, (
|
||||
"Invalid format '{0}'. Available formats are {1}. "
|
||||
"Invalid format '{}'. Available formats are {}. "
|
||||
"Set TEST_REQUEST_RENDERER_CLASSES to enable "
|
||||
"extra request formats.".format(
|
||||
format,
|
||||
|
@ -184,13 +179,13 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
ret = renderer.render(data)
|
||||
|
||||
# Determine the content-type header from the renderer
|
||||
content_type = "{0}; charset={1}".format(
|
||||
content_type = "{}; charset={}".format(
|
||||
renderer.media_type, renderer.charset
|
||||
)
|
||||
|
||||
# Coerce text to bytes if required.
|
||||
if isinstance(ret, six.text_type):
|
||||
ret = bytes(ret.encode(renderer.charset))
|
||||
if isinstance(ret, str):
|
||||
ret = ret.encode(renderer.charset)
|
||||
|
||||
return ret, content_type
|
||||
|
||||
|
@ -202,8 +197,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
# Fix to support old behavior where you have the arguments in the
|
||||
# url. See #1461.
|
||||
query_string = force_bytes(path.split('?')[1])
|
||||
if six.PY3:
|
||||
query_string = query_string.decode('iso-8859-1')
|
||||
query_string = query_string.decode('iso-8859-1')
|
||||
r['QUERY_STRING'] = query_string
|
||||
r.update(extra)
|
||||
return self.generic('GET', path, **r)
|
||||
|
@ -234,11 +228,11 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
if content_type is not None:
|
||||
extra['CONTENT_TYPE'] = str(content_type)
|
||||
|
||||
return super(APIRequestFactory, self).generic(
|
||||
return super().generic(
|
||||
method, path, data, content_type, secure, **extra)
|
||||
|
||||
def request(self, **kwargs):
|
||||
request = super(APIRequestFactory, self).request(**kwargs)
|
||||
request = super().request(**kwargs)
|
||||
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
|
||||
return request
|
||||
|
||||
|
@ -252,18 +246,18 @@ class ForceAuthClientHandler(ClientHandler):
|
|||
def __init__(self, *args, **kwargs):
|
||||
self._force_user = None
|
||||
self._force_token = None
|
||||
super(ForceAuthClientHandler, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_response(self, request):
|
||||
# This is the simplest place we can hook into to patch the
|
||||
# request object.
|
||||
force_authenticate(request, self._force_user, self._force_token)
|
||||
return super(ForceAuthClientHandler, self).get_response(request)
|
||||
return super().get_response(request)
|
||||
|
||||
|
||||
class APIClient(APIRequestFactory, DjangoClient):
|
||||
def __init__(self, enforce_csrf_checks=False, **defaults):
|
||||
super(APIClient, self).__init__(**defaults)
|
||||
super().__init__(**defaults)
|
||||
self.handler = ForceAuthClientHandler(enforce_csrf_checks)
|
||||
self._credentials = {}
|
||||
|
||||
|
@ -286,17 +280,17 @@ class APIClient(APIRequestFactory, DjangoClient):
|
|||
def request(self, **kwargs):
|
||||
# Ensure that any credentials set get added to every request.
|
||||
kwargs.update(self._credentials)
|
||||
return super(APIClient, self).request(**kwargs)
|
||||
return super().request(**kwargs)
|
||||
|
||||
def get(self, path, data=None, follow=False, **extra):
|
||||
response = super(APIClient, self).get(path, data=data, **extra)
|
||||
response = super().get(path, data=data, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(response, **extra)
|
||||
return response
|
||||
|
||||
def post(self, path, data=None, format=None, content_type=None,
|
||||
follow=False, **extra):
|
||||
response = super(APIClient, self).post(
|
||||
response = super().post(
|
||||
path, data=data, format=format, content_type=content_type, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(response, **extra)
|
||||
|
@ -304,7 +298,7 @@ class APIClient(APIRequestFactory, DjangoClient):
|
|||
|
||||
def put(self, path, data=None, format=None, content_type=None,
|
||||
follow=False, **extra):
|
||||
response = super(APIClient, self).put(
|
||||
response = super().put(
|
||||
path, data=data, format=format, content_type=content_type, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(response, **extra)
|
||||
|
@ -312,7 +306,7 @@ class APIClient(APIRequestFactory, DjangoClient):
|
|||
|
||||
def patch(self, path, data=None, format=None, content_type=None,
|
||||
follow=False, **extra):
|
||||
response = super(APIClient, self).patch(
|
||||
response = super().patch(
|
||||
path, data=data, format=format, content_type=content_type, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(response, **extra)
|
||||
|
@ -320,7 +314,7 @@ class APIClient(APIRequestFactory, DjangoClient):
|
|||
|
||||
def delete(self, path, data=None, format=None, content_type=None,
|
||||
follow=False, **extra):
|
||||
response = super(APIClient, self).delete(
|
||||
response = super().delete(
|
||||
path, data=data, format=format, content_type=content_type, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(response, **extra)
|
||||
|
@ -328,7 +322,7 @@ class APIClient(APIRequestFactory, DjangoClient):
|
|||
|
||||
def options(self, path, data=None, format=None, content_type=None,
|
||||
follow=False, **extra):
|
||||
response = super(APIClient, self).options(
|
||||
response = super().options(
|
||||
path, data=data, format=format, content_type=content_type, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(response, **extra)
|
||||
|
@ -342,7 +336,7 @@ class APIClient(APIRequestFactory, DjangoClient):
|
|||
self.handler._force_token = None
|
||||
|
||||
if self.session:
|
||||
super(APIClient, self).logout()
|
||||
super().logout()
|
||||
|
||||
|
||||
class APITransactionTestCase(testcases.TransactionTestCase):
|
||||
|
@ -389,11 +383,11 @@ class URLPatternsTestCase(testcases.SimpleTestCase):
|
|||
cls._module.urlpatterns = cls.urlpatterns
|
||||
|
||||
cls._override.enable()
|
||||
super(URLPatternsTestCase, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(URLPatternsTestCase, cls).tearDownClass()
|
||||
super().tearDownClass()
|
||||
cls._override.disable()
|
||||
|
||||
if hasattr(cls, '_module_urlpatterns'):
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
Provides various throttling policies.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import time
|
||||
|
||||
from django.core.cache import cache as default_cache
|
||||
|
@ -11,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
class BaseThrottle(object):
|
||||
class BaseThrottle:
|
||||
"""
|
||||
Rate throttling of requests.
|
||||
"""
|
||||
|
@ -232,7 +230,7 @@ class ScopedRateThrottle(SimpleRateThrottle):
|
|||
self.num_requests, self.duration = self.parse_rate(self.rate)
|
||||
|
||||
# We can now proceed as normal.
|
||||
return super(ScopedRateThrottle, self).allow_request(request, view)
|
||||
return super().allow_request(request, view)
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from rest_framework.compat import (
|
||||
|
|
|
@ -11,8 +11,6 @@ your API requires authentication:
|
|||
|
||||
You should make sure your authentication settings include `SessionAuthentication`.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib.auth import views
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls import get_script_prefix, resolve
|
||||
|
||||
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
"""
|
||||
Helper classes for parsers.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
import json # noqa
|
||||
import uuid
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils import six, timezone
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import Promise
|
||||
|
||||
|
@ -39,17 +37,17 @@ class JSONEncoder(json.JSONEncoder):
|
|||
representation = obj.isoformat()
|
||||
return representation
|
||||
elif isinstance(obj, datetime.timedelta):
|
||||
return six.text_type(obj.total_seconds())
|
||||
return str(obj.total_seconds())
|
||||
elif isinstance(obj, decimal.Decimal):
|
||||
# Serializers will coerce decimals to strings by default.
|
||||
return float(obj)
|
||||
elif isinstance(obj, uuid.UUID):
|
||||
return six.text_type(obj)
|
||||
return str(obj)
|
||||
elif isinstance(obj, QuerySet):
|
||||
return tuple(obj)
|
||||
elif isinstance(obj, bytes):
|
||||
# Best-effort for binary blobs. See #4187.
|
||||
return obj.decode('utf-8')
|
||||
return obj.decode()
|
||||
elif hasattr(obj, 'tolist'):
|
||||
# Numpy arrays and array scalars.
|
||||
return obj.tolist()
|
||||
|
@ -65,4 +63,4 @@ class JSONEncoder(json.JSONEncoder):
|
|||
pass
|
||||
elif hasattr(obj, '__iter__'):
|
||||
return tuple(item for item in obj)
|
||||
return super(JSONEncoder, self).default(obj)
|
||||
return super().default(obj)
|
||||
|
|
|
@ -16,7 +16,7 @@ NUMERIC_FIELD_TYPES = (
|
|||
)
|
||||
|
||||
|
||||
class ClassLookupDict(object):
|
||||
class ClassLookupDict:
|
||||
"""
|
||||
Takes a dictionary with classes as keys.
|
||||
Lookups against this object will traverses the object's inheritance
|
||||
|
@ -106,8 +106,7 @@ def get_field_kwargs(field_name, model_field):
|
|||
if model_field.null and not isinstance(model_field, models.NullBooleanField):
|
||||
kwargs['allow_null'] = True
|
||||
|
||||
if model_field.blank and (isinstance(model_field, models.CharField) or
|
||||
isinstance(model_field, models.TextField)):
|
||||
if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
|
||||
kwargs['allow_blank'] = True
|
||||
|
||||
if isinstance(model_field, models.FilePathField):
|
||||
|
@ -193,9 +192,7 @@ def get_field_kwargs(field_name, model_field):
|
|||
# Ensure that max_length is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
max_length = getattr(model_field, 'max_length', None)
|
||||
if max_length is not None and (isinstance(model_field, models.CharField) or
|
||||
isinstance(model_field, models.TextField) or
|
||||
isinstance(model_field, models.FileField)):
|
||||
if max_length is not None and (isinstance(model_field, (models.CharField, models.TextField, models.FileField))):
|
||||
kwargs['max_length'] = max_length
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
Utility functions to return a formatted name and description for a given view.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from django.utils.encoding import force_text
|
||||
|
@ -67,3 +65,29 @@ def markup_description(description):
|
|||
description = escape(description).replace('\n', '<br />')
|
||||
description = '<p>' + description + '</p>'
|
||||
return mark_safe(description)
|
||||
|
||||
|
||||
class lazy_format:
|
||||
"""
|
||||
Delay formatting until it's actually needed.
|
||||
|
||||
Useful when the format string or one of the arguments is lazy.
|
||||
|
||||
Not using Django's lazy because it is too slow.
|
||||
"""
|
||||
__slots__ = ('format_string', 'args', 'kwargs', 'result')
|
||||
|
||||
def __init__(self, format_string, *args, **kwargs):
|
||||
self.result = None
|
||||
self.format_string = format_string
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __str__(self):
|
||||
if self.result is None:
|
||||
self.result = self.format_string.format(*self.args, **self.kwargs)
|
||||
self.format_string, self.args, self.kwargs = None, None, None
|
||||
return self.result
|
||||
|
||||
def __mod__(self, value):
|
||||
return str(self) % value
|
||||
|
|
|
@ -5,9 +5,6 @@ REST framework should always import this wrapper module in order to maintain
|
|||
spec-compliant encoding/decoding. Support for non-standard features should be
|
||||
handled by users at the renderer and parser layer.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
import json # noqa
|
||||
|
||||
|
|
|
@ -3,10 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers.
|
|||
|
||||
See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
|
||||
|
@ -46,8 +43,7 @@ def order_by_precedence(media_type_lst):
|
|||
return [media_types for media_types in ret if media_types]
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class _MediaType(object):
|
||||
class _MediaType:
|
||||
def __init__(self, media_type_str):
|
||||
self.orig = '' if (media_type_str is None) else media_type_str
|
||||
self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING))
|
||||
|
|
|
@ -2,16 +2,12 @@
|
|||
Helper functions for creating user-friendly representations
|
||||
of serializer classes and serializer fields.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import Promise
|
||||
|
||||
from rest_framework.compat import unicode_repr
|
||||
|
||||
|
||||
def manager_repr(value):
|
||||
model = value.model
|
||||
|
@ -34,7 +30,7 @@ def smart_repr(value):
|
|||
if isinstance(value, Promise) and value._delegate_text:
|
||||
value = force_text(value)
|
||||
|
||||
value = unicode_repr(value)
|
||||
value = repr(value)
|
||||
|
||||
# Representations like u'help text'
|
||||
# should simply be presented as 'help text'
|
||||
|
@ -45,9 +41,7 @@ def smart_repr(value):
|
|||
# <django.core.validators.RegexValidator object at 0x1047af050>
|
||||
# Should be presented as
|
||||
# <django.core.validators.RegexValidator object>
|
||||
value = re.sub(' at 0x[0-9A-Fa-f]{4,32}>', '>', value)
|
||||
|
||||
return value
|
||||
return re.sub(' at 0x[0-9A-Fa-f]{4,32}>', '>', value)
|
||||
|
||||
|
||||
def field_repr(field, force_many=False):
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from rest_framework.compat import MutableMapping, unicode_to_repr
|
||||
from rest_framework.utils import json
|
||||
|
||||
|
||||
|
@ -17,7 +15,7 @@ class ReturnDict(OrderedDict):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.serializer = kwargs.pop('serializer')
|
||||
super(ReturnDict, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def copy(self):
|
||||
return ReturnDict(self, serializer=self.serializer)
|
||||
|
@ -40,7 +38,7 @@ class ReturnList(list):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.serializer = kwargs.pop('serializer')
|
||||
super(ReturnList, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return list.__repr__(self)
|
||||
|
@ -51,7 +49,7 @@ class ReturnList(list):
|
|||
return (list, (list(self),))
|
||||
|
||||
|
||||
class BoundField(object):
|
||||
class BoundField:
|
||||
"""
|
||||
A field object that also includes `.value` and `.error` properties.
|
||||
Returned when iterating over a serializer instance,
|
||||
|
@ -73,9 +71,9 @@ class BoundField(object):
|
|||
return self._field.__class__
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('<%s value=%s errors=%s>' % (
|
||||
return '<%s value=%s errors=%s>' % (
|
||||
self.__class__.__name__, self.value, self.errors
|
||||
))
|
||||
)
|
||||
|
||||
def as_form_field(self):
|
||||
value = '' if (self.value is None or self.value is False) else self.value
|
||||
|
@ -103,9 +101,9 @@ class NestedBoundField(BoundField):
|
|||
"""
|
||||
|
||||
def __init__(self, field, value, errors, prefix=''):
|
||||
if value is None or value is '':
|
||||
if value is None or value == '':
|
||||
value = {}
|
||||
super(NestedBoundField, self).__init__(field, value, errors, prefix)
|
||||
super().__init__(field, value, errors, prefix)
|
||||
|
||||
def __iter__(self):
|
||||
for field in self.fields.values():
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from urllib import parse
|
||||
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
|
||||
|
||||
def replace_query_param(url, key, val):
|
||||
|
@ -7,11 +8,11 @@ def replace_query_param(url, key, val):
|
|||
Given a URL and a key/val pair, set or replace an item in the query
|
||||
parameters of the URL, and return the new URL.
|
||||
"""
|
||||
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(force_str(url))
|
||||
query_dict = urlparse.parse_qs(query, keep_blank_values=True)
|
||||
(scheme, netloc, path, query, fragment) = parse.urlsplit(force_str(url))
|
||||
query_dict = parse.parse_qs(query, keep_blank_values=True)
|
||||
query_dict[force_str(key)] = [force_str(val)]
|
||||
query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||
query = parse.urlencode(sorted(list(query_dict.items())), doseq=True)
|
||||
return parse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
def remove_query_param(url, key):
|
||||
|
@ -19,8 +20,8 @@ def remove_query_param(url, key):
|
|||
Given a URL and a key/val pair, remove an item in the query
|
||||
parameters of the URL, and return the new URL.
|
||||
"""
|
||||
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(force_str(url))
|
||||
query_dict = urlparse.parse_qs(query, keep_blank_values=True)
|
||||
(scheme, netloc, path, query, fragment) = parse.urlsplit(force_str(url))
|
||||
query_dict = parse.parse_qs(query, keep_blank_values=True)
|
||||
query_dict.pop(key, None)
|
||||
query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||
query = parse.urlencode(sorted(list(query_dict.items())), doseq=True)
|
||||
return parse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
|
|
@ -6,12 +6,9 @@ This gives us better separation of concerns, allows us to use single-step
|
|||
object creation, and makes it possible to switch between using the implicit
|
||||
`ModelSerializer` class and an equivalent explicit `Serializer` class.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import DataError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.compat import unicode_to_repr
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.utils.representation import smart_repr
|
||||
|
||||
|
@ -33,7 +30,7 @@ def qs_filter(queryset, **kwargs):
|
|||
return queryset.none()
|
||||
|
||||
|
||||
class UniqueValidator(object):
|
||||
class UniqueValidator:
|
||||
"""
|
||||
Validator that corresponds to `unique=True` on a model field.
|
||||
|
||||
|
@ -82,13 +79,13 @@ class UniqueValidator(object):
|
|||
raise ValidationError(self.message, code='unique')
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('<%s(queryset=%s)>' % (
|
||||
return '<%s(queryset=%s)>' % (
|
||||
self.__class__.__name__,
|
||||
smart_repr(self.queryset)
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
class UniqueTogetherValidator(object):
|
||||
class UniqueTogetherValidator:
|
||||
"""
|
||||
Validator that corresponds to `unique_together = (...)` on a model class.
|
||||
|
||||
|
@ -170,14 +167,14 @@ class UniqueTogetherValidator(object):
|
|||
raise ValidationError(message, code='unique')
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % (
|
||||
return '<%s(queryset=%s, fields=%s)>' % (
|
||||
self.__class__.__name__,
|
||||
smart_repr(self.queryset),
|
||||
smart_repr(self.fields)
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
class BaseUniqueForValidator(object):
|
||||
class BaseUniqueForValidator:
|
||||
message = None
|
||||
missing_message = _('This field is required.')
|
||||
|
||||
|
@ -236,12 +233,12 @@ class BaseUniqueForValidator(object):
|
|||
}, code='unique')
|
||||
|
||||
def __repr__(self):
|
||||
return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % (
|
||||
return '<%s(queryset=%s, field=%s, date_field=%s)>' % (
|
||||
self.__class__.__name__,
|
||||
smart_repr(self.queryset),
|
||||
smart_repr(self.field),
|
||||
smart_repr(self.date_field)
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
class UniqueForDateValidator(BaseUniqueForValidator):
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.compat import unicode_http_header
|
||||
|
@ -13,7 +10,7 @@ from rest_framework.templatetags.rest_framework import replace_query_param
|
|||
from rest_framework.utils.mediatypes import _MediaType
|
||||
|
||||
|
||||
class BaseVersioning(object):
|
||||
class BaseVersioning:
|
||||
default_version = api_settings.DEFAULT_VERSION
|
||||
allowed_versions = api_settings.ALLOWED_VERSIONS
|
||||
version_param = api_settings.VERSION_PARAM
|
||||
|
@ -87,7 +84,7 @@ class URLPathVersioning(BaseVersioning):
|
|||
kwargs = {} if (kwargs is None) else kwargs
|
||||
kwargs[self.version_param] = request.version
|
||||
|
||||
return super(URLPathVersioning, self).reverse(
|
||||
return super().reverse(
|
||||
viewname, args, kwargs, request, format, **extra
|
||||
)
|
||||
|
||||
|
@ -133,7 +130,7 @@ class NamespaceVersioning(BaseVersioning):
|
|||
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||
if request.version is not None:
|
||||
viewname = self.get_versioned_viewname(viewname, request)
|
||||
return super(NamespaceVersioning, self).reverse(
|
||||
return super().reverse(
|
||||
viewname, args, kwargs, request, format, **extra
|
||||
)
|
||||
|
||||
|
@ -179,7 +176,7 @@ class QueryParameterVersioning(BaseVersioning):
|
|||
return version
|
||||
|
||||
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||
url = super(QueryParameterVersioning, self).reverse(
|
||||
url = super().reverse(
|
||||
viewname, args, kwargs, request, format, **extra
|
||||
)
|
||||
if request.version is not None:
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
Provides an APIView class that is the base of all views in REST framework.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import connection, models, transaction
|
||||
|
@ -137,7 +135,7 @@ class APIView(View):
|
|||
)
|
||||
cls.queryset._fetch_all = force_evaluation
|
||||
|
||||
view = super(APIView, cls).as_view(**initkwargs)
|
||||
view = super().as_view(**initkwargs)
|
||||
view.cls = cls
|
||||
view.initkwargs = initkwargs
|
||||
|
||||
|
@ -352,9 +350,13 @@ class APIView(View):
|
|||
Check if request should be throttled.
|
||||
Raises an appropriate exception if the request is throttled.
|
||||
"""
|
||||
throttle_durations = []
|
||||
for throttle in self.get_throttles():
|
||||
if not throttle.allow_request(request, self):
|
||||
self.throttled(request, throttle.wait())
|
||||
throttle_durations.append(throttle.wait())
|
||||
|
||||
if throttle_durations:
|
||||
self.throttled(request, max(throttle_durations))
|
||||
|
||||
def determine_version(self, request, *args, **kwargs):
|
||||
"""
|
||||
|
|
|
@ -16,8 +16,6 @@ automatically.
|
|||
router.register(r'users', UserViewSet, 'user')
|
||||
urlpatterns = router.urls
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import update_wrapper
|
||||
from inspect import getmembers
|
||||
|
@ -34,7 +32,7 @@ def _is_extra_action(attr):
|
|||
return hasattr(attr, 'mapping')
|
||||
|
||||
|
||||
class ViewSetMixin(object):
|
||||
class ViewSetMixin:
|
||||
"""
|
||||
This is the magic.
|
||||
|
||||
|
@ -134,7 +132,7 @@ class ViewSetMixin(object):
|
|||
"""
|
||||
Set the `.action` attribute on the view, depending on the request method.
|
||||
"""
|
||||
request = super(ViewSetMixin, self).initialize_request(request, *args, **kwargs)
|
||||
request = super().initialize_request(request, *args, **kwargs)
|
||||
method = request.method.lower()
|
||||
if method == 'options':
|
||||
# This is a special case as we always provide handling for the
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
#! /usr/bin/env python
|
||||
from __future__ import print_function
|
||||
|
||||
#! /usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
@ -13,7 +11,7 @@ PYTEST_ARGS = {
|
|||
|
||||
FLAKE8_ARGS = ['rest_framework', 'tests']
|
||||
|
||||
ISORT_ARGS = ['--recursive', '--check-only', '--diff', '-o' 'uritemplate', '-p', 'tests', 'rest_framework', 'tests']
|
||||
ISORT_ARGS = ['--recursive', '--check-only', '--diff', 'rest_framework', 'tests']
|
||||
|
||||
|
||||
def exit_on_failure(ret, message=None):
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE.md
|
||||
|
||||
|
@ -17,8 +14,8 @@ skip=.tox
|
|||
atomic=true
|
||||
multi_line_output=5
|
||||
known_standard_library=types
|
||||
known_third_party=pytest,_pytest,django,pytz
|
||||
known_first_party=rest_framework
|
||||
known_third_party=pytest,_pytest,django,pytz,uritemplate
|
||||
known_first_party=rest_framework,tests
|
||||
|
||||
[coverage:run]
|
||||
# NOTE: source is ignored with pytest-cov (but uses the same).
|
||||
|
|
37
setup.py
37
setup.py
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
@ -8,6 +7,34 @@ from io import open
|
|||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
CURRENT_PYTHON = sys.version_info[:2]
|
||||
REQUIRED_PYTHON = (3, 5)
|
||||
|
||||
# This check and everything above must remain compatible with Python 2.7.
|
||||
if CURRENT_PYTHON < REQUIRED_PYTHON:
|
||||
sys.stderr.write("""
|
||||
==========================
|
||||
Unsupported Python version
|
||||
==========================
|
||||
|
||||
This version of Django REST Framework requires Python {}.{}, but you're trying
|
||||
to install it on Python {}.{}.
|
||||
|
||||
This may be because you are using a version of pip that doesn't
|
||||
understand the python_requires classifier. Make sure you
|
||||
have pip >= 9.0 and setuptools >= 24.2, then try again:
|
||||
|
||||
$ python -m pip install --upgrade pip setuptools
|
||||
$ python -m pip install djangorestframework
|
||||
|
||||
This will install the latest version of Django REST Framework which works on
|
||||
your version of Python. If you can't upgrade your pip (or Python), request
|
||||
an older version of Django REST Framework:
|
||||
|
||||
$ python -m pip install "django<3.10"
|
||||
""".format(*(REQUIRED_PYTHON + CURRENT_PYTHON)))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def read(f):
|
||||
return open(f, 'r', encoding='utf-8').read()
|
||||
|
@ -52,7 +79,7 @@ setup(
|
|||
packages=find_packages(exclude=['tests*']),
|
||||
include_package_data=True,
|
||||
install_requires=[],
|
||||
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
|
||||
python_requires=">=3.5",
|
||||
zip_safe=False,
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
|
@ -66,13 +93,11 @@ setup(
|
|||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
]
|
||||
)
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
# coding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
|
@ -10,7 +6,6 @@ from django.conf.urls import include, url
|
|||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import (
|
||||
HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status
|
||||
|
@ -188,7 +183,7 @@ class SessionAuthTests(TestCase):
|
|||
cf. [#1810](https://github.com/encode/django-rest-framework/pull/1810)
|
||||
"""
|
||||
response = self.csrf_client.get('/auth/login/')
|
||||
content = response.content.decode('utf8')
|
||||
content = response.content.decode()
|
||||
assert '<label for="id_username">Username:</label>' in content
|
||||
|
||||
def test_post_form_session_auth_failing_csrf(self):
|
||||
|
@ -253,7 +248,7 @@ class SessionAuthTests(TestCase):
|
|||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
class BaseTokenAuthTests(object):
|
||||
class BaseTokenAuthTests:
|
||||
"""Token authentication"""
|
||||
model = None
|
||||
path = None
|
||||
|
@ -381,7 +376,7 @@ class TokenAuthTests(BaseTokenAuthTests, TestCase):
|
|||
"""Ensure generate_key returns a string"""
|
||||
token = self.model()
|
||||
key = token.generate_key()
|
||||
assert isinstance(key, six.string_types)
|
||||
assert isinstance(key, str)
|
||||
|
||||
def test_token_login_json(self):
|
||||
"""Ensure token login view using JSON POST works."""
|
||||
|
@ -534,7 +529,7 @@ class BasicAuthenticationUnitTests(TestCase):
|
|||
def test_basic_authentication_raises_error_if_user_not_active(self):
|
||||
from rest_framework import authentication
|
||||
|
||||
class MockUser(object):
|
||||
class MockUser:
|
||||
is_active = False
|
||||
old_authenticate = authentication.authenticate
|
||||
authentication.authenticate = lambda **kwargs: MockUser()
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from .views import MockView
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import MockView
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
|
@ -26,18 +24,18 @@ class DropdownWithAuthTests(TestCase):
|
|||
def test_name_shown_when_logged_in(self):
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
response = self.client.get('/')
|
||||
content = response.content.decode('utf8')
|
||||
content = response.content.decode()
|
||||
assert 'john' in content
|
||||
|
||||
def test_logout_shown_when_logged_in(self):
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
response = self.client.get('/')
|
||||
content = response.content.decode('utf8')
|
||||
content = response.content.decode()
|
||||
assert '>Log out<' in content
|
||||
|
||||
def test_login_shown_when_logged_out(self):
|
||||
response = self.client.get('/')
|
||||
content = response.content.decode('utf8')
|
||||
content = response.content.decode()
|
||||
assert '>Log in<' in content
|
||||
|
||||
|
||||
|
@ -61,16 +59,16 @@ class NoDropdownWithoutAuthTests(TestCase):
|
|||
def test_name_shown_when_logged_in(self):
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
response = self.client.get('/')
|
||||
content = response.content.decode('utf8')
|
||||
content = response.content.decode()
|
||||
assert 'john' in content
|
||||
|
||||
def test_dropdown_not_shown_when_logged_in(self):
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
response = self.client.get('/')
|
||||
content = response.content.decode('utf8')
|
||||
content = response.content.decode()
|
||||
assert '<li class="dropdown">' not in content
|
||||
|
||||
def test_dropdown_not_shown_when_logged_out(self):
|
||||
response = self.client.get('/')
|
||||
content = response.content.decode('utf8')
|
||||
content = response.content.decode()
|
||||
assert '<li class="dropdown">' not in content
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
@ -36,7 +34,7 @@ class DropdownWithAuthTests(TestCase):
|
|||
def test_login(self):
|
||||
response = self.client.get('/api/')
|
||||
assert 200 == response.status_code
|
||||
content = response.content.decode('utf-8')
|
||||
content = response.content.decode()
|
||||
assert 'form action="/api/"' in content
|
||||
assert 'input name="nested.one"' in content
|
||||
assert 'input name="nested.two"' in content
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import authentication, renderers
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.fields import (
|
||||
GenericForeignKey, GenericRelation
|
||||
)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Tag(models.Model):
|
||||
"""
|
||||
Tags have a descriptive slug, and are attached to an arbitrary object.
|
||||
|
@ -22,7 +18,6 @@ class Tag(models.Model):
|
|||
return self.tag
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Bookmark(models.Model):
|
||||
"""
|
||||
A URL bookmark that may have multiple tags attached.
|
||||
|
@ -34,7 +29,6 @@ class Bookmark(models.Model):
|
|||
return 'Bookmark: %s' % self.url
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Note(models.Model):
|
||||
"""
|
||||
A textual note that may have multiple tags attached.
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework import serializers
|
||||
|
|
|
@ -1 +1,16 @@
|
|||
from rest_framework import compat # noqa
|
||||
"""
|
||||
This test "app" exists to ensure that parts of Django REST Framework can be
|
||||
imported/invoked before Django itself has been fully initialized.
|
||||
"""
|
||||
|
||||
from rest_framework import compat, serializers # noqa
|
||||
|
||||
|
||||
# test initializing fields with lazy translations
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
charfield = serializers.CharField(min_length=1, max_length=2)
|
||||
integerfield = serializers.IntegerField(min_value=1, max_value=2)
|
||||
floatfield = serializers.FloatField(min_value=1, max_value=2)
|
||||
decimalfield = serializers.DecimalField(max_digits=10, decimal_places=1, min_value=1, max_value=2)
|
||||
durationfield = serializers.DurationField(min_value=1, max_value=2)
|
||||
listfield = serializers.ListField(min_length=1, max_length=2)
|
||||
|
|
|
@ -4,10 +4,21 @@ from tests import importable
|
|||
|
||||
|
||||
def test_installed():
|
||||
# ensure that apps can freely import rest_framework.compat
|
||||
# ensure the test app hasn't been removed from the test suite
|
||||
assert 'tests.importable' in settings.INSTALLED_APPS
|
||||
|
||||
|
||||
def test_imported():
|
||||
# ensure that the __init__ hasn't been mucked with
|
||||
def test_compat():
|
||||
assert hasattr(importable, 'compat')
|
||||
|
||||
|
||||
def test_serializer_fields_initialization():
|
||||
assert hasattr(importable, 'ExampleSerializer')
|
||||
|
||||
serializer = importable.ExampleSerializer()
|
||||
assert 'charfield' in serializer.fields
|
||||
assert 'integerfield' in serializer.fields
|
||||
assert 'floatfield' in serializer.fields
|
||||
assert 'decimalfield' in serializer.fields
|
||||
assert 'durationfield' in serializer.fields
|
||||
assert 'listfield' in serializer.fields
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class RESTFrameworkModel(models.Model):
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user