Compare commits

..

No commits in common. "master" and "1.14.0" have entirely different histories.

56 changed files with 809 additions and 1826 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@ testproj/db.sqlite3
testproj/staticfiles testproj/staticfiles
\.pytest_cache/ \.pytest_cache/
docs/\.doctrees/ docs/\.doctrees/
pip-wheel-metadata/
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)
### Python template ### Python template

View File

@ -19,7 +19,6 @@
<excludeFolder url="file://$MODULE_DIR$/.cache" /> <excludeFolder url="file://$MODULE_DIR$/.cache" />
<excludeFolder url="file://$MODULE_DIR$/.eggs" /> <excludeFolder url="file://$MODULE_DIR$/.eggs" />
<excludeFolder url="file://$MODULE_DIR$/.pytest_cache" /> <excludeFolder url="file://$MODULE_DIR$/.pytest_cache" />
<excludeFolder url="file://$MODULE_DIR$/.tox" />
<excludeFolder url="file://$MODULE_DIR$/build" /> <excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/dist" /> <excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/docs/.doctrees" /> <excludeFolder url="file://$MODULE_DIR$/docs/.doctrees" />

View File

@ -1,14 +1,16 @@
language: python language: python
python: python:
- '2.7'
- '3.4'
- '3.5'
- '3.6' - '3.6'
- '3.7' - '3.7'
- '3.8'
dist: xenial dist: xenial
cache: pip cache: pip
matrix: jobs:
include: include:
- python: '3.6' - python: '3.6'
env: TOXENV=docs env: TOXENV=docs
@ -58,6 +60,11 @@ after_success:
codecov codecov
fi fi
branches:
only:
- master
- /^v?\d+\.\d+(\.\d+)?(-?\S+)?$/
stages: stages:
- test - test
- name: publish - name: publish

View File

@ -6,7 +6,7 @@
Contributing Contributing
############ ############
Contributions are always welcome and appreciated! Here are some ways you can contribute. Contributions are always welcome and appreciated! Here are some ways you can contribut.
****** ******
Issues Issues
@ -57,7 +57,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
.. code:: console .. code:: console
(venv) $ python testproj/manage.py generate_swagger tests/reference.yaml --overwrite --user admin --url http://test.local:8002/ (venv) $ python testproj/manage.py generate_swagger ../tests/reference.yaml --overwrite --user admin --url http://test.local:8002/
After checking the git diff to verify that no unexpected changes appeared, you should commit the new After checking the git diff to verify that no unexpected changes appeared, you should commit the new
``reference.yaml`` together with your changes. ``reference.yaml`` together with your changes.
@ -95,7 +95,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
#. **Your code must pass all the required travis jobs before it is merged** #. **Your code must pass all the required travis jobs before it is merged**
As of now, this consists of running on Python 2.7, 3.5, 3.6 and 3.7, and building the docs succesfully. As of now, this consists of running on Python 2.7, 3.4, 3.5 and 3.6, and building the docs succesfully.
****************** ******************
Maintainer's notes Maintainer's notes

View File

@ -13,9 +13,9 @@ Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framewor
Compatible with Compatible with
- **Django Rest Framework**: 3.8, 3.9, 3.10, 3.11 - **Django Rest Framework**: 3.7.7, 3.8, 3.9
- **Django**: 1.11, 2.2, 3.0 - **Django**: 1.11, 2.0, 2.1
- **Python**: 2.7, 3.6, 3.7, 3.8 - **Python**: 2.7, 3.4, 3.5, 3.6, 3.7
Only the latest patch version of each ``major.minor`` series of Python, Django and Django REST Framework is supported. Only the latest patch version of each ``major.minor`` series of Python, Django and Django REST Framework is supported.
@ -235,7 +235,7 @@ Offline
^^^^^^^ ^^^^^^^
If your schema is not accessible from the internet, you can run a local copy of If your schema is not accessible from the internet, you can run a local copy of
`swagger-validator <https://hub.docker.com/r/swaggerapi/swagger-validator/>`_ and set the ``VALIDATOR_URL`` accordingly: `swagger-validator <https://hub.docker.com/r/swaggerapi/swagger-validator/>`_ and set the `VALIDATOR_URL` accordingly:
.. code:: python .. code:: python

View File

@ -2,74 +2,6 @@
Changelog Changelog
######### #########
**********
**1.17.1**
**********
*Release date: Feb 17, 2020*
- **FIXED:** fixed compatibility issue with CurrentUserDefault in Django Rest Framework 3.11
- **FIXED:** respect `USERNAME_FIELD` in `generate_swagger` command (:pr:`486`)
**Support was dropped for Python 3.5, Django 2.0, Django 2.1, DRF 3.7**
**********
**1.17.0**
**********
*Release date: Oct 03, 2019*
- **ADDED:** added `JSONFieldInspector` for `JSONField` support (:pr:`417`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.23.11
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.14 (:issue:`398`)
- **FIXED:** fixed a type hint support issue (:pr:`428`, :issue:`450`)
- **FIXED:** fixed packaging issue caused by a missing requirement (:issue:`412`)
**********
**1.16.1**
**********
*Release date: Jul 16, 2019*
- **IMPROVED:** better enum type detection for nested `ChoiceField`\ s (:pr:`400`)
- **FIXED:** fixed DRF 3.10 compatibility (:pr:`408`, :issue:`410`, :issue:`411`)
**********
**1.16.0**
**********
*Release date: Jun 13, 2019*
- **ADDED:** added `reference_resolver_class` attribute hook to `SwaggerAutoSchema` (:pr:`350`)
- **ADDED:** added `operation_keys` attribute to `SwaggerAutoSchema`, along with `__init__` parameter (:pr:`355`)
- **FIXED:** fixed potential crash on `issubclass` check without `isclass` check
**********
**1.15.1**
**********
*Release date: Jun 13, 2019*
- **IMPROVED:** updated ``swagger-ui`` to version 3.22.3
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.8-1
- **FIXED:** fixed an issue with inspection of typing hints on Python 2.7 (:issue:`363`)
- **FIXED:** fixed an issue with inspection of typing hints on Python 3.7 (:issue:`371`)
**Python 3.4 support has been dropped!**
**********
**1.15.0**
**********
*Release date: Apr 01, 2019*
- **ADDED:** added ``is_list_view`` and ``has_list_response`` extension points to ``SwaggerAutoSchema`` (:issue:`331`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.22.0
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.4
- **FIXED:** ``ListModelMixin`` will now always be treated as a list view (:issue:`306`)
- **FIXED:** non-primtive values in field ``choices`` will now be handled properly (:issue:`340`)
********** **********
**1.14.0** **1.14.0**
********** **********

View File

@ -45,8 +45,6 @@ some properties of the generated :class:`.Operation`. For example, in a ``ViewSe
.. code-block:: python .. code-block:: python
from drf_yasg.utils import swagger_auto_schema
@swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'}) @swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'})
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
"""partial_update method docstring""" """partial_update method docstring"""
@ -89,14 +87,14 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
* for ``ViewSet``, ``GenericViewSet``, ``ModelViewSet``, because each viewset corresponds to multiple **paths**, you have * for ``ViewSet``, ``GenericViewSet``, ``ModelViewSet``, because each viewset corresponds to multiple **paths**, you have
to decorate the *action methods*, i.e. ``list``, ``create``, ``retrieve``, etc. |br| to decorate the *action methods*, i.e. ``list``, ``create``, ``retrieve``, etc. |br|
Additionally, ``@action``\ s defined on the viewset, like function based api views, can respond to multiple HTTP Additionally, ``@action``\ s, ``@list_route``\ s or ``@detail_route``\ s defined on the viewset, like function based
methods and thus have multiple operations that must be decorated separately: api views, can respond to multiple HTTP methods and thus have multiple operations that must be decorated separately:
.. code-block:: python .. code-block:: python
class ArticleViewSet(viewsets.ModelViewSet): class ArticleViewSet(viewsets.ModelViewSet):
# method or 'methods' can be skipped because the action only handles a single method (GET) # method or 'methods' can be skipped because the list_route only handles a single method (GET)
@swagger_auto_schema(operation_description='GET /articles/today/') @swagger_auto_schema(operation_description='GET /articles/today/')
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def today(self, request): def today(self, request):
@ -212,8 +210,7 @@ Schema generation of ``serializers.SerializerMethodField`` is supported in two w
Serializer ``Meta`` nested class Serializer ``Meta`` nested class
******************************** ********************************
You can define some per-serializer or per-field options by adding a ``Meta`` class to your ``Serializer`` or You can define some per-serializer options by adding a ``Meta`` class to your serializer, e.g.:
serializer ``Field``, e.g.:
.. code-block:: python .. code-block:: python
@ -237,64 +234,6 @@ The available options are:
which are converted to Swagger ``Schema`` attribute names according to :func:`.make_swagger_name`. which are converted to Swagger ``Schema`` attribute names according to :func:`.make_swagger_name`.
Attribute names and values must conform to the `OpenAPI 2.0 specification <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject>`_. Attribute names and values must conform to the `OpenAPI 2.0 specification <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject>`_.
Suppose you wanted to model an email using a `JSONField` to store the subject and body for performance reasons:
.. code-block:: python
from django.contrib.postgres.fields import JSONField
class Email(models.Model):
# Store data as JSON, but the data should be made up of
# an object that has two properties, "subject" and "body"
# Example:
# {
# "subject": "My Title",
# "body": "The body of the message.",
# }
message = JSONField()
To instruct ``drf-yasg`` to output an OpenAPI schema that matches this, create a custom ``JSONField``:
.. code-block:: python
class EmailMessageField(serializers.JSONField):
class Meta:
swagger_schema_fields = {
"type": openapi.TYPE_OBJECT,
"title": "Email",
"properties": {
"subject": openapi.Schema(
title="Email subject",
type=openapi.TYPE_STRING,
),
"body": openapi.Schema(
title="Email body",
type=openapi.TYPE_STRING,
),
},
"required": ["subject", "body"],
}
class EmailSerializer(ModelSerializer):
class Meta:
model = Email
fields = "__all__"
message = EmailMessageField()
.. Warning::
Overriding a default ``Field`` generated by a ``ModelSerializer`` will also override automatically
generated validators for that ``Field``. To add ``Serializer`` validation back in manually, see the relevant
`DRF Validators`_ and `DRF Fields`_ documentation.
One example way to do this is to set the ``default_validators`` attribute on a field.
.. code-block:: python
class EmailMessageField(serializers.JSONField):
default_validators = [my_custom_email_validator]
...
************************* *************************
Subclassing and extending Subclassing and extending
@ -437,7 +376,7 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
class AnotherSerializer(serializers.ModelSerializer): class AnotherSerializer(serializers.ModelSerializer):
child = OneSerializer() chilf = OneSerializer()
class Meta: class Meta:
model = SomeParentModel model = SomeParentModel
@ -448,5 +387,3 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
.. _Python 3 type hinting: https://docs.python.org/3/library/typing.html .. _Python 3 type hinting: https://docs.python.org/3/library/typing.html
.. _DRF Validators: https://www.django-rest-framework.org/api-guide/validators/
.. _DRF Fields: https://www.django-rest-framework.org/api-guide/fields/#validators

