Skip to content

Commit

Permalink
Fix broken SwaggerDict pickling (#15)
Browse files Browse the repository at this point in the history
Closes #14.
  • Loading branch information
axnsan12 committed Dec 15, 2017
1 parent af2a44e commit 174f115
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 11 deletions.
1 change: 0 additions & 1 deletion .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/drf_yasg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ drf\_yasg\.openapi
:members:
:undoc-members:
:show-inheritance:
:exclude-members: _bare_SwaggerDict

drf\_yasg\.renderers
------------------------------
Expand Down
19 changes: 17 additions & 2 deletions src/drf_yasg/codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def generate_swagger_object(self, swagger):
:rtype: OrderedDict
"""
swagger.security_definitions = swagger_settings.SECURITY_DEFINITIONS
return swagger
return swagger.as_odict()


class OpenAPICodecJson(_OpenAPICodec):
Expand Down Expand Up @@ -146,9 +146,24 @@ def represent_odict(dump, mapping, flow_style=None): # pragma: no cover
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):
media_type = 'application/yaml'

def _dump_dict(self, spec):
"""Dump ``spec`` into YAML."""
return yaml.dump(spec, Dumper=SaneYamlDumper, default_flow_style=False, encoding='utf-8')
return yaml_sane_dump(spec, binary=True)
31 changes: 26 additions & 5 deletions src/drf_yasg/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ def make_swagger_name(attribute_name):
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):
"""A particular type of OrderedDict, which maps all attribute accesses to dict lookups using
:func:`.make_swagger_name`. Attribute names starting with ``_`` are set on the object as-is and are not included
Expand Down Expand Up @@ -108,11 +115,25 @@ def _insert_extras__(self):
for attr, val in self._extras__.items():
setattr(self, attr, val)

# noinspection PyArgumentList,PyDefaultArgument
def __deepcopy__(self, memodict={}):
result = OrderedDict(list(self.items()))
result.update(copy.deepcopy(result, memodict))
return result
@staticmethod
def _as_odict(obj):
if isinstance(obj, dict):
result = OrderedDict()
for attr, val in obj.items():
result[attr] = SwaggerDict._as_odict(val)
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):
Expand Down
5 changes: 4 additions & 1 deletion testproj/testproj/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ def plain_view(request):
urlpatterns = [
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'^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'^snippets/', include('snippets.urls')),
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ def validate_schema(swagger):
from flex.core import parse as validate_flex
from swagger_spec_validator.validator20 import validate_spec as validate_ssv

validate_flex(swagger)
validate_ssv(swagger)
validate_flex(copy.deepcopy(swagger))
validate_ssv(copy.deepcopy(swagger))

return validate_schema

Expand Down
21 changes: 21 additions & 0 deletions tests/test_schema_views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from collections import OrderedDict

import pytest
from ruamel import yaml
Expand Down Expand Up @@ -46,6 +47,26 @@ def test_redoc(client, validate_schema):
_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')
def test_non_public(client):
response = client.get('/private/swagger.yaml')
Expand Down

0 comments on commit 174f115

Please sign in to comment.