Compare commits

...

13 Commits

Author SHA1 Message Date
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
Cristi Vîjdea 1635e5e095 Add 1.16.0 changelog 2019-06-13 13:48:45 +03:00
Cristi Vîjdea 753be1a8bd Fix potential issubclass crash 2019-06-13 13:43:16 +03:00
Pavel White 2656696a0f generators: support custom ReferenceResolver class (#350) 2019-06-13 03:38:17 +03:00
Hugo Duroux a083d3cf7c Add operation_keys to SwaggerAutoSchema constructor (#355) 2019-06-13 03:37:06 +03:00
19 changed files with 153 additions and 44 deletions
+3 -3
View File
@@ -13,8 +13,8 @@ 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
- **Django Rest Framework**: 3.8, 3.9
- **Django**: 1.11, 2.1, 2.2
- **Python**: 2.7, 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.
@@ -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
+20 -1
View File
@@ -2,6 +2,26 @@
Changelog
#########
**********
**1.16.1**
**********
*Release date: Jul 16, 2019*
- **FIXED:** fixed DRF 3.10 compatibility (:pr:`408`, :issue:`410`, :issue:`411`)
- **IMPROVED:** better enum type detection for nested `ChoiceField`s (:pr:`400`)
**********
**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**
**********
@@ -15,7 +35,6 @@ Changelog
**Python 3.4 support has been dropped!**
**********
**1.15.0**
**********
+3 -3
View File
@@ -87,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
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):
+1 -1
View File
@@ -5,5 +5,5 @@ inflection>=0.3.1
six>=1.10.0
uritemplate>=3.0.0
djangorestframework>=3.7.7
djangorestframework>=3.8
Django>=1.11.7
+2
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
+3 -1
View File
@@ -3,7 +3,9 @@ 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>=0.2.0
# tempory replacement of broken lib
-e git+https://github.com/tfranzel/djangorestframework-camel-case.git@bd556d38fa7382acadfe91d93d92d99c663248a9#egg=djangorestframework_camel_case
djangorestframework-recursive>=0.1.2
dj-database-url>=0.4.2
user_agents>=1.1.0
+1 -1
View File
@@ -1 +1 @@
python-3.7.1
python-3.7.3
+14 -4
View File
@@ -4,14 +4,23 @@ import re
from collections import OrderedDict, defaultdict
import uritemplate
import rest_framework
from coreapi.compat import urlparse
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 packaging.version import Version
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
from . import openapi
from .app_settings import swagger_settings
from .errors import SwaggerGenerationError
@@ -160,6 +169,7 @@ class OpenAPISchemaGenerator(object):
Method implementations shamelessly stolen and adapted from rest-framework ``SchemaGenerator``.
"""
endpoint_enumerator_class = EndpointEnumerator
reference_resolver_class = ReferenceResolver
def __init__(self, info, version='', url=None, patterns=None, urlconf=None):
"""
@@ -238,7 +248,7 @@ class OpenAPISchemaGenerator(object):
:rtype: openapi.Swagger
"""
endpoints = self.get_endpoints(request)
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS, force_init=True)
components = self.reference_resolver_class(openapi.SCHEMA_DEFINITIONS, force_init=True)
self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES)
self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES)
paths, prefix = self.get_paths(endpoints, components, request, public)
@@ -440,7 +450,7 @@ class OpenAPISchemaGenerator(object):
if view_inspector_cls is None:
return None
view_inspector = view_inspector_cls(view, path, method, components, request, overrides)
view_inspector = view_inspector_cls(view, path, method, components, request, overrides, operation_keys)
operation = view_inspector.get_operation(operation_keys)
if operation is None:
return None
+6 -2
View File
@@ -610,7 +610,7 @@ class SerializerMethodFieldInspector(FieldInspector):
# look for Python 3.5+ style type hinting of the return value
hint_class = inspect_signature(method).return_annotation
if not issubclass(hint_class, inspect._empty):
if inspect.isclass(hint_class) and not issubclass(hint_class, inspect._empty):
type_info = get_basic_type_info_from_hint(hint_class)
if type_info is not None:
@@ -656,7 +656,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:
+11 -4
View File
@@ -17,12 +17,15 @@ logger = logging.getLogger(__name__)
class SwaggerAutoSchema(ViewInspector):
def __init__(self, view, path, method, components, request, overrides):
def __init__(self, view, path, method, components, request, overrides, operation_keys=None):
super(SwaggerAutoSchema, self).__init__(view, path, method, components, request, overrides)
self._sch = AutoSchema()
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()
produces = self.get_produces()
@@ -300,7 +303,7 @@ class SwaggerAutoSchema(ViewInspector):
return natural_parameters + serializer_parameters
def get_operation_id(self, operation_keys):
def get_operation_id(self, operation_keys=None):
"""Return an unique ID for this operation. The ID must be unique across
all :class:`.Operation` objects in the API.
@@ -308,6 +311,8 @@ class SwaggerAutoSchema(ViewInspector):
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: str
"""
operation_keys = operation_keys or self.operation_keys
operation_id = self.overrides.get('operation_id', '')
if not operation_id:
operation_id = '_'.join(operation_keys)
@@ -369,7 +374,7 @@ class SwaggerAutoSchema(ViewInspector):
"""
return self.overrides.get('deprecated', None)
def get_tags(self, operation_keys):
def get_tags(self, operation_keys=None):
"""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,
tags will be inferred from the operation url.
@@ -378,6 +383,8 @@ class SwaggerAutoSchema(ViewInspector):
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: list[str]
"""
operation_keys = operation_keys or self.operation_keys
tags = self.overrides.get('tags')
if not tags:
tags = [operation_keys[0]]
+4 -4
View File
@@ -166,7 +166,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 +179,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 +215,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)
+3 -12
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
+6
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):
+6 -1
View File
@@ -1,8 +1,13 @@
import rest_framework
from decimal import Decimal
from django.contrib.auth import get_user_model
from packaging.version import Version
if Version(rest_framework.__version__) < Version('3.10'):
from rest_framework.compat import MaxLengthValidator, MinValueValidator
else:
from django.core.validators import MaxLengthValidator, MinValueValidator
from rest_framework import serializers
from rest_framework.compat import MaxLengthValidator, MinValueValidator
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer
+11
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
+6 -1
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 = {
+4 -1
View File
@@ -276,7 +276,10 @@ paths:
get:
operationId: people_list
description: ''
parameters: []
parameters:
- name: unknown_paginator
in: query
type: string
responses:
'200':
description: ''
+43
View File
@@ -3,11 +3,14 @@ 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 rest_framework import routers, serializers, viewsets
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django_fake_model import models as fake_models
from drf_yasg import codecs, openapi
from drf_yasg.codecs import yaml_sane_load
from drf_yasg.errors import SwaggerGenerationError
@@ -230,3 +233,43 @@ 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)
+6 -5
View File
@@ -6,8 +6,10 @@ 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},
py27-django111-drf{38,39},
py{35,36}-django{111,21,22}-drf{38,39},
py37-django{21,22}-drf{38,39},
py37-django{21,22}-drf310,
djmaster, lint, docs
[testenv:.package]
@@ -17,13 +19,12 @@ 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
drf37: djangorestframework>=3.7.7,<3.8
drf38: djangorestframework>=3.8.0,<3.9
drf38: djangorestframework>=3.8,<3.9
drf39: djangorestframework>=3.9,<3.10
drf310: djangorestframework>=3.10
typing: typing>=3.6.6