View File

@ -14,7 +14,7 @@ This library generates OpenAPI 2.0 documents. The authoritative specification fo
be the official documentation over at `swagger.io <https://swagger.io/>`__ and the `OpenAPI 2.0 specification be the official documentation over at `swagger.io <https://swagger.io/>`__ and the `OpenAPI 2.0 specification
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__. page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__.
Because the above specifications are a bit heavy and convoluted, here is a general overview of how the specification Beause the above specifications are a bit heavy and convoluted, here is a general overview of how the specification
is structured, starting from the root ``Swagger`` object. is structured, starting from the root ``Swagger`` object.
* :class:`.Swagger` object * :class:`.Swagger` object

View File

@ -85,7 +85,6 @@ to this list.
:class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.JSONFieldInspector' <.inspectors.JSONFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.HiddenFieldInspector' <.inspectors.HiddenFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.HiddenFieldInspector' <.inspectors.HiddenFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.RecursiveFieldInspector' <.inspectors.RecursiveFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.RecursiveFieldInspector' <.inspectors.RecursiveFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.SerializerMethodFieldInspector' <.inspectors.SerializerMethodFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.SerializerMethodFieldInspector' <.inspectors.SerializerMethodFieldInspector>`, |br| \
@ -341,7 +340,7 @@ values for Parameters.
OAUTH2_REDIRECT_URL OAUTH2_REDIRECT_URL
------------------- -------------------
Used when OAuth2 authentication of API requests via swagger-ui is desired. If ``None`` is passed, the Used when OAuth2 authenitcation of API requests via swagger-ui is desired. If ``None`` is passed, the
``oauth2RedirectUrl`` parameter will be set to ``{% static 'drf-yasg/swagger-ui-dist/oauth2-redirect.html' %}``. This ``oauth2RedirectUrl`` parameter will be set to ``{% static 'drf-yasg/swagger-ui-dist/oauth2-redirect.html' %}``. This
is the default `https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html <oauth2-redirect>`_ is the default `https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html <oauth2-redirect>`_
file provided by ``swagger-ui``. file provided by ``swagger-ui``.
@ -352,7 +351,7 @@ file provided by ``swagger-ui``.
OAUTH2_CONFIG OAUTH2_CONFIG
------------- -------------
Used when OAuth2 authentication of API requests via swagger-ui is desired. Provides OAuth2 configuration parameters Used when OAuth2 authenitcation of API requests via swagger-ui is desired. Provides OAuth2 configuration parameters
to the ``SwaggerUIBundle#initOAuth`` method, and must be a dictionary. See to the ``SwaggerUIBundle#initOAuth`` method, and must be a dictionary. See
`OAuth2 configuration <https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md>`_. `OAuth2 configuration <https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md>`_.
@ -455,7 +454,7 @@ FETCH_SCHEMA_WITH_QUERY
Fetch the OpenAPI document using the query parameters passed to the ReDoc page request. Fetch the OpenAPI document using the query parameters passed to the ReDoc page request.
**Default**: :python:`True` |br| **Default**: :python:`'True` |br|
*Maps to parameter*: - *Maps to parameter*: -

948
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{ {
"name": "drf-yasg", "name": "drf-yasg",
"dependencies": { "dependencies": {
"redoc": "^2.0.0-rc.14", "redoc": "^2.0.0-rc.2",
"swagger-ui-dist": "^3.23.11" "swagger-ui-dist": "^3.21.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -4,7 +4,6 @@ ruamel.yaml>=0.15.34
inflection>=0.3.1 inflection>=0.3.1
six>=1.10.0 six>=1.10.0
uritemplate>=3.0.0 uritemplate>=3.0.0
packaging
djangorestframework>=3.8 djangorestframework>=3.7.7
Django>=1.11.7 Django>=1.11.7

View File

@ -4,4 +4,3 @@
-r lint.txt -r lint.txt
tox-battery>=0.5 tox-battery>=0.5
django-oauth-toolkit

View File

@ -5,7 +5,5 @@ pytest-cov>=2.6.0
pytest-xdist>=1.25.0 pytest-xdist>=1.25.0
pytest-django>=3.4.4 pytest-django>=3.4.4
datadiff==2.0.0 datadiff==2.0.0
psycopg2-binary==2.8.3
django-fake-model==0.1.4
-r testproj.txt -r testproj.txt

View File

@ -1,9 +1,12 @@
# test project requirements # test project requirements
Pillow>=4.3.0 Pillow>=4.3.0
pygments>=2.2.0
django-cors-headers>=2.1.0
django-filter>=1.1.0,<2.0; python_version == "2.7" django-filter>=1.1.0,<2.0; python_version == "2.7"
django-filter>=1.1.0; python_version >= "3.5" django-filter>=1.1.0; python_version >= "3.4"
djangorestframework-camel-case>=1.1.2 djangorestframework-camel-case>=0.2.0
djangorestframework-recursive>=0.1.2 djangorestframework-recursive>=0.1.2
dj-database-url>=0.4.2 dj-database-url>=0.4.2
user_agents>=1.1.0 user_agents>=1.1.0
django-cors-headers # django-oauth-toolkit 1.2 does not support Django 1.11
django-oauth-toolkit>=1.1.0,<1.2.0

View File

@ -1 +1 @@
python-3.7.3 python-3.7.1

View File

@ -19,18 +19,6 @@ with io.open('README.rst', encoding='utf-8') as readme:
requirements = read_req('base.txt') requirements = read_req('base.txt')
requirements_validation = read_req('validation.txt') requirements_validation = read_req('validation.txt')
py3_supported_range = (5, 8)
# convert inclusive range to exclusive range
py3_supported_range = (py3_supported_range[0], py3_supported_range[1] + 1)
python_requires = ", ".join([">=2.7"] + ["!=3.{}.*".format(v) for v in range(0, py3_supported_range[0])])
python_classifiers = [
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
] + ['Programming Language :: Python :: 3.{}'.format(v) for v in range(*py3_supported_range)]
def drf_yasg_setup(**kwargs): def drf_yasg_setup(**kwargs):
setup( setup(
@ -50,21 +38,28 @@ def drf_yasg_setup(**kwargs):
author_email='cristi@cvjd.me', author_email='cristi@cvjd.me',
keywords='drf django django-rest-framework schema swagger openapi codegen swagger-codegen ' keywords='drf django django-rest-framework schema swagger openapi codegen swagger-codegen '
'documentation drf-yasg django-rest-swagger drf-openapi', 'documentation drf-yasg django-rest-swagger drf-openapi',
python_requires=python_requires, python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
classifiers=[ classifiers=[
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Environment :: Web Environment', 'Environment :: Web Environment',
'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',
'Framework :: Django', 'Framework :: Django',
'Framework :: Django :: 1.11', 'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0', 'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1', 'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2',
'Topic :: Documentation', 'Topic :: Documentation',
'Topic :: Software Development :: Code Generators', 'Topic :: Software Development :: Code Generators',
] + python_classifiers, ],
**kwargs **kwargs
) )

View File

@ -12,7 +12,6 @@ SWAGGER_DEFAULTS = {
'drf_yasg.inspectors.ChoiceFieldInspector', 'drf_yasg.inspectors.ChoiceFieldInspector',
'drf_yasg.inspectors.FileFieldInspector', 'drf_yasg.inspectors.FileFieldInspector',
'drf_yasg.inspectors.DictFieldInspector', 'drf_yasg.inspectors.DictFieldInspector',
'drf_yasg.inspectors.JSONFieldInspector',
'drf_yasg.inspectors.HiddenFieldInspector', 'drf_yasg.inspectors.HiddenFieldInspector',
'drf_yasg.inspectors.RelatedFieldInspector', 'drf_yasg.inspectors.RelatedFieldInspector',
'drf_yasg.inspectors.SerializerMethodFieldInspector', 'drf_yasg.inspectors.SerializerMethodFieldInspector',

View File

@ -1,4 +1,4 @@
from six import binary_type, raise_from, text_type from six import raise_from
import copy import copy
import json import json
@ -176,14 +176,7 @@ class SaneYamlDumper(yaml.SafeDumper):
node.flow_style = best_style node.flow_style = best_style
return node return node
def represent_text(self, text):
if "\n" in text:
return self.represent_scalar('tag:yaml.org,2002:str', text, style='|')
return self.represent_scalar('tag:yaml.org,2002:str', text)
SaneYamlDumper.add_representer(binary_type, SaneYamlDumper.represent_text)
SaneYamlDumper.add_representer(text_type, SaneYamlDumper.represent_text)
SaneYamlDumper.add_representer(OrderedDict, SaneYamlDumper.represent_odict) SaneYamlDumper.add_representer(OrderedDict, SaneYamlDumper.represent_odict)
SaneYamlDumper.add_multi_representer(OrderedDict, SaneYamlDumper.represent_odict) SaneYamlDumper.add_multi_representer(OrderedDict, SaneYamlDumper.represent_odict)

View File

@ -3,14 +3,13 @@ import logging
import re import re
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
import rest_framework
import uritemplate import uritemplate
from coreapi.compat import urlparse from coreapi.compat import urlparse
from packaging.version import Version
from rest_framework import versioning from rest_framework import versioning
from rest_framework.compat import URLPattern, URLResolver, get_original_route from rest_framework.compat import URLPattern, URLResolver, get_original_route
from rest_framework.schemas.generators import EndpointEnumerator as _EndpointEnumerator from rest_framework.schemas.generators import EndpointEnumerator as _EndpointEnumerator
from rest_framework.schemas.generators import endpoint_ordering, get_pk_name from rest_framework.schemas.generators import SchemaGenerator, endpoint_ordering, get_pk_name
from rest_framework.schemas.inspectors import get_pk_description
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from . import openapi from . import openapi
@ -20,14 +19,6 @@ from .inspectors.field import get_basic_type_info, get_queryset_field, get_query
from .openapi import ReferenceResolver, SwaggerDict from .openapi import ReferenceResolver, SwaggerDict
from .utils import force_real_str, get_consumes, get_produces from .utils import force_real_str, get_consumes, get_produces
if Version(rest_framework.__version__) < Version('3.10'):
from rest_framework.schemas.generators import SchemaGenerator
from rest_framework.schemas.inspectors import get_pk_description
else:
from rest_framework.schemas import SchemaGenerator
from rest_framework.schemas.utils import get_pk_description
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}') PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
@ -169,7 +160,6 @@ class OpenAPISchemaGenerator(object):
Method implementations shamelessly stolen and adapted from rest-framework ``SchemaGenerator``. Method implementations shamelessly stolen and adapted from rest-framework ``SchemaGenerator``.
""" """
endpoint_enumerator_class = EndpointEnumerator endpoint_enumerator_class = EndpointEnumerator
reference_resolver_class = ReferenceResolver
def __init__(self, info, version='', url=None, patterns=None, urlconf=None): def __init__(self, info, version='', url=None, patterns=None, urlconf=None):
""" """
@ -248,7 +238,7 @@ class OpenAPISchemaGenerator(object):
:rtype: openapi.Swagger :rtype: openapi.Swagger
""" """
endpoints = self.get_endpoints(request) endpoints = self.get_endpoints(request)
components = self.reference_resolver_class(openapi.SCHEMA_DEFINITIONS, force_init=True) components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS, force_init=True)
self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES) self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES)
self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES) self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES)
paths, prefix = self.get_paths(endpoints, components, request, public) paths, prefix = self.get_paths(endpoints, components, request, public)
@ -450,7 +440,7 @@ class OpenAPISchemaGenerator(object):
if view_inspector_cls is None: if view_inspector_cls is None:
return None return None
view_inspector = view_inspector_cls(view, path, method, components, request, overrides, operation_keys) view_inspector = view_inspector_cls(view, path, method, components, request, overrides)
operation = view_inspector.get_operation(operation_keys) operation = view_inspector.get_operation(operation_keys)
if operation is None: if operation is None:
return None return None

