Compare commits

..

9 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
18 changed files with 127 additions and 36 deletions
+3 -3
View File
@@ -13,8 +13,8 @@ Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framewor
Compatible with Compatible with
- **Django Rest Framework**: 3.7.7, 3.8, 3.9 - **Django Rest Framework**: 3.8, 3.9
- **Django**: 1.11, 2.0, 2.1, 2.2 - **Django**: 1.11, 2.1, 2.2
- **Python**: 2.7, 3.5, 3.6, 3.7 - **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. 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
+9
View File
@@ -3,6 +3,15 @@ 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** **1.16.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 * 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, ``@list_route``\ s or ``@detail_route``\ s defined on the viewset, like function based Additionally, ``@action``\ s defined on the viewset, like function based api views, can respond to multiple HTTP
api views, can respond to multiple HTTP methods and thus have multiple operations that must be decorated separately: 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 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/') @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):
+1 -1
View File
@@ -5,5 +5,5 @@ inflection>=0.3.1
six>=1.10.0 six>=1.10.0
uritemplate>=3.0.0 uritemplate>=3.0.0
djangorestframework>=3.7.7 djangorestframework>=3.8
Django>=1.11.7 Django>=1.11.7
+2
View File
@@ -5,5 +5,7 @@ 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
+3 -1
View File
@@ -3,7 +3,9 @@ Pillow>=4.3.0
django-cors-headers>=2.1.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.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 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
+1 -1
View File
@@ -1 +1 @@
python-3.7.1 python-3.7.3
+11 -2
View File
@@ -4,14 +4,23 @@ import re
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
import uritemplate import uritemplate
import rest_framework
from coreapi.compat import urlparse from coreapi.compat import urlparse
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 SchemaGenerator, endpoint_ordering, get_pk_name from rest_framework.schemas.generators import 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 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 . import openapi
from .app_settings import swagger_settings from .app_settings import swagger_settings
from .errors import SwaggerGenerationError from .errors import SwaggerGenerationError
+5 -1
View File
@@ -656,7 +656,11 @@ class ChoiceFieldInspector(FieldInspector):
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')
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: 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:
+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: if len(available_http_methods) > 1:
assert _methods, \ 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" "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
@@ -179,8 +179,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, detail_route or " \ "the methods argument should only be specified when decorating an action; " \
"list_route; you should also ensure that you put the swagger_auto_schema decorator " \ "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
@@ -215,7 +215,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 a list_route # for ViewSets, it could be the default 'list' action, or an @action(detail=False)
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)
+2 -11
View File
@@ -1,10 +1,8 @@
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 FileUploadParser, MultiPartParser
@@ -93,17 +91,10 @@ class ArticleViewSet(viewsets.ModelViewSet):
swagger_schema = NoTitleAutoSchema swagger_schema = NoTitleAutoSchema
try:
from rest_framework.decorators import action 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])
@list_route(methods=['get']) @action(detail=False, 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)
@@ -118,7 +109,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)"
)]) )])
@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): def image(self, request, slug=None):
""" """
image method docstring image method docstring
+6
View File
@@ -1,13 +1,19 @@
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):
+6 -1
View File
@@ -1,8 +1,13 @@
import rest_framework
from decimal import Decimal from decimal import Decimal
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import serializers from packaging.version import Version
if Version(rest_framework.__version__) < Version('3.10'):
from rest_framework.compat import MaxLengthValidator, MinValueValidator from rest_framework.compat import MaxLengthValidator, MinValueValidator
else:
from django.core.validators import MaxLengthValidator, MinValueValidator
from rest_framework import serializers
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer 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, '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 = {
+4 -1
View File
@@ -276,7 +276,10 @@ 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: ''
+43
View File
@@ -3,11 +3,14 @@ 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 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
from django_fake_model import models as fake_models
from drf_yasg import codecs, openapi from drf_yasg import codecs, openapi
from drf_yasg.codecs import yaml_sane_load from drf_yasg.codecs import yaml_sane_load
from drf_yasg.errors import SwaggerGenerationError from drf_yasg.errors import SwaggerGenerationError
@@ -230,3 +233,43 @@ 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)
+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 # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
envlist = envlist =
py27-django111-drf39-typing, py27-django111-drf39-typing,
py{27,35,36}-django111-drf{37,38,39}, py27-django111-drf{38,39},
py{35,36,37}-django{20,21,22}-drf{37,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 djmaster, lint, docs
[testenv:.package] [testenv:.package]
@@ -17,13 +19,12 @@ deps =
[testenv] [testenv]
deps = deps =
django111: Django>=1.11,<2.0 django111: Django>=1.11,<2.0
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2 django21: Django>=2.1,<2.2
django22: Django>=2.2,<2.3 django22: Django>=2.2,<2.3
drf37: djangorestframework>=3.7.7,<3.8 drf38: djangorestframework>=3.8,<3.9
drf38: djangorestframework>=3.8.0,<3.9
drf39: djangorestframework>=3.9,<3.10 drf39: djangorestframework>=3.9,<3.10
drf310: djangorestframework>=3.10
typing: typing>=3.6.6 typing: typing>=3.6.6