Compare commits

...

13 Commits

Author SHA1 Message Date
Cristi Vîjdea 120c4dd884 Fix IPv4 pattern on Django 1.11 2018-09-13 05:14:08 +03:00
Cristi Vîjdea 5bdce56bba Fix docs build with sphinx 1.8 2018-09-13 05:12:40 +03:00
Cristi Vîjdea e761e2da50 Add 1.10.2 changelog 2018-09-13 04:19:31 +03:00
Cristi Vîjdea 9d330d9509 Use the first line of view method docstring as Operation summary
Closes #205.
2018-09-13 04:18:55 +03:00
Cristi Vîjdea c8f6d16891 Update ReDoc to 2.0.0-alpha.38 2018-09-13 03:44:10 +03:00
Cristi Vîjdea 857ef05b07 Handle reverse_lazy
Fixes #209.
2018-09-13 03:38:13 +03:00
Cristi Vîjdea 586b88d8f3 Allow pattern on any field 2018-09-13 03:34:01 +03:00
Cristi Vîjdea 27cd5f7f2a Add some convenience methods 2018-09-11 02:41:55 +03:00
Cristi Vîjdea 90812f5c43 Refactor get_queryset_from_view 2018-09-10 14:34:30 +03:00
Cristi Vîjdea a6ae8b0521 Add ReDoc settings descriptions 2018-09-10 14:02:37 +03:00
Cristi Vîjdea 91530b64c6 Add DISPLAY_OPERATION_ID setting for swagger-ui
Closes #206.
2018-09-10 14:02:25 +03:00
Cristi Vîjdea 6820d6bc70 Fix RelatedFieldInspector nesting issues 2018-09-10 05:04:03 +03:00
Cristi Vîjdea f50ad29a2b Fix lint errors 2018-09-10 02:48:08 +03:00
20 changed files with 184 additions and 62 deletions
+2 -3
View File
@@ -1,7 +1,8 @@
node_modules/
testproj/db.sqlite3
testproj/staticfiles
.vscode/
\.pytest_cache/
docs/\.doctrees/
# Created by .ignore support plugin (hsz.mobi)
### Python template
@@ -159,5 +160,3 @@ com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
\.pytest_cache/
+1
View File
@@ -18,6 +18,7 @@
<sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.pytest_cache" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/docs/.doctrees" />
<excludeFolder url="file://$MODULE_DIR$/docs/_build" />
<excludeFolder url="file://$MODULE_DIR$/htmlcov" />
<excludeFolder url="file://$MODULE_DIR$/src/drf_yasg.egg-info" />
+15 -1
View File
@@ -3,6 +3,20 @@ Changelog
#########
**********
**1.10.2**
**********
*Release date: Sep 13, 2018*
- **ADDED:** added the ``DISPLAY_OPERATION_ID `` ``swagger-ui`` setting
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-alpha.38
- **IMPROVED:** Operation summary will now be parsed from multi-line view method docstrings (:issue:`205`)
- **IMPROVED:** ``pattern`` will now work on any field with a ``RegexValidator``
(would previously not appear on fields with special formats such as ``EmailField``)
- **FIXED:** fixed an issue with ``RelatedFieldInspector`` handling of nested serializers
- **FIXED:** fixed handling of ``reverse_lazy`` in URL settings (:issue:`209`)
**********
**1.10.1**
**********
@@ -19,7 +33,7 @@ Changelog
- **FIXED:** fixed implementation of ``operation_summary`` and ``deprecated`` (:pr:`194`, :pr:`198`)
- **FIXED:** fixed a bug related to nested ``typing`` hints (:pr:`195`)
- **FIXED:** removed dependency on ``future`` (:issue:`196`)
- **FIXED:** removed exceptions loged for fields with ``default=None`` (:issue:`203`)
- **FIXED:** fixed exceptions logged for fields with ``default=None`` (:issue:`203`)
- **FIXED:** fixed ``request_body=no_body`` handling and related tests (:issue:`188`, :issue:`199`)
+3 -1
View File
@@ -155,7 +155,9 @@ texinfo_documents = [
'Miscellaneous'),
]
autodoc_default_flags = ['private-members']
autodoc_default_options = {
'private-members': None
}
autodoc_member_order = 'bysource'
autoclass_content = 'both'
autodoc_mock_imports = []
+24
View File
@@ -332,6 +332,14 @@ This does not filter the operations from the display.
**Default**: :python:`['get','put','post','delete','options','head','patch','trace']` |br|
*Maps to parameter*: ``supportedSubmitMethods``
DISPLAY_OPERATION_ID
--------------------
Controls the display of operationId in operations list.
**Default**: :python:`True`
*Maps to parameter*: ``displayOperationId``
**************
REDOC_SETTINGS
**************
@@ -356,6 +364,10 @@ URL which serves the UI; see :ref:`note on URL settings <url-settings>` above.
LAZY_RENDERING
--------------
If set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50).
In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing
progress bar on the top.
**NOTE:** this feature might be removed in future versions of ReDoc (see https://github.com/Rebilly/ReDoc/issues/475)
**Default**: :python:`False` |br|
@@ -364,30 +376,42 @@ LAZY_RENDERING
HIDE_HOSTNAME
-------------
If set, the protocol and hostname is not shown in the operation definition.
**Default**: :python:`False` |br|
*Maps to attribute*: ``hideHostname``
EXPAND_RESPONSES
----------------
Specify which responses to expand by default by response codes. Values should be passed as comma-separated list without
spaces e.g. expandResponses="200,201". Special value "all" expands all responses by default.
Be careful: this option can slow-down documentation rendering time.
**Default**: :python:`'all'` |br|
*Maps to attribute*: ``expandResponses``
PATH_IN_MIDDLE
--------------
Show path link and HTTP verb in the middle panel instead of the right one.
**Default**: :python:`False` |br|
*Maps to attribute*: ``pathInMiddlePanel``
NATIVE_SCROLLBARS
-----------------
Use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs).
**Default**: :python:`False` |br|
*Maps to attribute*: ``nativeScrollbars``
REQUIRED_PROPS_FIRST
--------------------
Show required properties first ordered in the same order as in required array.
**Default**: :python:`False` |br|
*Maps to attribute*: ``requiredPropsFirst``
+26 -26
View File
@@ -72,11 +72,11 @@
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
"integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
"requires": {
"ms": "2.0.0"
"ms": "^2.1.1"
}
},
"decko": {
@@ -272,18 +272,18 @@
}
},
"mobx-react": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-5.2.6.tgz",
"integrity": "sha512-AGo3m/ICzN2SEbtNi8ZdF1ShDcAOnEKonZFc3mm8uuF2obDXg6rXqV5mtkr9ZHPYizGp51kT/vRg0TlGzok+CA==",
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-5.2.8.tgz",
"integrity": "sha512-VAkeqkrIpdoy3VPPoqvxjdQmcTT6hi7i3TsZSwcKbSrPbSTuWc7cp1EOiX/zV1wPWEuoNAQ22bCrskQwvdYTJA==",
"requires": {
"hoist-non-react-statics": "^2.5.0",
"react-lifecycles-compat": "^3.0.2"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node-fetch": {
"version": "1.7.3",
@@ -321,9 +321,9 @@
"integrity": "sha512-/2Sk/khljhdrsamjJYS5NjrH+GKEHEwh7zFSiYyxROyYKagkE4kSn2zDQDRTOMo8mpT2jikxx6yI1dG7lNP/hw=="
},
"polished": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/polished/-/polished-1.9.3.tgz",
"integrity": "sha512-4NmSD7fMFlM8roNxs7YXPv7UFRbYzb0gufR5zBxJLRzY54+zFsavxBo6zsQzP9ep6Hh3pC2pTyrpSTBEaB6IkQ=="
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/polished/-/polished-2.0.3.tgz",
"integrity": "sha512-MMwthxXsmZZlZY/x79KX/8kXpSU0lq3ntnDmAdhpQGk9pU9EM/rQmUbsgIuuk2Cvl6zYSWjyBsDEU8sa1598xg=="
},
"postcss-value-parser": {
"version": "3.3.0",
@@ -401,9 +401,9 @@
}
},
"redoc": {
"version": "2.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-alpha.37.tgz",
"integrity": "sha512-yuT8sTT+oNzg8G9PfsUhu8Jhd9rWYJCTSQnbgaq9KzAFcJ2O0XfvlstkGXDK1zmtSSBBe5cy9Z/1XcQGDiGYug==",
"version": "2.0.0-alpha.38",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-alpha.38.tgz",
"integrity": "sha512-4pT/cRJ/aKdLlCxjFOSWvkA9ZVG5b9dUfxIM+H5698hlRaF6rcu4qh29eGwEikSDw0unH0iNd6MHy9u0fAjRGw==",
"requires": {
"classnames": "^2.2.6",
"decko": "^1.2.0",
@@ -418,15 +418,15 @@
"mobx-react": "^5.2.5",
"openapi-sampler": "1.0.0-beta.14",
"perfect-scrollbar": "^1.4.0",
"polished": "^1.9.3",
"polished": "^2.0.2",
"prismjs": "^1.15.0",
"prop-types": "^15.6.2",
"react-dropdown": "^1.6.1",
"react-hot-loader": "^4.3.4",
"react-dropdown": "^1.6.2",
"react-hot-loader": "^4.3.5",
"react-tabs": "^2.0.0",
"slugify": "^1.3.1",
"stickyfill": "^1.1.1",
"styled-components": "^3.4.2",
"styled-components": "^3.4.5",
"tslib": "^1.9.3"
}
},
@@ -467,9 +467,9 @@
"integrity": "sha1-OUE/7p0CXHSn5ZzuyyN4TMDxfwI="
},
"styled-components": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-3.4.5.tgz",
"integrity": "sha512-/4c4/72+QAZ8LO+VIzTcitdjMcJleOln1laK9HZr166O+OmGpKZYt3+hRn0YhYPytwDGfx9J5c7WhU7Oys+nSw==",
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-3.4.6.tgz",
"integrity": "sha512-HiEVVY93oH/PuekPuIE7ownn2wmO708rS8MM1yS20i0TCyFgPs3mbop8fJqKLRtC2/npVn2fO5Je6azaL6T2tg==",
"requires": {
"buffer": "^5.0.3",
"css-to-react-native": "^2.0.3",
@@ -522,9 +522,9 @@
"integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA=="
},
"whatwg-fetch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
"integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
"integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "drf-yasg",
"dependencies": {
"redoc": "^2.0.0-alpha.37",
"redoc": "^2.0.0-alpha.38",
"swagger-ui-dist": "^3.18.2"
},
"repository": {
+1
View File
@@ -63,6 +63,7 @@ SWAGGER_DEFAULTS = {
'patch',
'trace'
],
'DISPLAY_OPERATION_ID': True,
}
REDOC_DEFAULTS = {
+2 -2
View File
@@ -15,7 +15,7 @@ from rest_framework.settings import api_settings as rest_framework_settings
from . import openapi
from .app_settings import swagger_settings
from .errors import SwaggerGenerationError
from .inspectors.field import get_basic_type_info, get_queryset_field
from .inspectors.field import get_basic_type_info, get_queryset_field, get_queryset_from_view
from .openapi import ReferenceResolver
from .utils import force_real_str, get_consumes, get_produces
@@ -424,7 +424,7 @@ class OpenAPISchemaGenerator(object):
:rtype: list[openapi.Parameter]
"""
parameters = []
queryset = getattr(view_cls, 'queryset', None)
queryset = get_queryset_from_view(view_cls)
for variable in sorted(uritemplate.variables(path)):
model, model_field = get_queryset_field(queryset, variable)
+35 -4
View File
@@ -165,6 +165,25 @@ def get_model_field(model, field_name):
return None
def get_queryset_from_view(view, serializer=None):
"""Try to get the queryset of the given view
:param view: the view instance or class
:param serializer: if given, will check that the view's get_serializer_class return matches this serialzier
:return: queryset or ``None``
"""
try:
queryset = getattr(view, 'queryset', None)
if queryset is not None and serializer is not None:
# make sure the view is actually using *this* serializer
assert type(serializer) == view.get_serializer_class()
return queryset
except Exception: # pragma: no cover
return None
def get_parent_serializer(field):
"""Get the nearest parent ``Serializer`` instance for the given field.
@@ -231,10 +250,18 @@ class RelatedFieldInspector(FieldInspector):
else:
# if the RelatedField has no queryset (e.g. read only), try to find the target model
# from the view queryset or ModelSerializer model, if present
view_queryset = getattr(self.view, 'queryset', None)
serializer_meta = getattr(get_parent_serializer(field), 'Meta', None)
this_model = getattr(view_queryset, 'model', None) or getattr(serializer_meta, 'model', None)
parent_serializer = get_parent_serializer(field)
serializer_meta = getattr(parent_serializer, 'Meta', None)
this_model = getattr(serializer_meta, 'model', None)
if not this_model:
view_queryset = get_queryset_from_view(self.view, parent_serializer)
this_model = getattr(view_queryset, 'model', None)
source = getattr(field, 'source', '') or field.field_name
if not source and isinstance(field.parent, serializers.ManyRelatedField):
source = field.parent.field_name
model = get_related_model(this_model, source)
model_field = get_model_field(model, target_field)
@@ -256,6 +283,10 @@ def find_regex(regex_field):
regex_validator = None
for validator in regex_field.validators:
if isinstance(validator, validators.RegexValidator):
if isinstance(validator, validators.URLValidator) or validator == validators.validate_ipv4_address:
# skip the default url and IP regexes because they are complex and unhelpful
# validate_ipv4_address is a RegexValidator instance in Django 1.11
continue
if regex_validator is not None:
# bail if multiple validators are found - no obvious way to choose
return None # pragma: no cover
@@ -401,7 +432,7 @@ def get_basic_type_info(field):
return None
pattern = None
if swagger_type == openapi.TYPE_STRING and format in (None, openapi.FORMAT_SLUG):
if swagger_type == openapi.TYPE_STRING:
pattern = find_regex(field)
limits = find_limits(field)
+25 -11
View File
@@ -8,8 +8,8 @@ from rest_framework.status import is_success
from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import (
force_real_str, force_serializer_instance, get_consumes, get_produces, guess_response_status, is_list_view, no_body,
param_list_to_odict
force_real_str, force_serializer_instance, get_consumes, get_produces, guess_response_status, is_list_view,
merge_params, no_body, param_list_to_odict
)
from .base import ViewInspector
@@ -162,21 +162,19 @@ class SwaggerAutoSchema(ViewInspector):
:return: modified parameters
:rtype: list[openapi.Parameter]
"""
parameters = param_list_to_odict(parameters)
manual_parameters = self.overrides.get('manual_parameters', None) or []
if any(param.in_ == openapi.IN_BODY for param in manual_parameters): # pragma: no cover
raise SwaggerGenerationError("specify the body parameter as a Schema or Serializer in request_body")
if any(param.in_ == openapi.IN_FORM for param in manual_parameters): # pragma: no cover
if any(param.in_ == openapi.IN_BODY for param in parameters.values()):
if any(param.in_ == openapi.IN_BODY for param in parameters):
raise SwaggerGenerationError("cannot add form parameters when the request has a request body; "
"did you forget to set an appropriate parser class on the view?")
if self.method not in self.body_methods:
raise SwaggerGenerationError("form parameters can only be applied to (" + ','.join(self.body_methods) +
") HTTP methods")
parameters.update(param_list_to_odict(manual_parameters))
return list(parameters.values())
return merge_params(parameters, manual_parameters)
def get_responses(self):
"""Get the possible responses for this view as a swagger :class:`.Responses` object.
@@ -323,16 +321,32 @@ class SwaggerAutoSchema(ViewInspector):
operation_id = '_'.join(operation_keys)
return operation_id
def _extract_description_and_summary(self):
description = self.overrides.get('operation_description', None)
summary = self.overrides.get('operation_summary', None)
if description is None:
description = self._sch.get_description(self.path, self.method) or ''
description = description.strip().replace('\r', '')
if description and (summary is None):
# description from docstring ... do summary magic
# https://www.python.org/dev/peps/pep-0257/#multi-line-docstrings
summary_max_len = 120 # OpenAPI 2.0 spec says summary should be under 120 characters
sections = description.split('\n\n', 1)
if len(sections) == 2:
sections[0] = sections[0].strip()
if len(sections[0]) < summary_max_len:
summary, description = sections
return description, summary
def get_description(self):
"""Return an operation description determined as appropriate from the view's method and class docstrings.
:return: the operation description
:rtype: str
"""
description = self.overrides.get('operation_description', None)
if description is None:
description = self._sch.get_description(self.path, self.method)
return description
return self._extract_description_and_summary()[0]
def get_summary(self):
"""Return a summary description for this operation.
@@ -340,7 +354,7 @@ class SwaggerAutoSchema(ViewInspector):
:return: the summary
:rtype: str
"""
return self.overrides.get('operation_summary', None)
return self._extract_description_and_summary()[1]
def get_security(self):
"""Return a list of security requirements for this operation.
+11
View File
@@ -291,6 +291,8 @@ class Paths(SwaggerDict):
class PathItem(SwaggerDict):
OPERATION_NAMES = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']
def __init__(self, get=None, put=None, post=None, delete=None, options=None,
head=None, patch=None, parameters=None, **extra):
"""Information about a single path
@@ -315,6 +317,15 @@ class PathItem(SwaggerDict):
self.parameters = filter_none(parameters)
self._insert_extras__()
@property
def operations(self):
"""A list of all standard Operations on this PathItem object. See :attr:`.OPERATION_NAMES`.
:return: list of (method name, Operation) tuples
:rtype: list[tuple[str,Operation]]
"""
return [(k, v) for k, v in self.items() if k in PathItem.OPERATION_NAMES and v]
class Operation(SwaggerDict):
def __init__(self, operation_id, responses, parameters=None, consumes=None, produces=None, summary=None,
+5 -2
View File
@@ -1,8 +1,7 @@
import warnings
import six
from django.shortcuts import render, resolve_url
from django.utils.functional import Promise
from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer
from rest_framework.utils import json
@@ -83,6 +82,9 @@ class _UIRenderer(BaseRenderer):
renderer_context.update(self.get_auth_urls())
def resolve_url(self, to):
if isinstance(to, Promise):
to = str(to)
if to is None:
return None
@@ -135,6 +137,7 @@ class SwaggerUIRenderer(_UIRenderer):
'showCommonExtensions': swagger_settings.SHOW_COMMON_EXTENSIONS,
'oauth2RedirectUrl': swagger_settings.OAUTH2_REDIRECT_URL,
'supportedSubmitMethods': swagger_settings.SUPPORTED_SUBMIT_METHODS,
'displayOperationId': swagger_settings.DISPLAY_OPERATION_ID,
}
data = filter_none(data)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -32,7 +32,6 @@ function initSwaggerUi() {
var swaggerConfig = {
url: specURL,
dom_id: '#swagger-ui',
displayOperationId: true,
displayRequestDuration: true,
presets: [
SwaggerUIBundle.presets.apis,
+16
View File
@@ -254,6 +254,22 @@ def param_list_to_odict(parameters):
return result
def merge_params(parameters, overrides):
"""Merge `overrides` into `parameters`. This is the same as appending `overrides` to `parameters`, but any element
of `parameters` whose ``(name, in_)`` tuple collides with an element in `overrides` is replaced by it.
Raises an ``AssertionError`` if either list contains duplicate parameters.
:param list[.Parameter] parameters: initial parameters
:param list[.Parameter] overrides: overriding parameters
:return: merged list
:rtype: list[.Parameter]
"""
parameters = param_list_to_odict(parameters)
parameters.update(param_list_to_odict(overrides))
return list(parameters.values())
def filter_none(obj):
"""Remove ``None`` values from tuples, lists or dictionaries. Return other objects as-is.
+4
View File
@@ -46,6 +46,10 @@ class SnippetList(generics.ListCreateAPIView):
),
)
def delete(self, *args, **kwargs):
"""summary from docstring
description body is here, summary is not included
"""
pass
+3 -1
View File
@@ -1,6 +1,8 @@
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
from django.urls import reverse_lazy
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
ALLOWED_HOSTS = [
@@ -91,7 +93,7 @@ REST_FRAMEWORK = {
# drf-yasg
SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login',
'LOGIN_URL': reverse_lazy('admin:login'),
'LOGOUT_URL': '/admin/logout',
'DEFAULT_INFO': 'testproj.urls.swagger_info',
+2 -1
View File
@@ -391,7 +391,8 @@ paths:
- snippets
delete:
operationId: snippetsDeleteBulk
description: SnippetList classdoc
summary: summary from docstring
description: description body is here, summary is not included
parameters:
- name: data
in: body