View File

@ -4,8 +4,8 @@ from .base import (
) )
from .field import ( from .field import (
CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, HiddenFieldInspector, CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, HiddenFieldInspector,
InlineSerializerInspector, JSONFieldInspector, RecursiveFieldInspector, ReferencingSerializerInspector, InlineSerializerInspector, RecursiveFieldInspector, ReferencingSerializerInspector, RelatedFieldInspector,
RelatedFieldInspector, SerializerMethodFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector SerializerMethodFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector
) )
from .query import CoreAPICompatInspector, DjangoRestResponsePagination from .query import CoreAPICompatInspector, DjangoRestResponsePagination
from .view import SwaggerAutoSchema from .view import SwaggerAutoSchema
@ -24,7 +24,7 @@ __all__ = [
# field inspectors # field inspectors
'InlineSerializerInspector', 'RecursiveFieldInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector', 'InlineSerializerInspector', 'RecursiveFieldInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector',
'SimpleFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector', 'JSONFieldInspector', 'SimpleFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector',
'StringDefaultFieldInspector', 'CamelCaseJSONFilter', 'HiddenFieldInspector', 'SerializerMethodFieldInspector', 'StringDefaultFieldInspector', 'CamelCaseJSONFilter', 'HiddenFieldInspector', 'SerializerMethodFieldInspector',
# view inspectors # view inspectors

View File

@ -272,8 +272,7 @@ class FieldInspector(BaseInspector):
assert not isinstance(field, openapi.SwaggerDict), "passed field is already a SwaggerDict object" assert not isinstance(field, openapi.SwaggerDict), "passed field is already a SwaggerDict object"
title = force_real_str(field.label) if field.label else None title = force_real_str(field.label) if field.label else None
title = title if swagger_object_type == openapi.Schema else None # only Schema has title title = title if swagger_object_type == openapi.Schema else None # only Schema has title
help_text = getattr(field, 'help_text', None) description = force_real_str(field.help_text) if field.help_text else None
description = force_real_str(help_text) if help_text else None
description = description if swagger_object_type != openapi.Items else None # Items has no description either description = description if swagger_object_type != openapi.Items else None # Items has no description either
def SwaggerType(existing_object=None, **instance_kwargs): def SwaggerType(existing_object=None, **instance_kwargs):
@ -342,9 +341,6 @@ class ViewInspector(BaseInspector):
#: methods that are assumed to require a request body determined by the view's ``serializer_class`` #: methods that are assumed to require a request body determined by the view's ``serializer_class``
implicit_body_methods = ('PUT', 'PATCH', 'POST') implicit_body_methods = ('PUT', 'PATCH', 'POST')
#: methods which are assumed to return a list of objects when present on non-detail endpoints
implicit_list_response_methods = ('GET',)
# real values set in __init__ to prevent import errors # real values set in __init__ to prevent import errors
field_inspectors = [] #: field_inspectors = [] #:
filter_inspectors = [] #: filter_inspectors = [] #:
@ -378,30 +374,20 @@ class ViewInspector(BaseInspector):
""" """
raise NotImplementedError("ViewInspector must implement get_operation()!") raise NotImplementedError("ViewInspector must implement get_operation()!")
def is_list_view(self): # methods below provided as default implementations for probing inspectors
"""Determine whether this view is a list or a detail view. The difference between the two is that
detail views depend on a pk/id path parameter. Note that a non-detail view does not necessarily imply a list
reponse (:meth:`.has_list_response`), nor are list responses limited to non-detail views.
For example, one might have a `/topic/<pk>/posts` endpoint which is a detail view that has a list response.
:rtype: bool"""
return is_list_view(self.path, self.method, self.view)
def has_list_response(self):
"""Determine whether this view returns multiple objects. By default this is any non-detail view
(see :meth:`.is_list_view`) whose request method is one of :attr:`.implicit_list_response_methods`.
:rtype: bool
"""
return self.is_list_view() and (self.method.upper() in self.implicit_list_response_methods)
def should_filter(self): def should_filter(self):
"""Determine whether filter backend parameters should be included for this request. """Determine whether filter backend parameters should be included for this request.
:rtype: bool :rtype: bool
""" """
return getattr(self.view, 'filter_backends', None) and self.has_list_response() if not getattr(self.view, 'filter_backends', None):
return False
if self.method.lower() not in ["get", "delete"]:
return False
return is_list_view(self.path, self.method, self.view)
def get_filter_parameters(self): def get_filter_parameters(self):
"""Return the parameters added to the view by its filter backends. """Return the parameters added to the view by its filter backends.
@ -422,7 +408,13 @@ class ViewInspector(BaseInspector):
:rtype: bool :rtype: bool
""" """
return getattr(self.view, 'paginator', None) and self.has_list_response() if not getattr(self.view, 'paginator', None):
return False
if self.method.lower() != 'get':
return False
return is_list_view(self.path, self.method, self.view)
def get_pagination_parameters(self): def get_pagination_parameters(self):
"""Return the parameters added to the view by its paginator. """Return the parameters added to the view by its paginator.

View File

@ -2,7 +2,6 @@ import datetime
import inspect import inspect
import logging import logging
import operator import operator
import sys
import uuid import uuid
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
@ -14,9 +13,7 @@ from rest_framework.settings import api_settings as rest_framework_settings
from .. import openapi from .. import openapi
from ..errors import SwaggerGenerationError from ..errors import SwaggerGenerationError
from ..utils import ( from ..utils import decimal_as_float, filter_none, get_serializer_class, get_serializer_ref_name
decimal_as_float, field_value_to_representation, filter_none, get_serializer_class, get_serializer_ref_name
)
from .base import FieldInspector, NotHandled, SerializerInspector, call_view_method from .base import FieldInspector, NotHandled, SerializerInspector, call_view_method
try: try:
@ -382,21 +379,8 @@ def find_limits(field):
def decimal_field_type(field): def decimal_field_type(field):
return openapi.TYPE_NUMBER if decimal_as_float(field) else openapi.TYPE_STRING return openapi.TYPE_NUMBER if decimal_as_float(field) else openapi.TYPE_STRING
def recurse_one_to_one(field, visited_set=None):
if visited_set is None:
visited_set = set()
if field in visited_set:
return None #cycle?
if isinstance(field, models.OneToOneField):
tgt = field.target_field
visited_set.add(field)
return recurse_one_to_one(tgt, visited_set=visited_set)
else:
tmp = get_basic_type_info(field)
return tmp['type']
model_field_to_basic_type = [ model_field_to_basic_type = [
(models.OneToOneField, (recurse_one_to_one, None)),
(models.AutoField, (openapi.TYPE_INTEGER, None)), (models.AutoField, (openapi.TYPE_INTEGER, None)),
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)), (models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
(models.BooleanField, (openapi.TYPE_BOOLEAN, None)), (models.BooleanField, (openapi.TYPE_BOOLEAN, None)),
@ -503,10 +487,6 @@ hinting_type_info = [
(datetime.date, (openapi.TYPE_STRING, openapi.FORMAT_DATE)), (datetime.date, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
] ]
if sys.version_info < (3, 0):
# noinspection PyUnresolvedReferences
hinting_type_info.append((unicode, (openapi.TYPE_STRING, None))) # noqa: F821
if typing: if typing:
def inspect_collection_hint_class(hint_class): def inspect_collection_hint_class(hint_class):
args = hint_class.__args__ args = hint_class.__args__
@ -545,14 +525,11 @@ def get_basic_type_info_from_hint(hint_class):
:rtype: OrderedDict :rtype: OrderedDict
""" """
union_types = _get_union_types(hint_class) union_types = _get_union_types(hint_class)
if typing and union_types: if typing and union_types:
# Optional is implemented as Union[T, None] # Optional is implemented as Union[T, None]
if len(union_types) == 2 and isinstance(None, union_types[1]): if len(union_types) == 2 and isinstance(None, union_types[1]):
result = get_basic_type_info_from_hint(union_types[0]) result = get_basic_type_info_from_hint(union_types[0])
if result: result['x-nullable'] = True
result['x-nullable'] = True
return result return result
return None return None
@ -658,24 +635,13 @@ class ChoiceFieldInspector(FieldInspector):
if isinstance(field, serializers.ChoiceField): if isinstance(field, serializers.ChoiceField):
enum_type = openapi.TYPE_STRING enum_type = openapi.TYPE_STRING
enum_values = [] enum_values = list(field.choices.keys())
for choice in field.choices.keys():
if isinstance(field, serializers.MultipleChoiceField):
choice = field_value_to_representation(field, [choice])[0]
else:
choice = field_value_to_representation(field, choice)
enum_values.append(choice)
# for ModelSerializer, try to infer the type from the associated model field # for ModelSerializer, try to infer the type from the associated model field
serializer = get_parent_serializer(field) serializer = get_parent_serializer(field)
if isinstance(serializer, serializers.ModelSerializer): if isinstance(serializer, serializers.ModelSerializer):
model = getattr(getattr(serializer, 'Meta'), 'model') model = getattr(getattr(serializer, 'Meta'), 'model')
# Use the parent source for nested fields model_field = get_model_field(model, field.source)
model_field = get_model_field(model, field.source or field.parent.source)
# If the field has a base_field its type must be used
if getattr(model_field, "base_field", None):
model_field = model_field.base_field
if model_field: if model_field:
model_type = get_basic_type_info(model_field) model_type = get_basic_type_info(model_field)
if model_type: if model_type:
@ -760,23 +726,11 @@ class HiddenFieldInspector(FieldInspector):
return NotHandled return NotHandled
class JSONFieldInspector(FieldInspector):
"""Provides conversion for ``JSONField``."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, serializers.JSONField) and swagger_object_type == openapi.Schema:
return SwaggerType(type=openapi.TYPE_OBJECT)
return NotHandled
class StringDefaultFieldInspector(FieldInspector): class StringDefaultFieldInspector(FieldInspector):
"""For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects.""" """For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover
# TODO unhandled fields: TimeField # TODO unhandled fields: TimeField JSONField
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
return SwaggerType(type=openapi.TYPE_STRING) return SwaggerType(type=openapi.TYPE_STRING)

View File

@ -9,7 +9,7 @@ from .. import openapi
from ..errors import SwaggerGenerationError from ..errors import SwaggerGenerationError
from ..utils import ( from ..utils import (
filter_none, force_real_str, force_serializer_instance, get_consumes, get_produces, guess_response_status, filter_none, force_real_str, force_serializer_instance, get_consumes, get_produces, guess_response_status,
merge_params, no_body, param_list_to_odict is_list_view, merge_params, no_body, param_list_to_odict
) )
from .base import ViewInspector, call_view_method from .base import ViewInspector, call_view_method
@ -17,15 +17,12 @@ logger = logging.getLogger(__name__)
class SwaggerAutoSchema(ViewInspector): class SwaggerAutoSchema(ViewInspector):
def __init__(self, view, path, method, components, request, overrides, operation_keys=None): def __init__(self, view, path, method, components, request, overrides):
super(SwaggerAutoSchema, self).__init__(view, path, method, components, request, overrides) super(SwaggerAutoSchema, self).__init__(view, path, method, components, request, overrides)
self._sch = AutoSchema() self._sch = AutoSchema()
self._sch.view = view self._sch.view = view
self.operation_keys = operation_keys
def get_operation(self, operation_keys=None):
operation_keys = operation_keys or self.operation_keys
def get_operation(self, operation_keys):
consumes = self.get_consumes() consumes = self.get_consumes()
produces = self.get_produces() produces = self.get_produces()
@ -211,7 +208,7 @@ class SwaggerAutoSchema(ViewInspector):
default_schema = self.serializer_to_schema(default_schema) or '' default_schema = self.serializer_to_schema(default_schema) or ''
if default_schema: if default_schema:
if self.has_list_response(): if is_list_view(self.path, self.method, self.view) and self.method.lower() == 'get':
default_schema = openapi.Schema(type=openapi.TYPE_ARRAY, items=default_schema) default_schema = openapi.Schema(type=openapi.TYPE_ARRAY, items=default_schema)
if self.should_page(): if self.should_page():
default_schema = self.get_paginated_response(default_schema) or default_schema default_schema = self.get_paginated_response(default_schema) or default_schema
@ -303,7 +300,7 @@ class SwaggerAutoSchema(ViewInspector):
return natural_parameters + serializer_parameters return natural_parameters + serializer_parameters
def get_operation_id(self, operation_keys=None): def get_operation_id(self, operation_keys):
"""Return an unique ID for this operation. The ID must be unique across """Return an unique ID for this operation. The ID must be unique across
all :class:`.Operation` objects in the API. all :class:`.Operation` objects in the API.
@ -311,8 +308,6 @@ class SwaggerAutoSchema(ViewInspector):
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc. of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: str :rtype: str
""" """
operation_keys = operation_keys or self.operation_keys
operation_id = self.overrides.get('operation_id', '') operation_id = self.overrides.get('operation_id', '')
if not operation_id: if not operation_id:
operation_id = '_'.join(operation_keys) operation_id = '_'.join(operation_keys)
@ -374,7 +369,7 @@ class SwaggerAutoSchema(ViewInspector):
""" """
return self.overrides.get('deprecated', None) return self.overrides.get('deprecated', None)
def get_tags(self, operation_keys=None): def get_tags(self, operation_keys):
"""Get a list of tags for this operation. Tags determine how operations relate with each other, and in the UI """Get a list of tags for this operation. Tags determine how operations relate with each other, and in the UI
each tag will show as a group containing the operations that use it. If not provided in overrides, each tag will show as a group containing the operations that use it. If not provided in overrides,
tags will be inferred from the operation url. tags will be inferred from the operation url.
@ -383,8 +378,6 @@ class SwaggerAutoSchema(ViewInspector):
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc. of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: list[str] :rtype: list[str]
""" """
operation_keys = operation_keys or self.operation_keys
tags = self.overrides.get('tags') tags = self.overrides.get('tags')
if not tags: if not tags:
tags = [operation_keys[0]] tags = [operation_keys[0]]

View File

@ -131,7 +131,7 @@ class Command(BaseCommand):
if user: if user:
# Only call get_user_model if --user was passed in order to # Only call get_user_model if --user was passed in order to
# avoid crashing if auth is not configured in the project # avoid crashing if auth is not configured in the project
user = get_user_model().objects.get(**{get_user_model().USERNAME_FIELD: user}) user = get_user_model().objects.get(username=user)
mock = mock or private or (user is not None) or (api_version is not None) mock = mock or private or (user is not None) or (api_version is not None)
if mock and not api_url: if mock and not api_url:

View File

@ -470,7 +470,7 @@ class Schema(SwaggerDict):
:type properties: dict[str,Schema or SchemaRef] :type properties: dict[str,Schema or SchemaRef]
:param additional_properties: allow wildcard properties not listed in `properties` :param additional_properties: allow wildcard properties not listed in `properties`
:type additional_properties: bool or Schema or SchemaRef :type additional_properties: bool or Schema or SchemaRef
:param list[str] required: list of required property names :param list[str] required: list of requried property names
:param items: type of array items, only valid if `type` is ``array`` :param items: type of array items, only valid if `type` is ``array``
:type items: Schema or SchemaRef :type items: Schema or SchemaRef
:param default: only valid when insider another ``Schema``\\ 's ``properties``; :param default: only valid when insider another ``Schema``\\ 's ``properties``;

View File

@ -2,7 +2,7 @@ import six
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.encoding import force_str from django.utils.encoding import force_text
from django.utils.functional import Promise from django.utils.functional import Promise
from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer
from rest_framework.utils import encoders, json from rest_framework.utils import encoders, json
@ -124,7 +124,7 @@ class SwaggerUIRenderer(_UIRenderer):
swagger_ui_settings = self.get_swagger_ui_settings() swagger_ui_settings = self.get_swagger_ui_settings()
request = renderer_context.get('request', None) request = renderer_context.get('request', None)
oauth_redirect_url = force_str(swagger_ui_settings.get('oauth2RedirectUrl', '')) oauth_redirect_url = force_text(swagger_ui_settings.get('oauth2RedirectUrl', ''))
if request and oauth_redirect_url: if request and oauth_redirect_url:
swagger_ui_settings['oauth2RedirectUrl'] = request.build_absolute_uri(oauth_redirect_url) swagger_ui_settings['oauth2RedirectUrl'] = request.build_absolute_uri(oauth_redirect_url)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,14 +1,12 @@
import inspect import inspect
import logging import logging
import sys import sys
import textwrap
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal
from django.db import models from django.db import models
from django.utils.encoding import force_str from django.utils.encoding import force_text
from rest_framework import serializers, status from rest_framework import serializers, status
from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.parsers import FileUploadParser from rest_framework.parsers import FileUploadParser
from rest_framework.request import is_form_media_type from rest_framework.request import is_form_media_type
from rest_framework.settings import api_settings as rest_framework_settings from rest_framework.settings import api_settings as rest_framework_settings
@ -96,8 +94,8 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
* a ``Serializer`` class or instance will be converted into a :class:`.Schema` and treated as above * a ``Serializer`` class or instance will be converted into a :class:`.Schema` and treated as above
* a :class:`.Response` object will be used as-is; however if its ``schema`` attribute is a ``Serializer``, * a :class:`.Response` object will be used as-is; however if its ``schema`` attribute is a ``Serializer``,
it will automatically be converted into a :class:`.Schema` it will automatically be converted into a :class:`.Schema`
:type responses: dict[int or str, (drf_yasg.openapi.Schema or drf_yasg.openapi.SchemaRef or :type responses: dict[str,(drf_yasg.openapi.Schema or drf_yasg.openapi.SchemaRef or drf_yasg.openapi.Response or
drf_yasg.openapi.Response or str or rest_framework.serializers.Serializer)] str or rest_framework.serializers.Serializer)]
:param list[type[drf_yasg.inspectors.FieldInspector]] field_inspectors: extra serializer and field inspectors; these :param list[type[drf_yasg.inspectors.FieldInspector]] field_inspectors: extra serializer and field inspectors; these
will be tried before :attr:`.ViewInspector.field_inspectors` on the :class:`.inspectors.SwaggerAutoSchema` will be tried before :attr:`.ViewInspector.field_inspectors` on the :class:`.inspectors.SwaggerAutoSchema`
@ -167,7 +165,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
if len(available_http_methods) > 1: if len(available_http_methods) > 1:
assert _methods, \ assert _methods, \
"on multi-method api_view or action, you must specify " \ "on multi-method api_view, action, detail_route or list_route, you must specify " \
"swagger_auto_schema on a per-method basis using one of the `method` or `methods` arguments" "swagger_auto_schema on a per-method basis using one of the `method` or `methods` arguments"
else: else:
# for a single-method view we assume that single method as the decorator target # for a single-method view we assume that single method as the decorator target
@ -180,8 +178,8 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
view_method._swagger_auto_schema = existing_data view_method._swagger_auto_schema = existing_data
else: else:
assert not _methods, \ assert not _methods, \
"the methods argument should only be specified when decorating an action; " \ "the methods argument should only be specified when decorating an action, detail_route or " \
"you should also ensure that you put the swagger_auto_schema decorator " \ "list_route; you should also ensure that you put the swagger_auto_schema decorator " \
"AFTER (above) the _route decorator" "AFTER (above) the _route decorator"
assert not existing_data, "swagger_auto_schema applied twice to method" assert not existing_data, "swagger_auto_schema applied twice to method"
view_method._swagger_auto_schema = data view_method._swagger_auto_schema = data
@ -216,7 +214,7 @@ def is_list_view(path, method, view):
:param APIView view: target view :param APIView view: target view
:rtype: bool :rtype: bool
""" """
# for ViewSets, it could be the default 'list' action, or an @action(detail=False) # for ViewSets, it could be the default 'list' action, or a list_route
action = getattr(view, 'action', '') action = getattr(view, 'action', '')
method = getattr(view, action, None) or method method = getattr(view, action, None) or method
detail = getattr(method, 'detail', None) detail = getattr(method, 'detail', None)
@ -228,9 +226,6 @@ def is_list_view(path, method, view):
# a detail action is surely not a list route # a detail action is surely not a list route
return False return False
if isinstance(view, ListModelMixin):
return True
# for GenericAPIView, if it's a detail view it can't also be a list view # for GenericAPIView, if it's a detail view it can't also be a list view
if isinstance(view, (RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin)): if isinstance(view, (RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin)):
return False return False
@ -375,16 +370,10 @@ def get_consumes(parser_classes):
parser_classes = [pc for pc in parser_classes if not issubclass(pc, FileUploadParser)] parser_classes = [pc for pc in parser_classes if not issubclass(pc, FileUploadParser)]
media_types = [parser.media_type for parser in parser_classes or []] media_types = [parser.media_type for parser in parser_classes or []]
non_form_media_types = [encoding for encoding in media_types if not is_form_media_type(encoding)] non_form_media_types = [encoding for encoding in media_types if not is_form_media_type(encoding)]
# Because swagger Parameter objects don't support complex data types (nested objects, arrays),
# we can't use those unless we are sure the view *only* accepts form data
# This means that a view won't support file upload in swagger unless it explicitly
# sets its parser classes to include only form parsers
if len(non_form_media_types) == 0: if len(non_form_media_types) == 0:
return media_types return media_types
else:
# If the form accepts both form data and another type, like json (which is the default config), return non_form_media_types
# we will render its input as a Schema and thus it file parameters will be read-only
return non_form_media_types
def get_produces(renderer_classes): def get_produces(renderer_classes):
@ -441,35 +430,13 @@ def force_real_str(s, encoding='utf-8', strings_only=False, errors='strict'):
Fix for https://github.com/axnsan12/drf-yasg/issues/159 Fix for https://github.com/axnsan12/drf-yasg/issues/159
""" """
if s is not None: if s is not None:
s = force_str(s, encoding, strings_only, errors) s = force_text(s, encoding, strings_only, errors)
if type(s) != str: if type(s) != str:
s = '' + s s = '' + s
# Remove common indentation to get the correct Markdown rendering
s = textwrap.dedent(s)
return s return s
def field_value_to_representation(field, value):
"""Convert a python value related to a field (default, choices, etc.) into its OpenAPI-compatible representation.
:param serializers.Field field: field associated with the value
:param object value: value
:return: the converted value
"""
value = field.to_representation(value)
if isinstance(value, Decimal):
if decimal_as_float(field):
value = float(value)
else:
value = str(value)
# JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists
return json.loads(json.dumps(value, cls=encoders.JSONEncoder))
def get_field_default(field): def get_field_default(field):
""" """
Get the default value for a field, converted to a JSON-compatible value while properly handling callables. Get the default value for a field, converted to a JSON-compatible value while properly handling callables.
@ -483,10 +450,7 @@ def get_field_default(field):
try: try:
if hasattr(default, 'set_context'): if hasattr(default, 'set_context'):
default.set_context(field) default.set_context(field)
if getattr(default, 'requires_context', False): default = default()
default = default(field)
else:
default = default()
except Exception: # pragma: no cover except Exception: # pragma: no cover
logger.warning("default for %s is callable but it raised an exception when " logger.warning("default for %s is callable but it raised an exception when "
"called; 'default' will not be set on schema", field, exc_info=True) "called; 'default' will not be set on schema", field, exc_info=True)
@ -494,7 +458,12 @@ def get_field_default(field):
if default is not serializers.empty and default is not None: if default is not serializers.empty and default is not None:
try: try:
default = field_value_to_representation(field, default) default = field.to_representation(default)
# JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists
default = json.loads(json.dumps(default, cls=encoders.JSONEncoder))
if decimal_as_float(field):
default = float(default)
except Exception: # pragma: no cover except Exception: # pragma: no cover
logger.warning("'default' on schema for %s will not be set because " logger.warning("'default' on schema for %s will not be set because "
"to_representation raised an exception", field, exc_info=True) "to_representation raised an exception", field, exc_info=True)

View File

@ -1,7 +1,8 @@
import warnings import warnings
from functools import WRAPPER_ASSIGNMENTS, wraps from functools import wraps
from django.utils.cache import add_never_cache_headers from django.utils.cache import add_never_cache_headers
from django.utils.decorators import available_attrs
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers from django.views.decorators.vary import vary_on_headers
from rest_framework import exceptions from rest_framework import exceptions
@ -29,7 +30,7 @@ def deferred_never_cache(view_func):
never be cached. never be cached.
""" """
@wraps(view_func, assigned=WRAPPER_ASSIGNMENTS) @wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view_func(request, *args, **kwargs): def _wrapped_view_func(request, *args, **kwargs):
response = view_func(request, *args, **kwargs) response = view_func(request, *args, **kwargs)

View File

@ -1,4 +1,4 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from articles.models import Article, ArticleGroup from articles.models import Article, ArticleGroup

View File

@ -1,11 +1,13 @@
import datetime import datetime
import functools
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets from rest_framework import viewsets
# noinspection PyDeprecation
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from rest_framework.parsers import FileUploadParser, MultiPartParser from rest_framework.parsers import MultiPartParser, FileUploadParser
from rest_framework.response import Response from rest_framework.response import Response
from articles import serializers from articles import serializers
@ -91,10 +93,17 @@ class ArticleViewSet(viewsets.ModelViewSet):
swagger_schema = NoTitleAutoSchema swagger_schema = NoTitleAutoSchema
from rest_framework.decorators import action try:
from rest_framework.decorators import action
list_route = functools.partial(action, detail=False)
detail_route = functools.partial(action, detail=True)
except ImportError:
# TODO: remove when dropping support for DRF 3.7
action = None
from rest_framework.decorators import list_route, detail_route
@swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector]) @swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector])
@action(detail=False, methods=['get']) @list_route(methods=['get'])
def today(self, request): def today(self, request):
today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min) today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max) today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max)
@ -109,7 +118,7 @@ class ArticleViewSet(viewsets.ModelViewSet):
type=openapi.TYPE_INTEGER, type=openapi.TYPE_INTEGER,
description="this should not crash (form parameter on DELETE method)" description="this should not crash (form parameter on DELETE method)"
)]) )])
@action(detail=True, methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser, FileUploadParser)) @detail_route(methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser, FileUploadParser))
def image(self, request, slug=None): def image(self, request, slug=None):
""" """
image method docstring image method docstring

