Compare commits

...

4 Commits

Author SHA1 Message Date
Cristi Vîjdea bdf7e8a4ae Prepare release 1.0.3 2017-12-15 19:25:25 +01:00
Cristi Vîjdea 174f1153b4 Fix broken SwaggerDict pickling (#15)
Closes #14.
2017-12-15 18:47:10 +01:00
Cristi Vîjdea af2a44e1e9 Update documentation about Responses and form data 2017-12-15 12:13:09 +01:00
Cristi Vîjdea f6a535eb45 Do not attempt to generate a response schema for form-only requests
It would probably fail because Schema objects cannot represent files
2017-12-15 11:14:47 +01:00
14 changed files with 163 additions and 23 deletions
-1
View File
@@ -54,7 +54,6 @@
</option> </option>
</inspection_tool> </inspection_tool>
<inspection_tool class="PyShadowingNamesInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="PyShadowingNamesInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnusedLocalInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false"> <inspection_tool class="PyUnusedLocalInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false">
<option name="ignoreTupleUnpacking" value="true" /> <option name="ignoreTupleUnpacking" value="true" />
<option name="ignoreLambdaParameters" value="true" /> <option name="ignoreLambdaParameters" value="true" />
+18
View File
@@ -0,0 +1,18 @@
#########
Changelog
#########
*********
**1.0.3**
*********
- **FIX:** fixed bug that caused schema views returned from cache to fail (:issue:`14`)
- **FIX:** disabled automatic generation of response schemas for form operations to avoid confusing errors caused by
attempting to shove file parameters into Schema objects
*********
**1.0.2**
*********
- First published version
+41
View File
@@ -7,6 +7,9 @@ import os
import sys import sys
import sphinx_rtd_theme import sphinx_rtd_theme
from docutils import nodes, utils
from docutils.parsers.rst import roles
from docutils.parsers.rst.roles import set_classes
from pkg_resources import get_distribution from pkg_resources import get_distribution
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
@@ -204,3 +207,41 @@ import drf_yasg.views # noqa: E402
# instantiate a SchemaView in the views module to make it available to autodoc # instantiate a SchemaView in the views module to make it available to autodoc
drf_yasg.views.SchemaView = drf_yasg.views.get_schema_view(None) drf_yasg.views.SchemaView = drf_yasg.views.get_schema_view(None)
ghiss_uri = "https://github.com/axnsan12/drf-yasg/issues/%d"
ghpr_uri = "https://github.com/axnsan12/drf-yasg/pull/%d"
def role_github_pull_request_or_issue(name, rawtext, text, lineno, inliner, options=None, content=None):
options = options or {}
content = content or []
try:
ghid = int(text)
if ghid <= 0:
raise ValueError
except ValueError:
msg = inliner.reporter.error(
'GitHub pull request or issue number must be a number greater than or equal to 1; '
'"%s" is invalid.' % text, line=lineno
)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
# Base URL mainly used by inliner.rfc_reference, so this is correct:
if name == 'pr':
ref = ghpr_uri
elif name == 'issue':
ref = ghiss_uri
else:
msg = inliner.reporter.error('unknown tag name for GitHub reference - "%s"' % name, line=lineno)
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
ref = ref % ghid
set_classes(options)
node = nodes.reference(rawtext, '#' + utils.unescape(text), refuri=ref, **options)
return [node], []
roles.register_local_role('pr', role_github_pull_request_or_issue)
roles.register_local_role('issue', role_github_pull_request_or_issue)
+21 -3
View File
@@ -50,9 +50,27 @@ is structured, starting from the root ``Swagger`` object.
+ ``operationId`` - should be unique across all operations + ``operationId`` - should be unique across all operations
+ ``tags`` - used to group operations in the listing + ``tags`` - used to group operations in the listing
It is interesting to note that the main difference between ``Parameter`` and ``Schema`` is that Schemas can nest other It is interesting to note the main differences between :class:`.Parameter` and :class:`.Schema` objects:
Schemas, while Parameters are "primitives" and cannot contain other Parameters. The only exception are ``body``
Parameters, which can contain a Schema. +----------------------------------------------------------+-----------------------------------------------------------+
| :class:`.Schema` | :class:`.Parameter` |
+==========================================================+===========================================================+
| Can nest other Schemas | Cannot nest other Parameters |br| |
| | Can only nest a Schema if the parameter is ``in: body`` |
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot describe file uploads |br| | Can describe file uploads via ``type`` = ``file``, |br| |
| - ``file`` is not permitted as a value for ``type`` | but only as part of a form :class:`.Operation` [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
| Can be used in :class:`.Response`\ s | Cannot be used in :class:`.Response`\ s |
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot be used in form :class:`.Operation`\ s [#formop]_ | Can be used in form :class:`.Operation`\ s [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
.. [#formop] a form Operation is an :class:`.Operation` that consumes ``multipart/form-data`` or
``application/x-www-form-urlencoded``
* a form Operation cannot have ``body`` parameters
* a non-form operation cannot have ``form`` parameters
************************************** **************************************
The ``@swagger_auto_schema`` decorator The ``@swagger_auto_schema`` decorator
+1
View File
@@ -57,6 +57,7 @@ drf\_yasg\.openapi
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:exclude-members: _bare_SwaggerDict
drf\_yasg\.renderers drf\_yasg\.renderers
------------------------------ ------------------------------
+1
View File
@@ -17,6 +17,7 @@ drf-yasg
settings.rst settings.rst
contributing.rst contributing.rst
license.rst license.rst
changelog.rst
Source code documentation Source code documentation
========================= =========================
+17 -2
View File
@@ -87,7 +87,7 @@ class _OpenAPICodec(object):
:rtype: OrderedDict :rtype: OrderedDict
""" """
swagger.security_definitions = swagger_settings.SECURITY_DEFINITIONS swagger.security_definitions = swagger_settings.SECURITY_DEFINITIONS
return swagger return swagger.as_odict()
class OpenAPICodecJson(_OpenAPICodec): class OpenAPICodecJson(_OpenAPICodec):
@@ -146,9 +146,24 @@ SaneYamlDumper.add_representer(OrderedDict, SaneYamlDumper.represent_odict)
SaneYamlDumper.add_multi_representer(OrderedDict, SaneYamlDumper.represent_odict) SaneYamlDumper.add_multi_representer(OrderedDict, SaneYamlDumper.represent_odict)
def yaml_sane_dump(data, binary):
"""Dump the given data dictionary into a sane format:
* OrderedDicts are dumped as regular mappings instead of non-standard !!odict
* multi-line mapping style instead of json-like inline style
* list elements are indented into their parents
:param dict data: the data to be serializers
:param bool binary: True to return a utf-8 encoded binary object, False to return a string
:return: the serialized YAML
:rtype: str,bytes
"""
return yaml.dump(data, Dumper=SaneYamlDumper, default_flow_style=False, encoding='utf-8' if binary else None)
class OpenAPICodecYaml(_OpenAPICodec): class OpenAPICodecYaml(_OpenAPICodec):
media_type = 'application/yaml' media_type = 'application/yaml'
def _dump_dict(self, spec): def _dump_dict(self, spec):
"""Dump ``spec`` into YAML.""" """Dump ``spec`` into YAML."""
return yaml.dump(spec, Dumper=SaneYamlDumper, default_flow_style=False, encoding='utf-8') return yaml_sane_dump(spec, binary=True)
+2
View File
@@ -225,6 +225,8 @@ class SwaggerAutoSchema(object):
default_schema = self.get_request_serializer() default_schema = self.get_request_serializer()
default_schema = default_schema or '' default_schema = default_schema or ''
if any(is_form_media_type(encoding) for encoding in self.get_consumes()):
default_schema = ''
if default_schema: if default_schema:
if not isinstance(default_schema, openapi.Schema): if not isinstance(default_schema, openapi.Schema):
default_schema = self.serializer_to_schema(default_schema) default_schema = self.serializer_to_schema(default_schema)
+31 -8
View File
@@ -1,4 +1,3 @@
import copy
from collections import OrderedDict from collections import OrderedDict
from coreapi.compat import urlparse from coreapi.compat import urlparse
@@ -48,8 +47,8 @@ def make_swagger_name(attribute_name):
Convert a python variable name into a Swagger spec attribute name. Convert a python variable name into a Swagger spec attribute name.
In particular, In particular,
* if name starts with x\_, return "x-{camelCase}" * if name starts with ``x_``, return ``x-{camelCase}``
* if name is 'ref', return "$ref" * if name is ``ref``, return ``$ref``
* else return the name converted to camelCase, with trailing underscores stripped * else return the name converted to camelCase, with trailing underscores stripped
:param str attribute_name: python attribute name :param str attribute_name: python attribute name
@@ -62,9 +61,19 @@ def make_swagger_name(attribute_name):
return camelize(attribute_name.rstrip('_'), uppercase_first_letter=False) return camelize(attribute_name.rstrip('_'), uppercase_first_letter=False)
def _bare_SwaggerDict(cls):
assert issubclass(cls, SwaggerDict)
result = cls.__new__(cls)
OrderedDict.__init__(result) # no __init__ called for SwaggerDict subclasses!
return result
class SwaggerDict(OrderedDict): class SwaggerDict(OrderedDict):
"""A particular type of OrderedDict, which maps all attribute accesses to dict lookups using """A particular type of OrderedDict, which maps all attribute accesses to dict lookups using
:func:`.make_swagger_name`. Used as a base class for all Swagger helper models. :func:`.make_swagger_name`. Attribute names starting with ``_`` are set on the object as-is and are not included
in the specification output.
Used as a base class for all Swagger helper models.
""" """
def __init__(self, **attrs): def __init__(self, **attrs):
@@ -105,11 +114,25 @@ class SwaggerDict(OrderedDict):
for attr, val in self._extras__.items(): for attr, val in self._extras__.items():
setattr(self, attr, val) setattr(self, attr, val)
# noinspection PyArgumentList,PyDefaultArgument @staticmethod
def __deepcopy__(self, memodict={}): def _as_odict(obj):
result = OrderedDict(list(self.items())) if isinstance(obj, dict):
result.update(copy.deepcopy(result, memodict)) result = OrderedDict()
for attr, val in obj.items():
result[attr] = SwaggerDict._as_odict(val)
return result return result
elif isinstance(obj, (list, tuple)):
return type(obj)(SwaggerDict._as_odict(elem) for elem in obj)
return obj
def as_odict(self):
return SwaggerDict._as_odict(self)
def __reduce__(self):
# for pickle supprt; this skips calls to all SwaggerDict __init__ methods and relies
# on the already set attributes instead
return _bare_SwaggerDict, (type(self),), vars(self), None, iter(self.items())
class Contact(SwaggerDict): class Contact(SwaggerDict):
+1 -1
View File
@@ -54,7 +54,7 @@ class ArticleViewSet(viewsets.ModelViewSet):
return Response(serializer.data) return Response(serializer.data)
@swagger_auto_schema(method='get', operation_description="image GET description override") @swagger_auto_schema(method='get', operation_description="image GET description override")
@swagger_auto_schema(method='post', request_body=serializers.ImageUploadSerializer, responses={200: 'success'}) @swagger_auto_schema(method='post', request_body=serializers.ImageUploadSerializer)
@detail_route(methods=['get', 'post'], parser_classes=(MultiPartParser,)) @detail_route(methods=['get', 'post'], parser_classes=(MultiPartParser,))
def image(self, request, slug=None): def image(self, request, slug=None):
""" """
+4 -1
View File
@@ -29,7 +29,10 @@ def plain_view(request):
urlpatterns = [ urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'), url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
url(r'^cached/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'),
url(r'^cached/swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
url(r'^cached/redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^snippets/', include('snippets.urls')), url(r'^snippets/', include('snippets.urls')),
+2 -2
View File
@@ -45,8 +45,8 @@ def validate_schema():
from flex.core import parse as validate_flex from flex.core import parse as validate_flex
from swagger_spec_validator.validator20 import validate_spec as validate_ssv from swagger_spec_validator.validator20 import validate_spec as validate_ssv
validate_flex(swagger) validate_flex(copy.deepcopy(swagger))
validate_ssv(swagger) validate_ssv(copy.deepcopy(swagger))
return validate_schema return validate_schema
+2 -4
View File
@@ -187,8 +187,6 @@ paths:
responses: responses:
'200': '200':
description: '' description: ''
schema:
$ref: '#/definitions/Article'
consumes: consumes:
- multipart/form-data - multipart/form-data
tags: tags:
@@ -221,8 +219,8 @@ paths:
required: true required: true
type: file type: file
responses: responses:
'200': '201':
description: success description: ''
consumes: consumes:
- multipart/form-data - multipart/form-data
tags: tags:
+21
View File
@@ -1,4 +1,5 @@
import json import json
from collections import OrderedDict
import pytest import pytest
from ruamel import yaml from ruamel import yaml
@@ -46,6 +47,26 @@ def test_redoc(client, validate_schema):
_validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads) _validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads)
def test_caching(client, validate_schema):
prev_schema = None
for i in range(3):
_validate_ui_schema_view(client, '/cached/redoc/', 'redoc/redoc.min.js')
_validate_text_schema_view(client, validate_schema, '/cached/redoc/?format=openapi', json.loads)
_validate_ui_schema_view(client, '/cached/swagger/', 'swagger-ui-dist/swagger-ui-bundle.js')
_validate_text_schema_view(client, validate_schema, '/cached/swagger/?format=openapi', json.loads)
json_schema = client.get('/cached/swagger.json')
assert json_schema.status_code == 200
json_schema = json.loads(json_schema.content.decode('utf-8'), object_pairs_hook=OrderedDict)
if prev_schema is None:
validate_schema(json_schema)
prev_schema = json_schema
else:
from datadiff.tools import assert_equal
assert_equal(prev_schema, json_schema)
@pytest.mark.urls('urlconfs.non_public_urls') @pytest.mark.urls('urlconfs.non_public_urls')
def test_non_public(client): def test_non_public(client):
response = client.get('/private/swagger.yaml') response = client.get('/private/swagger.yaml')