Compare commits

..

39 Commits

Author SHA1 Message Date
Pietro Brenna 728c02356c fix #559: recursively resolve type of one2one using target_field 2020-03-18 19:15:18 +01:00
Cristi Vîjdea 9ccf24c27a Add 1.17.1 changelog 2020-02-17 03:40:09 +02:00
Cristi Vîjdea 8aa255cf56 Drop Python 2.7 tests 2020-02-17 03:40:00 +02:00
Cristi Vîjdea 7491d330a8 Fix ugettext_lazy warning 2020-02-17 03:12:59 +02:00
Cristi Vîjdea ebe21b77c6 Fix tox.ini 2020-02-17 03:12:07 +02:00
Cristi Vîjdea 17da098940 Fix lint errors 2020-02-17 03:06:37 +02:00
Cristi Vîjdea a872eb66d6 Add Django 3.0, DRF 3.11, drop Python 3.5, Django 2.1 2020-02-17 02:58:55 +02:00
johnthagen 6a1166deb5 Add example for using swagger_schema_fields for a Field (#494)
* Add example for using swagger_schema_fields for a Field
* Mention that Meta class can be added to fields as well
* Reference the DRF docs on how to add validation to serializers
2019-11-16 18:27:03 +02:00
Yannick Chabbert b700191f46 add comments on why we returns non form media types by default (#436) 2019-11-14 15:03:11 +02:00
Jethro Lee 5c25ecd8f2 Edit type check for swagger_auto_schema (#490) 2019-11-14 14:57:49 +02:00
johnthagen 8fd27664f1 Fix typo in docstring (#479) 2019-11-14 14:17:53 +02:00
yurihs 456b697ca2 Apply dedent to descriptions (#416) (#464) 2019-11-14 14:13:40 +02:00
johnthagen 9966297f87 Support Python 3.8 (#477)
* Support Python 3.8
* Add Python 3.8 trove classifier
* Add Python 3.8 support to README
2019-11-14 14:01:55 +02:00
Carlos Martinez 27007a9cf4 Fix #485 (#486) 2019-11-14 00:13:29 +02:00
Ned Batchelder a72e5b2899 Write multi-line strings in block style (#466)
Closes  #439
2019-10-03 02:06:05 +03:00
Cristi Vîjdea 13311582ea Add 1.17.0 changelog
Closes #412
2019-10-03 02:02:02 +03:00
Aliaksei Urbanski 9a89d8ccb0 Improve testing (#415)
These changes:

 - Fix the lint and the djmaster jobs
 - Fix compatibility with upcoming Django 3.0
 - Replace jobs with matrix in .travis.yml
 - Add a test job for Python 3.8
 - Allow running tests on any branch
2019-10-03 01:38:02 +03:00
Cristi Vîjdea 16f67cd8c2 Fix CHANGELOG.rst syntax
Fixes #425
2019-09-29 19:15:42 +03:00
Cristi Vîjdea 99fa7c25ca Add missing newline for #444 2019-09-29 19:08:37 +03:00
grumbling-tom 97e70d9d16 custom_spec.rst: Update to indicate swagger_auto_schema import location. (#444) 2019-09-29 19:07:08 +03:00
Myungseo Kang ee086a6eec Fix typo in docs (#460) 2019-09-29 19:03:05 +03:00
johnthagen 4af38c970a Fix typo (#456) 2019-09-29 19:02:50 +03:00
Ilya Stepin 95337f85ad custom_spec: fix typo in docs (#447) 2019-09-29 19:02:37 +03:00
Étienne Noss 1352c2a23b fix Optional typing hint for SerializerMethodField (#428) 2019-09-29 19:01:28 +03:00
Cristi Vîjdea 8578b93eba Update swagger-ui to 3.23.11 and ReDoc to 2.0.0-rc.14
Supersedes #434
Fixes #398
2019-09-29 18:57:54 +03:00
johnthagen 212891b1b8 Update README to support DRF 3.10 (#424) 2019-09-29 18:51:54 +03:00
Étienne Noss ab6444a32e inspectors: add support for JSONField (#417) 2019-07-19 14:13:26 +03:00
Cristi Vijdea 2e0f9a19a9 Update swagger-ui to 3.23.1 2019-07-16 21:22:44 +03:00
Cristi Vijdea cda808fe11 Fix isort errors 2019-07-16 21:20:46 +03:00
Cristi Vijdea e6219ab8b7 Add packaging to requirements 2019-07-16 21:19:19 +03:00
Cristi Vijdea bc931677dc Add 1.16.1 changelog 2019-07-16 20:59:36 +03:00
tfranzel 1904b0499e Fix imports for DRF 3.10 (#408)
Fixes #410
Fixes #411
2019-07-16 20:39:44 +03:00
johnthagen 1e380fe68b Fix variable formatting (#406) 2019-07-15 12:14:37 +03:00
Étienne Noss 6417bb3770 Handle enum type for nested ChoiceFields (#400) 2019-07-15 12:14:23 +03:00
Cristi Vîjdea e9f27442fc Remove unused import 2019-06-13 16:58:41 +03:00
Cristi Vîjdea cf8b912c10 Align tox matrix to python version 2019-06-13 16:57:15 +03:00
Cristi Vîjdea 3a37c4a019 Drop Django 2.0 and DRF 3.7 support 2019-06-13 16:49:46 +03:00
Cristi Vîjdea 8acab171ea Fix isort
[ci skip]
2019-06-13 16:43:16 +03:00
Cristi Vîjdea acc204e4ea Add test for #382 2019-06-13 16:34:23 +03:00
40 changed files with 790 additions and 407 deletions

View File

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

View File

@ -6,7 +6,7 @@
Contributing
############
Contributions are always welcome and appreciated! Here are some ways you can contribut.
Contributions are always welcome and appreciated! Here are some ways you can contribute.
******
Issues
@ -57,7 +57,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
.. 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
``reference.yaml`` together with your changes.

View File

@ -13,9 +13,9 @@ Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framewor
Compatible with
- **Django Rest Framework**: 3.7.7, 3.8, 3.9
- **Django**: 1.11, 2.0, 2.1, 2.2
- **Python**: 2.7, 3.5, 3.6, 3.7
- **Django Rest Framework**: 3.8, 3.9, 3.10, 3.11
- **Django**: 1.11, 2.2, 3.0
- **Python**: 2.7, 3.6, 3.7, 3.8
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
`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

View File

@ -3,6 +3,38 @@ 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**
**********

View File

@ -45,6 +45,8 @@ some properties of the generated :class:`.Operation`. For example, in a ``ViewSe
.. 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'})
def partial_update(self, request, *args, **kwargs):
"""partial_update method docstring"""
@ -87,14 +89,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
to decorate the *action methods*, i.e. ``list``, ``create``, ``retrieve``, etc. |br|
Additionally, ``@action``\ s, ``@list_route``\ s or ``@detail_route``\ s defined on the viewset, like function based
api views, can respond to multiple HTTP methods and thus have multiple operations that must be decorated separately:
Additionally, ``@action``\ s defined on the viewset, like function based api views, can respond to multiple HTTP
methods and thus have multiple operations that must be decorated separately:
.. code-block:: python
class ArticleViewSet(viewsets.ModelViewSet):
# method or 'methods' can be skipped because the list_route only handles a single method (GET)
# method or 'methods' can be skipped because the action only handles a single method (GET)
@swagger_auto_schema(operation_description='GET /articles/today/')
@action(detail=False, methods=['get'])
def today(self, request):
@ -210,7 +212,8 @@ Schema generation of ``serializers.SerializerMethodField`` is supported in two w
Serializer ``Meta`` nested class
********************************
You can define some per-serializer options by adding a ``Meta`` class to your serializer, e.g.:
You can define some per-serializer or per-field options by adding a ``Meta`` class to your ``Serializer`` or
serializer ``Field``, e.g.:
.. code-block:: python
@ -234,6 +237,64 @@ The available options are:
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>`_.
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
@ -376,7 +437,7 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
class AnotherSerializer(serializers.ModelSerializer):
chilf = OneSerializer()
child = OneSerializer()
class Meta:
model = SomeParentModel
@ -387,3 +448,5 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
.. _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

@ -85,6 +85,7 @@ to this list.
:class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |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.RecursiveFieldInspector' <.inspectors.RecursiveFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.SerializerMethodFieldInspector' <.inspectors.SerializerMethodFieldInspector>`, |br| \
@ -454,7 +455,7 @@ FETCH_SCHEMA_WITH_QUERY
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*: -

506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ with io.open('README.rst', encoding='utf-8') as readme:
requirements = read_req('base.txt')
requirements_validation = read_req('validation.txt')
py3_supported_range = (5, 7)
py3_supported_range = (5, 8)
# convert inclusive range to exclusive range
py3_supported_range = (py3_supported_range[0], py3_supported_range[1] + 1)

View File

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

View File

@ -1,4 +1,4 @@
from six import raise_from
from six import binary_type, raise_from, text_type
import copy
import json
@ -176,7 +176,14 @@ class SaneYamlDumper(yaml.SafeDumper):
node.flow_style = best_style
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_multi_representer(OrderedDict, SaneYamlDumper.represent_odict)

View File

@ -3,13 +3,14 @@ import logging
import re
from collections import OrderedDict, defaultdict
import rest_framework
import uritemplate
from coreapi.compat import urlparse
from packaging.version import Version
from rest_framework import versioning
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 SchemaGenerator, endpoint_ordering, get_pk_name
from rest_framework.schemas.inspectors import get_pk_description
from rest_framework.schemas.generators import endpoint_ordering, get_pk_name
from rest_framework.settings import api_settings
from . import openapi
@ -19,6 +20,14 @@ from .inspectors.field import get_basic_type_info, get_queryset_field, get_query
from .openapi import ReferenceResolver, SwaggerDict
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__)
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')

View File

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

View File

@ -382,8 +382,21 @@ def find_limits(field):
def decimal_field_type(field):
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 = [
(models.OneToOneField, (recurse_one_to_one, None)),
(models.AutoField, (openapi.TYPE_INTEGER, None)),
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
(models.BooleanField, (openapi.TYPE_BOOLEAN, None)),
@ -610,6 +623,8 @@ class SerializerMethodFieldInspector(FieldInspector):
# look for Python 3.5+ style type hinting of the return value
hint_class = inspect_signature(method).return_annotation
if not inspect.isclass(hint_class) and hasattr(hint_class, '__args__'):
hint_class = hint_class.__args__[0]
if inspect.isclass(hint_class) and not issubclass(hint_class, inspect._empty):
type_info = get_basic_type_info_from_hint(hint_class)
@ -656,7 +671,11 @@ class ChoiceFieldInspector(FieldInspector):
serializer = get_parent_serializer(field)
if isinstance(serializer, serializers.ModelSerializer):
model = getattr(getattr(serializer, 'Meta'), 'model')
model_field = get_model_field(model, field.source)
# Use the parent source for nested fields
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:
model_type = get_basic_type_info(model_field)
if model_type:
@ -741,11 +760,23 @@ class HiddenFieldInspector(FieldInspector):
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):
"""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
# TODO unhandled fields: TimeField JSONField
# TODO unhandled fields: TimeField
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
return SwaggerType(type=openapi.TYPE_STRING)

View File

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

View File

@ -470,7 +470,7 @@ class Schema(SwaggerDict):
:type properties: dict[str,Schema or SchemaRef]
:param additional_properties: allow wildcard properties not listed in `properties`
:type additional_properties: bool or Schema or SchemaRef
:param list[str] required: list of requried property names
:param list[str] required: list of required property names
:param items: type of array items, only valid if `type` is ``array``
:type items: Schema or SchemaRef
: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.template.loader import render_to_string
from django.utils.encoding import force_text
from django.utils.encoding import force_str
from django.utils.functional import Promise
from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer
from rest_framework.utils import encoders, json
@ -124,7 +124,7 @@ class SwaggerUIRenderer(_UIRenderer):
swagger_ui_settings = self.get_swagger_ui_settings()
request = renderer_context.get('request', None)
oauth_redirect_url = force_text(swagger_ui_settings.get('oauth2RedirectUrl', ''))
oauth_redirect_url = force_str(swagger_ui_settings.get('oauth2RedirectUrl', ''))
if request and 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,11 +1,12 @@
import inspect
import logging
import sys
import textwrap
from collections import OrderedDict
from decimal import Decimal
from django.db import models
from django.utils.encoding import force_text
from django.utils.encoding import force_str
from rest_framework import serializers, status
from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.parsers import FileUploadParser
@ -95,8 +96,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 :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`
:type responses: dict[str,(drf_yasg.openapi.Schema or drf_yasg.openapi.SchemaRef or drf_yasg.openapi.Response or
str or rest_framework.serializers.Serializer)]
:type responses: dict[int or str, (drf_yasg.openapi.Schema or drf_yasg.openapi.SchemaRef or
drf_yasg.openapi.Response or str or rest_framework.serializers.Serializer)]
: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`
@ -166,7 +167,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
if len(available_http_methods) > 1:
assert _methods, \
"on multi-method api_view, action, detail_route or list_route, you must specify " \
"on multi-method api_view or action, you must specify " \
"swagger_auto_schema on a per-method basis using one of the `method` or `methods` arguments"
else:
# for a single-method view we assume that single method as the decorator target
@ -179,8 +180,8 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
view_method._swagger_auto_schema = existing_data
else:
assert not _methods, \
"the methods argument should only be specified when decorating an action, detail_route or " \
"list_route; you should also ensure that you put the swagger_auto_schema decorator " \
"the methods argument should only be specified when decorating an action; " \
"you should also ensure that you put the swagger_auto_schema decorator " \
"AFTER (above) the _route decorator"
assert not existing_data, "swagger_auto_schema applied twice to method"
view_method._swagger_auto_schema = data
@ -215,7 +216,7 @@ def is_list_view(path, method, view):
:param APIView view: target view
:rtype: bool
"""
# for ViewSets, it could be the default 'list' action, or a list_route
# for ViewSets, it could be the default 'list' action, or an @action(detail=False)
action = getattr(view, 'action', '')
method = getattr(view, action, None) or method
detail = getattr(method, 'detail', None)
@ -374,10 +375,16 @@ def get_consumes(parser_classes):
parser_classes = [pc for pc in parser_classes if not issubclass(pc, FileUploadParser)]
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)]
# 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:
return media_types
else:
return non_form_media_types
# If the form accepts both form data and another type, like json (which is the default config),
# 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):
@ -434,10 +441,13 @@ def force_real_str(s, encoding='utf-8', strings_only=False, errors='strict'):
Fix for https://github.com/axnsan12/drf-yasg/issues/159
"""
if s is not None:
s = force_text(s, encoding, strings_only, errors)
s = force_str(s, encoding, strings_only, errors)
if type(s) != str:
s = '' + s
# Remove common indentation to get the correct Markdown rendering
s = textwrap.dedent(s)
return s
@ -473,7 +483,10 @@ def get_field_default(field):
try:
if hasattr(default, 'set_context'):
default.set_context(field)
default = default()
if getattr(default, 'requires_context', False):
default = default(field)
else:
default = default()
except Exception: # pragma: no cover
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)

View File

@ -1,8 +1,7 @@
import warnings
from functools import wraps
from functools import WRAPPER_ASSIGNMENTS, wraps
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.vary import vary_on_headers
from rest_framework import exceptions
@ -30,7 +29,7 @@ def deferred_never_cache(view_func):
never be cached.
"""
@wraps(view_func, assigned=available_attrs(view_func))
@wraps(view_func, assigned=WRAPPER_ASSIGNMENTS)
def _wrapped_view_func(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)

View File

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

View File

@ -1,10 +1,8 @@
import datetime
import functools
from django.utils.decorators import method_decorator
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
# noinspection PyDeprecation
from rest_framework.filters import OrderingFilter
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.parsers import FileUploadParser, MultiPartParser
@ -93,17 +91,10 @@ class ArticleViewSet(viewsets.ModelViewSet):
swagger_schema = NoTitleAutoSchema
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
from rest_framework.decorators import action
@swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector])
@list_route(methods=['get'])
@action(detail=False, methods=['get'])
def today(self, request):
today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max)
@ -118,7 +109,7 @@ class ArticleViewSet(viewsets.ModelViewSet):
type=openapi.TYPE_INTEGER,
description="this should not crash (form parameter on DELETE method)"
)])
@detail_route(methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser, FileUploadParser))
@action(detail=True, methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser, FileUploadParser))
def image(self, request, slug=None):
"""
image method docstring

View File

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

View File

@ -1,11 +1,17 @@
from decimal import Decimal
import rest_framework
from django.contrib.auth import get_user_model
from packaging.version import Version
from rest_framework import serializers
from rest_framework.compat import MaxLengthValidator, MinValueValidator
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer
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):
name = serializers.ChoiceField(

View File

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

View File

@ -13,9 +13,9 @@ swagger_info = openapi.Info(
default_version='v1',
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).
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`.""", # noqa
terms_of_service="https://www.google.com/policies/terms/",

View File

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

View File

@ -1,11 +1,14 @@
swagger: '2.0'
info:
title: Snippets API
description: "This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg)\
\ 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\
\ document can be found [here](/cached/swagger.yaml). \n\nYou can log in using\
\ the pre-existing `admin` user with password `passwordadmin`."
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`.
termsOfService: https://www.google.com/policies/terms/
contact:
email: contact@snippets.local
@ -276,7 +279,10 @@ paths:
get:
operationId: people_list
description: ''
parameters: []
parameters:
- name: unknown_paginator
in: query
type: string
responses:
'200':
description: ''
@ -1499,7 +1505,9 @@ definitions:
readOnly: true
help_text_example_3:
title: Help text example 3
description: "\n docstring is set so should appear in swagger as fallback\n\
\ :return:\n "
description: |2
docstring is set so should appear in swagger as fallback
:return:
type: integer
readOnly: true

View File

@ -1,9 +1,13 @@
import json
import sys
from collections import OrderedDict
import pytest
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_fake_model import models as fake_models
from rest_framework import routers, serializers, viewsets
from rest_framework.decorators import api_view
from rest_framework.response import Response
@ -14,6 +18,11 @@ from drf_yasg.errors import SwaggerGenerationError
from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.utils import swagger_auto_schema
try:
import typing
except ImportError:
typing = None
def test_schema_is_valid(swagger, codec_yaml):
codec_yaml.encode(swagger)
@ -230,3 +239,116 @@ def test_choice_field(choices, expected_type):
property_schema = swagger['definitions']['Detail']['properties']['detail']
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

34
tox.ini
View File

@ -5,10 +5,11 @@ isolated_build_env = .package
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
envlist =
py27-django111-drf39-typing,
py{27,35,36}-django111-drf{37,38,39},
py{35,36,37}-django{20,21,22}-drf{37,38,39},
py36-django{111,22}-drf{38,39},
py37-django22-drf{38,39,310,311},
py38-django{22,3}-drf{310,311},
djmaster, lint, docs
skip_missing_interpreters = true
[testenv:.package]
# no additional dependencies besides PEP 517
@ -17,19 +18,30 @@ deps =
[testenv]
deps =
django111: Django>=1.11,<2.0
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2
django22: Django>=2.2,<2.3
django111: django-oauth-toolkit>=1.1.0,<1.2.0
drf37: djangorestframework>=3.7.7,<3.8
drf38: djangorestframework>=3.8.0,<3.9
django21: Django>=2.1,<2.2
django21: django-oauth-toolkit>=1.2.0
django22: Django>=2.2,<2.3
django22: django-oauth-toolkit>=1.2.0
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
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
djmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
# 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: django-oauth-toolkit>=1.2.0
# other dependencies
-r requirements/validation.txt
@ -76,5 +88,5 @@ known_standard_library =
known_third_party =
coreapi,coreschema,datadiff,dj_database_url,django,django_filters,djangorestframework_camel_case,
rest_framework_recursive,flex,gunicorn,inflection,pytest,rest_framework,ruamel,setuptools_scm,
swagger_spec_validator,uritemplate,user_agents,whitenoise,oauth2_provider
swagger_spec_validator,uritemplate,user_agents,whitenoise,oauth2_provider,packaging
known_first_party = drf_yasg,testproj,articles,people,snippets,todo,users,urlconfs