View File

@ -1,19 +1,13 @@
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.pagination import BasePagination
from .models import Identity, Person from .models import Identity, Person
from .serializers import IdentitySerializer, PersonSerializer from .serializers import IdentitySerializer, PersonSerializer
class UnknownPagination(BasePagination):
paginator_query_args = ['unknown_paginator']
class PersonViewSet(viewsets.ModelViewSet): class PersonViewSet(viewsets.ModelViewSet):
model = Person model = Person
queryset = Person.objects queryset = Person.objects
serializer_class = PersonSerializer serializer_class = PersonSerializer
pagination_class = UnknownPagination
class IdentityViewSet(viewsets.ModelViewSet): class IdentityViewSet(viewsets.ModelViewSet):

View File

@ -1,24 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-16 14:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('snippets', '0002_auto_20181219_1016'),
]
operations = [
migrations.CreateModel(
name='SnippetViewer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('snippet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='viewers', to='snippets.Snippet')),
('viewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snippet_views', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 2.2.2 on 2019-06-12 22:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('snippets', '0003_snippetviewer'),
]
operations = [
migrations.AlterField(
model_name='snippet',
name='language',
field=models.CharField(choices=[('cpp', 'cpp'), ('js', 'js'), ('python', 'python')], default='python', max_length=100),
),
migrations.AlterField(
model_name='snippet',
name='style',
field=models.CharField(choices=[('monokai', 'monokai'), ('solarized-dark', 'solarized-dark'), ('vim', 'vim')], default='solarized-dark', max_length=100),
),
]

