Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13311582ea | |||
| 9a89d8ccb0 | |||
| 16f67cd8c2 | |||
| 99fa7c25ca | |||
| 97e70d9d16 | |||
| ee086a6eec | |||
| 4af38c970a | |||
| 95337f85ad | |||
| 1352c2a23b | |||
| 8578b93eba | |||
| 212891b1b8 | |||
| ab6444a32e | |||
| 2e0f9a19a9 | |||
| cda808fe11 | |||
| e6219ab8b7 | |||
| bc931677dc | |||
| 1904b0499e | |||
| 1e380fe68b | |||
| 6417bb3770 | |||
| e9f27442fc | |||
| cf8b912c10 | |||
| 3a37c4a019 | |||
| 8acab171ea | |||
| acc204e4ea |
+3
-6
@@ -4,12 +4,13 @@ python:
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
- '3.7'
|
||||
- '3.8-dev'
|
||||
|
||||
dist: xenial
|
||||
|
||||
cache: pip
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
include:
|
||||
- python: '3.6'
|
||||
env: TOXENV=docs
|
||||
@@ -38,6 +39,7 @@ jobs:
|
||||
allow_failures:
|
||||
- env: TOXENV=lint
|
||||
- env: TOXENV=djmaster
|
||||
- python: '3.8-dev'
|
||||
|
||||
fast_finish: true
|
||||
|
||||
@@ -59,11 +61,6 @@ after_success:
|
||||
codecov
|
||||
fi
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v?\d+\.\d+(\.\d+)?(-?\S+)?$/
|
||||
|
||||
stages:
|
||||
- test
|
||||
- name: publish
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-3
@@ -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, 3.10
|
||||
- **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
|
||||
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
Changelog
|
||||
#########
|
||||
|
||||
**********
|
||||
**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**
|
||||
|
||||
@@ -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):
|
||||
@@ -376,7 +378,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
|
||||
|
||||
+2
-1
@@ -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*: -
|
||||
|
||||
|
||||
|
||||
Generated
+260
-246
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# 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>=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
|
||||
# django-oauth-toolkit 1.2 does not support Django 1.11
|
||||
django-oauth-toolkit>=1.1.0,<1.2.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
python-3.7.1
|
||||
python-3.7.3
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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+)}')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -610,6 +610,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 +658,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 +747,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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+41
-19
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
@@ -5,7 +5,7 @@ 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
|
||||
@@ -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)
|
||||
@@ -434,7 +434,7 @@ 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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 = {
|
||||
|
||||
@@ -276,7 +276,10 @@ paths:
|
||||
get:
|
||||
operationId: people_list
|
||||
description: ''
|
||||
parameters: []
|
||||
parameters:
|
||||
- name: unknown_paginator
|
||||
in: query
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
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 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
|
||||
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,98 @@ 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)
|
||||
|
||||
@@ -6,9 +6,12 @@ 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,310},
|
||||
py38-django22-drf310,
|
||||
djmaster, lint, docs
|
||||
skip_missing_interpreters = true
|
||||
|
||||
[testenv:.package]
|
||||
# no additional dependencies besides PEP 517
|
||||
@@ -17,19 +20,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-cors-headers>=2.1.0
|
||||
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-cors-headers>=2.1.0
|
||||
django21: django-oauth-toolkit>=1.2.0
|
||||
|
||||
django22: Django>=2.2,<2.3
|
||||
django22: django-cors-headers>=2.1.0
|
||||
django22: django-oauth-toolkit>=1.2.0
|
||||
|
||||
|
||||
drf38: djangorestframework>=3.8,<3.9
|
||||
drf39: djangorestframework>=3.9,<3.10
|
||||
drf310: djangorestframework>=3.10
|
||||
|
||||
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 +90,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
|
||||
|
||||
Reference in New Issue
Block a user