View File

@ -1,7 +1,10 @@
from django.db import models from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
LANGUAGE_CHOICES = sorted((item, item) for item in ('cpp', 'python', 'js')) LEXERS = [item for item in get_all_lexers() if item[1]]
STYLE_CHOICES = sorted((item, item) for item in ('solarized-dark', 'monokai', 'vim')) LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
class Snippet(models.Model): class Snippet(models.Model):
@ -11,12 +14,7 @@ class Snippet(models.Model):
code = models.TextField(help_text="code model help text") code = models.TextField(help_text="code model help text")
linenos = models.BooleanField(default=False) linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100) language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='solarized-dark', max_length=100) style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
class Meta: class Meta:
ordering = ('created',) ordering = ('created',)
class SnippetViewer(models.Model):
snippet = models.ForeignKey(Snippet, on_delete=models.CASCADE, related_name='viewers')
viewer = models.ForeignKey('auth.User', related_name='snippet_views', on_delete=models.CASCADE)

View File

@ -1,16 +1,10 @@
from decimal import Decimal from decimal import Decimal
import rest_framework
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from packaging.version import Version
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import MaxLengthValidator, MinValueValidator
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet
if Version(rest_framework.__version__) < Version('3.10'):
from rest_framework.compat import MaxLengthValidator, MinValueValidator
else:
from django.core.validators import MaxLengthValidator, MinValueValidator
class LanguageSerializer(serializers.Serializer): class LanguageSerializer(serializers.Serializer):
@ -23,7 +17,7 @@ class LanguageSerializer(serializers.Serializer):
class ExampleProjectSerializer(serializers.Serializer): class ExampleProjectSerializer(serializers.Serializer):
project_name = serializers.CharField(label='project name custom title', help_text='Name of the project') project_name = serializers.CharField(help_text='Name of the project')
github_repo = serializers.CharField(required=True, help_text='Github repository of the project') github_repo = serializers.CharField(required=True, help_text='Github repository of the project')
class Meta: class Meta:
@ -75,7 +69,7 @@ class SnippetSerializer(serializers.Serializer):
tags = serializers.ListField(child=serializers.CharField(min_length=2), min_length=3, max_length=15) tags = serializers.ListField(child=serializers.CharField(min_length=2), min_length=3, max_length=15)
linenos = serializers.BooleanField(required=False) linenos = serializers.BooleanField(required=False)
language = LanguageSerializer(help_text="Sample help text for language") language = LanguageSerializer(help_text="Sample help text for language")
styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['solarized-dark']) styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['friendly'])
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False) lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True, example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True,
validators=[MaxLengthValidator(100)]) validators=[MaxLengthValidator(100)])
@ -106,9 +100,3 @@ class SnippetSerializer(serializers.Serializer):
instance.style = validated_data.get('style', instance.style) instance.style = validated_data.get('style', instance.style)
instance.save() instance.save()
return instance return instance
class SnippetViewerSerializer(serializers.ModelSerializer):
class Meta:
model = SnippetViewer
fields = '__all__'

View File

@ -8,13 +8,10 @@ if django.VERSION[:2] >= (2, 0):
urlpatterns = [ urlpatterns = [
path('', views.SnippetList.as_view()), path('', views.SnippetList.as_view()),
path('<int:pk>/', views.SnippetDetail.as_view()), path('<int:pk>/', views.SnippetDetail.as_view()),
path('views/<int:snippet_pk>/', views.SnippetViewerList.as_view()),
] ]
else: else:
from django.conf.urls import url from django.conf.urls import url
urlpatterns = [ urlpatterns = [
url('^$', views.SnippetList.as_view()), url('^$', views.SnippetList.as_view()),
url(r'^(?P<pk>\d+)/$', views.SnippetDetail.as_view()), url(r'^(?P<pk>\d+)/$', views.SnippetDetail.as_view()),
url(r'^views/(?P<snippet_pk>\d+)/$', views.SnippetViewerList.as_view()),
] ]

View File

@ -2,15 +2,13 @@ from djangorestframework_camel_case.parser import CamelCaseJSONParser
from djangorestframework_camel_case.render import CamelCaseJSONRenderer from djangorestframework_camel_case.render import CamelCaseJSONRenderer
from inflection import camelize from inflection import camelize
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.generics import get_object_or_404 from rest_framework.parsers import FormParser, FileUploadParser
from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import FileUploadParser, FormParser
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.inspectors import SwaggerAutoSchema from drf_yasg.inspectors import SwaggerAutoSchema
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from snippets.models import Snippet, SnippetViewer from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, SnippetViewerSerializer from snippets.serializers import SnippetSerializer
class CamelCaseOperationIDAutoSchema(SwaggerAutoSchema): class CamelCaseOperationIDAutoSchema(SwaggerAutoSchema):
@ -95,31 +93,3 @@ class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
"""delete method docstring""" """delete method docstring"""
return super(SnippetDetail, self).patch(request, *args, **kwargs) return super(SnippetDetail, self).patch(request, *args, **kwargs)
class SnippetViewerList(generics.ListAPIView):
"""SnippetViewerList classdoc"""
serializer_class = SnippetViewerSerializer
pagination_class = PageNumberPagination
parser_classes = (FormParser, CamelCaseJSONParser, FileUploadParser)
renderer_classes = (CamelCaseJSONRenderer,)
swagger_schema = CamelCaseOperationIDAutoSchema
lookup_url_kwarg = 'snippet_pk'
def get_object(self):
queryset = Snippet.objects.all()
# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
def get_queryset(self):
return SnippetViewer.objects.filter(snippet=self.get_object())

View File

@ -1,11 +0,0 @@
from drf_yasg import openapi
from drf_yasg.inspectors import NotHandled, PaginatorInspector
class UnknownPaginatorInspector(PaginatorInspector):
def get_paginator_parameters(self, paginator):
if hasattr(paginator, 'paginator_query_args'):
return [openapi.Parameter(name=arg, in_=openapi.IN_QUERY, type=openapi.TYPE_STRING)
for arg in getattr(paginator, 'paginator_query_args')]
return NotHandled

View File

@ -140,12 +140,7 @@ SWAGGER_SETTINGS = {
'clientId': OAUTH2_CLIENT_ID, 'clientId': OAUTH2_CLIENT_ID,
'clientSecret': OAUTH2_CLIENT_SECRET, 'clientSecret': OAUTH2_CLIENT_SECRET,
'appName': OAUTH2_APP_NAME, 'appName': OAUTH2_APP_NAME,
}, }
"DEFAULT_PAGINATOR_INSPECTORS": [
'testproj.inspectors.UnknownPaginatorInspector',
'drf_yasg.inspectors.DjangoRestResponsePagination',
'drf_yasg.inspectors.CoreAPICompatInspector',
]
} }
REDOC_SETTINGS = { REDOC_SETTINGS = {
@ -193,6 +188,16 @@ LOGGING = {
'propagate': False, 'propagate': False,
}, },
'django': { 'django': {
'handlers': ['console_log'],
'level': 'DEBUG',
'propagate': False,
},
'django.db.backends': {
'handlers': ['console_log'],
'level': 'INFO',
'propagate': False,
},
'django.template': {
'handlers': ['console_log'], 'handlers': ['console_log'],
'level': 'INFO', 'level': 'INFO',
'propagate': False, 'propagate': False,

View File

@ -1,21 +0,0 @@
# Generated by Django 2.1.5 on 2019-04-01 00:28
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('todo', '0002_todotree'),
]
operations = [
migrations.CreateModel(
name='Pack',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('size_code', models.DecimalField(choices=[(Decimal('50'), '5x10'), (Decimal('100'), '10x10'), (Decimal('200'), '10x20')], decimal_places=3, default=Decimal('200'), max_digits=7)),
],
),
]

View File

@ -1,5 +1,3 @@
from decimal import Decimal
from django.db import models from django.db import models
@ -20,19 +18,3 @@ class TodoYetAnother(models.Model):
class TodoTree(models.Model): class TodoTree(models.Model):
parent = models.ForeignKey('self', on_delete=models.CASCADE, related_name='children', null=True) parent = models.ForeignKey('self', on_delete=models.CASCADE, related_name='children', null=True)
title = models.CharField(max_length=50) title = models.CharField(max_length=50)
class Pack(models.Model):
SIZE_10x20 = Decimal(200.000)
SIZE_10x10 = Decimal(100.000)
SIZE_5x10 = Decimal(50.000)
size_code_choices = (
(SIZE_5x10, '5x10'),
(SIZE_10x10, '10x10'),
(SIZE_10x20, '10x20'),
)
size_code = models.DecimalField(max_digits=7,
decimal_places=3,
choices=size_code_choices,
default=SIZE_10x20)

View File

@ -4,7 +4,7 @@ from django.utils import timezone
from rest_framework import serializers from rest_framework import serializers
from rest_framework_recursive.fields import RecursiveField from rest_framework_recursive.fields import RecursiveField
from .models import Pack, Todo, TodoAnother, TodoTree, TodoYetAnother from .models import Todo, TodoAnother, TodoTree, TodoYetAnother
class TodoSerializer(serializers.ModelSerializer): class TodoSerializer(serializers.ModelSerializer):
@ -57,14 +57,3 @@ class TodoRecursiveSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = TodoTree model = TodoTree
fields = ('id', 'title', 'parent', 'parent_id') fields = ('id', 'title', 'parent', 'parent_id')
class HarvestSerializer(serializers.ModelSerializer):
class Meta:
model = Pack
fields = (
'size_code',
)
read_only_fields = (
'size_code',
)

View File

@ -9,7 +9,6 @@ router.register(r'another', views.TodoAnotherViewSet)
router.register(r'yetanother', views.TodoYetAnotherViewSet) router.register(r'yetanother', views.TodoYetAnotherViewSet)
router.register(r'tree', views.TodoTreeView) router.register(r'tree', views.TodoTreeView)
router.register(r'recursive', views.TodoRecursiveView) router.register(r'recursive', views.TodoRecursiveView)
router.register(r'harvest', views.HarvestViewSet)
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -1,13 +1,11 @@
from rest_framework import mixins, permissions, viewsets from rest_framework import viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.generics import RetrieveAPIView from rest_framework.generics import RetrieveAPIView
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from .models import Pack, Todo, TodoAnother, TodoTree, TodoYetAnother from .models import Todo, TodoAnother, TodoTree, TodoYetAnother
from .serializer import ( from .serializer import (
HarvestSerializer, TodoAnotherSerializer, TodoRecursiveSerializer, TodoSerializer, TodoTreeSerializer, TodoAnotherSerializer, TodoRecursiveSerializer, TodoSerializer, TodoTreeSerializer, TodoYetAnotherSerializer
TodoYetAnotherSerializer
) )
@ -77,16 +75,3 @@ class TodoRecursiveView(viewsets.ModelViewSet):
@swagger_auto_schema(responses={200: TodoRecursiveSerializer(many=True)}) @swagger_auto_schema(responses={200: TodoRecursiveSerializer(many=True)})
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
return super(TodoRecursiveView, self).list(request, *args, **kwargs) return super(TodoRecursiveView, self).list(request, *args, **kwargs)
class HarvestViewSet(mixins.ListModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
queryset = Pack.objects.all()
serializer_class = HarvestSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [TokenAuthentication]
def perform_update(self, serializer):
pass

View File

@ -3,7 +3,7 @@ import sys
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.db import migrations, IntegrityError, transaction from django.db import migrations, IntegrityError
def add_default_user(apps, schema_editor): def add_default_user(apps, schema_editor):
@ -13,15 +13,14 @@ def add_default_user(apps, schema_editor):
User = apps.get_model(settings.AUTH_USER_MODEL) User = apps.get_model(settings.AUTH_USER_MODEL)
try: try:
with transaction.atomic(): admin = User(
admin = User( username=username,
username=username, email=email,
email=email, password=make_password(password),
password=make_password(password), is_superuser=True,
is_superuser=True, is_staff=True
is_staff=True )
) admin.save()
admin.save()
except IntegrityError: except IntegrityError:
sys.stdout.write(" User '%s <%s>' already exists..." % (username, email)) sys.stdout.write(" User '%s <%s>' already exists..." % (username, email))
else: else:

View File

@ -1,5 +1,3 @@
import sys
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import serializers from rest_framework import serializers
@ -8,10 +6,7 @@ from snippets.models import Snippet
try: try:
import typing # noqa: F401 import typing # noqa: F401
if sys.version_info >= (3, 4): from .method_serializers_with_typing import MethodFieldExampleSerializer
from .method_serializers_with_typing import MethodFieldExampleSerializer
else:
from .method_serializers_without_typing import MethodFieldExampleSerializer
except ImportError: except ImportError:
from .method_serializers_without_typing import MethodFieldExampleSerializer from .method_serializers_without_typing import MethodFieldExampleSerializer

View File

@ -1,14 +1,11 @@
swagger: '2.0' swagger: '2.0'
info: info:
title: Snippets API title: Snippets API
description: |- description: "This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg)\
This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library. \ Django Rest Framework library.\n\nThe `swagger-ui` view can be found [here](/cached/swagger).\
\ \nThe `ReDoc` view can be found [here](/cached/redoc). \nThe swagger YAML\
The `swagger-ui` view can be found [here](/cached/swagger). \ document can be found [here](/cached/swagger.yaml). \n\nYou can log in using\
The `ReDoc` view can be found [here](/cached/redoc). \ the pre-existing `admin` user with password `passwordadmin`."
The swagger YAML document can be found [here](/cached/swagger.yaml).
You can log in using the pre-existing `admin` user with password `passwordadmin`.
termsOfService: https://www.google.com/policies/terms/ termsOfService: https://www.google.com/policies/terms/
contact: contact:
email: contact@snippets.local email: contact@snippets.local
@ -279,10 +276,7 @@ paths:
get: get:
operationId: people_list operationId: people_list
description: '' description: ''
parameters: parameters: []
- name: unknown_paginator
in: query
type: string
responses: responses:
'200': '200':
description: '' description: ''
@ -446,46 +440,6 @@ paths:
tags: tags:
- snippets - snippets
parameters: [] parameters: []
/snippets/views/{snippet_pk}/:
get:
operationId: snippetsViewsRead
description: SnippetViewerList classdoc
parameters:
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/SnippetViewer'
tags:
- snippets
parameters:
- name: snippet_pk
in: path
required: true
type: string
/snippets/{id}/: /snippets/{id}/:
get: get:
operationId: snippetsRead operationId: snippetsRead
@ -598,60 +552,6 @@ paths:
description: A unique integer value identifying this todo another. description: A unique integer value identifying this todo another.
required: true required: true
type: integer type: integer
/todo/harvest/:
get:
operationId: todo_harvest_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/Harvest'
tags:
- todo
parameters: []
/todo/harvest/{id}/:
put:
operationId: todo_harvest_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Harvest'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Harvest'
tags:
- todo
patch:
operationId: todo_harvest_partial_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Harvest'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Harvest'
tags:
- todo
parameters:
- name: id
in: path
description: A unique integer value identifying this pack.
required: true
type: integer
/todo/recursive/: /todo/recursive/:
get: get:
operationId: todo_recursive_list operationId: todo_recursive_list
@ -1116,7 +1016,7 @@ definitions:
type: object type: object
properties: properties:
projectName: projectName:
title: project name custom title title: Project name
description: Name of the project description: Name of the project
type: string type: string
minLength: 1 minLength: 1
@ -1182,9 +1082,443 @@ definitions:
description: The name of the programming language description: The name of the programming language
type: string type: string
enum: enum:
- abap
- abnf
- ada
- adl
- agda
- aheui
- ahk
- alloy
- ampl
- antlr
- antlr-as
- antlr-cpp
- antlr-csharp
- antlr-java
- antlr-objc
- antlr-perl
- antlr-python
- antlr-ruby
- apacheconf
- apl
- applescript
- arduino
- as
- as3
- aspectj
- aspx-cs
- aspx-vb
- asy
- at
- autoit
- awk
- basemake
- bash
- bat
- bbcode
- bc
- befunge
- bib
- blitzbasic
- blitzmax
- bnf
- boo
- boogie
- brainfuck
- bro
- bst
- bugs
- c
- c-objdump
- ca65
- cadl
- camkes
- capdl
- capnp
- cbmbas
- ceylon
- cfc
- cfengine3
- cfm
- cfs
- chai
- chapel
- cheetah
- cirru
- clay
- clean
- clojure
- clojurescript
- cmake
- cobol
- cobolfree
- coffee-script
- common-lisp
- componentpascal
- console
- control
- coq
- cpp - cpp
- cpp-objdump
- cpsa
- cr
- crmsh
- croc
- cryptol
- csharp
- csound
- csound-document
- csound-score
- css
- css+django
- css+erb
- css+genshitext
- css+lasso
- css+mako
- css+mozpreproc
- css+myghty
- css+php
- css+smarty
- cucumber
- cuda
- cypher
- cython
- d
- d-objdump
- dart
- delphi
- dg
- diff
- django
- docker
- doscon
- dpatch
- dtd
- duel
- dylan
- dylan-console
- dylan-lid
- earl-grey
- easytrieve
- ebnf
- ec
- ecl
- eiffel
- elixir
- elm
- emacs
- erb
- erl
- erlang
- evoque
- extempore
- ezhil
- factor
- fan
- fancy
- felix
- fennel
- fish
- flatline
- forth
- fortran
- fortranfixed
- foxpro
- fsharp
- gap
- gas
- genshi
- genshitext
- glsl
- gnuplot
- go
- golo
- gooddata-cl
- gosu
- groff
- groovy
- gst
- haml
- handlebars
- haskell
- haxeml
- hexdump
- hlsl
- hsail
- html
- html+cheetah
- html+django
- html+evoque
- html+genshi
- html+handlebars
- html+lasso
- html+mako
- html+myghty
- html+ng2
- html+php
- html+smarty
- html+twig
- html+velocity
- http
- hx
- hybris
- hylang
- i6t
- idl
- idris
- iex
- igor
- inform6
- inform7
- ini
- io
- ioke
- irc
- isabelle
- j
- jags
- jasmin
- java
- javascript+mozpreproc
- jcl
- jlcon
- js - js
- js+cheetah
- js+django
- js+erb
- js+genshitext
- js+lasso
- js+mako
- js+myghty
- js+php
- js+smarty
- jsgf
- json
- json-object
- jsonld
- jsp
- julia
- juttle
- kal
- kconfig
- koka
- kotlin
- lagda
- lasso
- lcry
- lean
- less
- lhs
- lidr
- lighty
- limbo
- liquid
- live-script
- llvm
- logos
- logtalk
- lsl
- lua
- make
- mako
- maql
- mask
- mason
- mathematica
- matlab
- matlabsession
- md
- minid
- modelica
- modula2
- monkey
- monte
- moocode
- moon
- mozhashpreproc
- mozpercentpreproc
- mql
- mscgen
- mupad
- mxml
- myghty
- mysql
- nasm
- ncl
- nemerle
- nesc
- newlisp
- newspeak
- ng2
- nginx
- nim
- nit
- nixos
- nsis
- numpy
- nusmv
- objdump
- objdump-nasm
- objective-c
- objective-c++
- objective-j
- ocaml
- octave
- odin
- ooc
- opa
- openedge
- pacmanconf
- pan
- parasail
- pawn
- perl
- perl6
- php
- pig
- pike
- pkgconfig
- plpgsql
- postgresql
- postscript
- pot
- pov
- powershell
- praat
- prolog
- properties
- protobuf
- ps1con
- psql
- pug
- puppet
- py3tb
- pycon
- pypylog
- pytb
- python - python
- python3
- qbasic
- qml
- qvto
- racket
- ragel
- ragel-c
- ragel-cpp
- ragel-d
- ragel-em
- ragel-java
- ragel-objc
- ragel-ruby
- raw
- rb
- rbcon
- rconsole
- rd
- rebol
- red
- redcode
- registry
- resource
- rexx
- rhtml
- rnc
- roboconf-graph
- roboconf-instances
- robotframework
- rql
- rsl
- rst
- rts
- rust
- sas
- sass
- sc
- scala
- scaml
- scheme
- scilab
- scss
- shen
- silver
- slim
- smali
- smalltalk
- smarty
- sml
- snobol
- snowball
- sourceslist
- sp
- sparql
- spec
- splus
- sql
- sqlite3
- squidconf
- ssp
- stan
- stata
- swift
- swig
- systemverilog
- tads3
- tap
- tasm
- tcl
- tcsh
- tcshcon
- tea
- termcap
- terminfo
- terraform
- tex
- text
- thrift
- todotxt
- trac-wiki
- treetop
- ts
- tsql
- turtle
- twig
- typoscript
- typoscriptcssdata
- typoscripthtmldata
- urbiscript
- vala
- vb.net
- vcl
- vclsnippets
- vctreestatus
- velocity
- verilog
- vgl
- vhdl
- vim
- wdiff
- whiley
- x10
- xml
- xml+cheetah
- xml+django
- xml+erb
- xml+evoque
- xml+lasso
- xml+mako
- xml+myghty
- xml+php
- xml+smarty
- xml+velocity
- xorg.conf
- xquery
- xslt
- xtend
- xul+mozpreproc
- yaml
- yaml+jinja
- zephir
default: python default: python
readOnlyNullable: readOnlyNullable:
title: Read only nullable title: Read only nullable
@ -1197,11 +1531,37 @@ definitions:
items: items:
type: string type: string
enum: enum:
- abap
- algol
- algol_nu
- arduino
- autumn
- borland
- bw
- colorful
- default
- emacs
- friendly
- fruity
- igor
- lovelace
- manni
- monokai - monokai
- solarized-dark - murphy
- native
- paraiso-dark
- paraiso-light
- pastie
- perldoc
- rainbow_dash
- rrt
- tango
- trac
- vim - vim
- vs
- xcode
default: default:
- solarized-dark - friendly
lines: lines:
type: array type: array
items: items:
@ -1230,22 +1590,6 @@ definitions:
format: decimal format: decimal
default: 0.0 default: 0.0
minimum: 0.0 minimum: 0.0
SnippetViewer:
required:
- snippet
- viewer
type: object
properties:
id:
title: ID
type: integer
readOnly: true
snippet:
title: Snippet
type: integer
viewer:
title: Viewer
type: integer
Todo: Todo:
required: required:
- title - title
@ -1269,17 +1613,6 @@ definitions:
minLength: 1 minLength: 1
todo: todo:
$ref: '#/definitions/Todo' $ref: '#/definitions/Todo'
Harvest:
type: object
properties:
size_code:
title: Size code
type: string
enum:
- '50'
- '100'
- '200'
readOnly: true
TodoRecursive: TodoRecursive:
required: required:
- title - title
@ -1505,9 +1838,7 @@ definitions:
readOnly: true readOnly: true
help_text_example_3: help_text_example_3:
title: Help text example 3 title: Help text example 3
description: |2 description: "\n docstring is set so should appear in swagger as fallback\n\
\ :return:\n "
docstring is set so should appear in swagger as fallback
:return:
type: integer type: integer
readOnly: true readOnly: true

View File

@ -1,13 +1,9 @@
import json import json
import sys
from collections import OrderedDict from collections import OrderedDict
import pytest import pytest
from django.conf.urls import url from django.conf.urls import url
from django.contrib.postgres import fields as postgres_fields
from django.db import models
from django.utils.inspect import get_func_args from django.utils.inspect import get_func_args
from django_fake_model import models as fake_models
from rest_framework import routers, serializers, viewsets from rest_framework import routers, serializers, viewsets
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
@ -18,11 +14,6 @@ from drf_yasg.errors import SwaggerGenerationError
from drf_yasg.generators import OpenAPISchemaGenerator from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
try:
import typing
except ImportError:
typing = None
def test_schema_is_valid(swagger, codec_yaml): def test_schema_is_valid(swagger, codec_yaml):
codec_yaml.encode(swagger) codec_yaml.encode(swagger)
@ -213,7 +204,6 @@ def test_action_mapping():
@pytest.mark.parametrize('choices, expected_type', [ @pytest.mark.parametrize('choices, expected_type', [
(['A', 'B'], openapi.TYPE_STRING), (['A', 'B'], openapi.TYPE_STRING),
([u'A', u'B'], openapi.TYPE_STRING),
([123, 456], openapi.TYPE_INTEGER), ([123, 456], openapi.TYPE_INTEGER),
([1.2, 3.4], openapi.TYPE_NUMBER), ([1.2, 3.4], openapi.TYPE_NUMBER),
(['A', 456], openapi.TYPE_STRING) (['A', 456], openapi.TYPE_STRING)
@ -239,116 +229,3 @@ def test_choice_field(choices, expected_type):
property_schema = swagger['definitions']['Detail']['properties']['detail'] property_schema = swagger['definitions']['Detail']['properties']['detail']
assert property_schema == openapi.Schema(title='Detail', type=expected_type, enum=choices) assert property_schema == openapi.Schema(title='Detail', type=expected_type, enum=choices)
@pytest.mark.parametrize('choices, field, expected_type', [
([1, 2, 3], models.IntegerField, openapi.TYPE_INTEGER),
(["A", "B"], models.CharField, openapi.TYPE_STRING),
])
def test_nested_choice_in_array_field(choices, field, expected_type):
# Create a model class on the fly to avoid warnings about using the several
# model class name several times
model_class = type(
"%sModel" % field.__name__,
(fake_models.FakeModel,),
{
"array": postgres_fields.ArrayField(
field(choices=((i, "choice %s" % i) for i in choices))
),
"__module__": "test_models",
}
)
class ArraySerializer(serializers.ModelSerializer):
class Meta:
model = model_class
fields = ("array",)
class ArrayViewSet(viewsets.ModelViewSet):
serializer_class = ArraySerializer
router = routers.DefaultRouter()
router.register(r'arrays', ArrayViewSet, **_basename_or_base_name('arrays'))
generator = OpenAPISchemaGenerator(
info=openapi.Info(title='Test array model generator', default_version='v1'),
patterns=router.urls
)
swagger = generator.get_schema(None, True)
property_schema = swagger['definitions']['Array']['properties']['array']['items']
assert property_schema == openapi.Schema(title='Array', type=expected_type, enum=choices)
def test_json_field():
class TestJSONFieldSerializer(serializers.Serializer):
json = serializers.JSONField()
class JSONViewSet(viewsets.ModelViewSet):
serializer_class = TestJSONFieldSerializer
router = routers.DefaultRouter()
router.register(r'jsons', JSONViewSet, **_basename_or_base_name('jsons'))
generator = OpenAPISchemaGenerator(
info=openapi.Info(title='Test json field generator', default_version='v1'),
patterns=router.urls
)
swagger = generator.get_schema(None, True)
property_schema = swagger["definitions"]["TestJSONField"]["properties"]["json"]
assert property_schema == openapi.Schema(title='Json', type=openapi.TYPE_OBJECT)
@pytest.mark.parametrize('py_type, expected_type', [
(str, openapi.TYPE_STRING),
(int, openapi.TYPE_INTEGER),
(float, openapi.TYPE_NUMBER),
(bool, openapi.TYPE_BOOLEAN),
])
@pytest.mark.skipif(typing is None or sys.version_info.major < 3, reason="typing not supported")
def test_optional_return_type(py_type, expected_type):
class OptionalMethodSerializer(serializers.Serializer):
x = serializers.SerializerMethodField()
def get_x(self, instance):
pass
# Add the type annotation here in order to avoid a SyntaxError in py27
get_x.__annotations__["return"] = typing.Optional[py_type]
class OptionalMethodViewSet(viewsets.ViewSet):
@swagger_auto_schema(responses={200: openapi.Response("OK", OptionalMethodSerializer)})
def retrieve(self, request, pk=None):
return Response({'optional': None})
router = routers.DefaultRouter()
router.register(r'optional', OptionalMethodViewSet, **_basename_or_base_name('optional'))
generator = OpenAPISchemaGenerator(
info=openapi.Info(title='Test optional parameter', default_version='v1'),
patterns=router.urls
)
swagger = generator.get_schema(None, True)
property_schema = swagger["definitions"]["OptionalMethod"]["properties"]["x"]
assert property_schema == openapi.Schema(title='X', type=expected_type, readOnly=True)
EXPECTED_DESCRIPTION = """\
description: |-
This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library.
The `swagger-ui` view can be found [here](/cached/swagger).
The `ReDoc` view can be found [here](/cached/redoc).
The swagger YAML document can be found [here](/cached/swagger.yaml).
You can log in using the pre-existing `admin` user with password `passwordadmin`.
"""
def test_multiline_strings(call_generate_swagger):
output = call_generate_swagger(format='yaml')
print("|\n|".join(output.splitlines()[:20]))
assert EXPECTED_DESCRIPTION in output

37
tox.ini
View File

@ -1,15 +1,14 @@
[tox] [tox]
minversion = 3.3.0 minversion = 3.3.0
isolated_build = true isolated_build = true
isolated_build_env = .package isolated_build_env=.package
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
envlist = envlist =
py36-django{111,22}-drf{38,39}, py{27,34,35,36}-django111-drf{37,38,39},
py37-django22-drf{38,39,310,311}, py{34,35,36,37}-django20-drf{37,38,39},
py38-django{22,3}-drf{310,311}, py{35,36,37}-django21-drf{37,38,39},
djmaster, lint, docs djmaster, lint, docs
skip_missing_interpreters = true
[testenv:.package] [testenv:.package]
# no additional dependencies besides PEP 517 # no additional dependencies besides PEP 517
@ -18,30 +17,16 @@ deps =
[testenv] [testenv]
deps = deps =
django111: Django>=1.11,<2.0 django111: Django>=1.11,<2.0
django111: django-oauth-toolkit>=1.1.0,<1.2.0 django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2 django21: Django>=2.1,<2.2
django21: django-oauth-toolkit>=1.2.0
django22: Django>=2.2,<2.3 drf37: djangorestframework>=3.7.7,<3.8
django22: django-oauth-toolkit>=1.2.0 drf38: djangorestframework>=3.8.0,<3.9
django3: Django>=2.2,<2.3
django3: django-oauth-toolkit>=1.2.0
drf38: djangorestframework>=3.8,<3.9
drf39: djangorestframework>=3.9,<3.10 drf39: djangorestframework>=3.9,<3.10
drf310: djangorestframework>=3.10,<3.11
drf311: djangorestframework>=3.11,<3.12
typing: typing>=3.6.6 # test with the latest build of django-rest-framework to get early warning of compatibility issues
# test with the latest builds of Django and django-rest-framework
# to get early warning of compatibility issues
djmaster: https://github.com/django/django/archive/master.tar.gz
djmaster: https://github.com/ottoyiu/django-cors-headers/archive/master.tar.gz
djmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz djmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
djmaster: django-oauth-toolkit>=1.2.0 djmaster: https://github.com/django/django/archive/master.tar.gz
# other dependencies # other dependencies
-r requirements/validation.txt -r requirements/validation.txt
@ -87,6 +72,6 @@ known_standard_library =
types,warnings types,warnings
known_third_party = known_third_party =
coreapi,coreschema,datadiff,dj_database_url,django,django_filters,djangorestframework_camel_case, coreapi,coreschema,datadiff,dj_database_url,django,django_filters,djangorestframework_camel_case,
rest_framework_recursive,flex,gunicorn,inflection,pytest,rest_framework,ruamel,setuptools_scm, rest_framework_recursive,flex,gunicorn,inflection,pygments,pytest,rest_framework,ruamel,setuptools_scm,
swagger_spec_validator,uritemplate,user_agents,whitenoise,oauth2_provider,packaging swagger_spec_validator,uritemplate,user_agents,whitenoise,oauth2_provider
known_first_party = drf_yasg,testproj,articles,people,snippets,todo,users,urlconfs known_first_party = drf_yasg,testproj,articles,people,snippets,todo,users,urlconfs