Rewrite schema generation (#1)

* Completeley rewritten schema generation
* Added support for python 2.7 and 3.4
* Restructured testing and build configuration
* Added nested request schemas

This rewrite completely replaces the public interface of the django rest schema generation library, so 
further changes will be needed to re-enable and further extend the customization points one might want.
openapi3
Cristi Vîjdea 2017-12-05 19:46:02 +01:00 committed by GitHub
parent 5658910711
commit dce00156d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1512 additions and 346 deletions

32
.coveragerc 100644
View File

@ -0,0 +1,32 @@
[run]
source = drf_swagger
branch = True
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain about missing debug-only code:
def __repr__
if self/.debug
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
warnings.warn
# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:
ignore_errors = True
precision = 0
[paths]
source =
src/drf_swagger/
.tox/*/Lib/site-packages/drf_swagger/
.tox/*/lib/*/site-packages/drf_swagger/
/home/travis/virtualenv/*/lib/*/site-packages/drf_swagger/

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/testproj" />
<option name="settingsModule" value="testproj/settings.py" />
<option name="manageScript" value="manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.6 (drf-swagger)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/drf_swagger/templates" />
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="projectConfiguration" value="py.test" />
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>

View File

@ -0,0 +1,60 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CssUnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JSCheckFunctionSignatures" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSUnresolvedVariable" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownUnresolvedFileReference" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyAbstractClassInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyBroadExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ourVersions">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="2.7" />
<item index="1" class="java.lang.String" itemvalue="3.4" />
<item index="2" class="java.lang.String" itemvalue="3.5" />
<item index="3" class="java.lang.String" itemvalue="3.6" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyMethodMayBeStaticInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ignoredPackages">
<value>
<list size="0" />
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N806" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyProtectedMemberInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredNames">
<list>
<option value="license" />
<option value="format" />
<option value="type" />
<option value="filter" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyShadowingNamesInspection" 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="ignoreLambdaParameters" value="true" />
<option name="ignoreLoopIterationVariables" value="true" />
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

73
.idea/misc.xml 100644
View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownProjectSettings">
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true">
<PanelProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
</PanelProvider>
</PreviewSettings>
<ParserSettings gitHubSyntaxChange="false">
<PegdownExtensions>
<option name="ABBREVIATIONS" value="false" />
<option name="ANCHORLINKS" value="true" />
<option name="ASIDE" value="false" />
<option name="ATXHEADERSPACE" value="true" />
<option name="AUTOLINKS" value="true" />
<option name="DEFINITIONS" value="false" />
<option name="DEFINITION_BREAK_DOUBLE_BLANK_LINE" value="false" />
<option name="FENCED_CODE_BLOCKS" value="true" />
<option name="FOOTNOTES" value="false" />
<option name="HARDWRAPS" value="false" />
<option name="HTML_DEEP_PARSER" value="false" />
<option name="INSERTED" value="false" />
<option name="QUOTES" value="false" />
<option name="RELAXEDHRULES" value="true" />
<option name="SMARTS" value="false" />
<option name="STRIKETHROUGH" value="true" />
<option name="SUBSCRIPT" value="false" />
<option name="SUPERSCRIPT" value="false" />
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
<option name="SUPPRESS_INLINE_HTML" value="false" />
<option name="TABLES" value="true" />
<option name="TASKLISTITEMS" value="true" />
<option name="TOC" value="false" />
<option name="WIKILINKS" value="true" />
</PegdownExtensions>
<ParserOptions>
<option name="COMMONMARK_LISTS" value="true" />
<option name="DUMMY" value="false" />
<option name="EMOJI_SHORTCUTS" value="true" />
<option name="FLEXMARK_FRONT_MATTER" value="false" />
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="false" />
<option name="GFM_TABLE_RENDERING" value="true" />
<option name="GITBOOK_URL_ENCODING" value="false" />
<option name="GITHUB_EMOJI_URL" value="false" />
<option name="GITHUB_LISTS" value="false" />
<option name="GITHUB_WIKI_LINKS" value="true" />
<option name="JEKYLL_FRONT_MATTER" value="false" />
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
</ParserOptions>
</ParserSettings>
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true">
<GeneratorProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
</GeneratorProvider>
<headerTop />
<headerBottom />
<bodyTop />
<bodyBottom />
</HtmlSettings>
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssTextEnabled="false" isDynamicPageWidth="true">
<StylesheetProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
</StylesheetProvider>
<ScriptProviders />
<cssText />
</CssSettings>
<HtmlExportSettings updateOnSave="false" parentDir="$ProjectFileDir$" targetDir="$ProjectFileDir$" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetExt="" useTargetExt="false" noCssNoScripts="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" />
<LinkMapSettings>
<textMaps />
</LinkMapSettings>
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (drf-swagger)" project-jdk-type="Python SDK" />
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/drf-swagger.iml" filepath="$PROJECT_DIR$/.idea/drf-swagger.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml 100644
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,21 +1,31 @@
language: python
cache: pip
python:
- '2.7'
- '3.4'
- '3.5'
- '3.6'
- '3.7-dev'
env:
- DRF=3.7
matrix:
fast_finish: true
include:
- python: "3.5"
- python: '2.7'
env: TOXENV=flake8
- python: '3.6'
env: DRF=master
allow_failures:
- env: TOXENV=flake8
- env: DRF=master
- python: '2.7'
- python: '3.7'
install:
- pip install -r requirements_dev.txt
- pip install -r requirements/ci.txt
before_script:
- coverage erase
@ -24,4 +34,8 @@ script:
- tox
after_success:
- coveralls
- codecov
branches:
only:
- master

View File

@ -1,5 +1,5 @@
include README.md
include LICENSE
include requirements*
recursive-include requirements *
recursive-include src/drf_swagger/static *
recursive-include src/drf_swagger/templates *

View File

@ -1,5 +1,6 @@
djangorestframework>=3.7.3
django>=1.11.7
coreapi>=2.3.3
coreschema>=0.0.4
openapi_codec>=1.3.2
ruamel.yaml>=0.15.34
inflection>=0.3.1
future>=0.16.0

View File

@ -0,0 +1,4 @@
# requirements for CI test suite
-r dev.txt
tox-travis>=0.10
codecov>=2.0.9

View File

@ -0,0 +1,3 @@
# requirements for local development
tox>=2.9.1
tox-battery>=0.5

View File

@ -0,0 +1,11 @@
# pytest runner + plugins
pytest-django>=3.1.2
pytest-pythonpath>=0.7.1
pytest-cov>=2.5.1
# test project requirements
Pillow>=4.3.0
pygments>=2.2.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.4"

View File

@ -0,0 +1,3 @@
# requirements for the validation feature
flex>=6.11.1
swagger-spec-validator>=2.1.0

View File

@ -1,5 +0,0 @@
# Packages required for development and CI
tox>=2.9.1
tox-battery>=0.5
tox-travis>=0.10
python-coveralls>=2.9.1

View File

@ -1,6 +0,0 @@
# Packages required for running the tests
pygments>=2.2.0
django-cors-headers>=2.1.0
pytest-django>=3.1.2
pytest-pythonpath>=0.7.1
pytest-cov>=2.5.1

View File

@ -1,3 +0,0 @@
# Packages required for the validation feature
flex>=6.11.1
swagger-spec-validator>=2.1.0

View File

@ -1,17 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from setuptools import setup, find_packages
def read_req(req_file):
with open(req_file) as req:
with open(os.path.join('requirements', req_file)) as req:
return [line for line in req.readlines() if line and not line.isspace()]
requirements = read_req('requirements.txt')
requirements_validation = read_req('requirements_validation.txt')
requirements_dev = read_req('requirements_dev.txt')
requirements_test = read_req('requirements_test.txt')
requirements = ['djangorestframework>=3.7.3'] + read_req('base.txt')
requirements_validation = read_req('validation.txt')
requirements_test = read_req('test.txt')
setup(
name='drf-swagger',
@ -24,7 +25,6 @@ setup(
extras_require={
'validation': requirements_validation,
'test': requirements_test,
'dev': requirements_dev,
},
license='BSD License',
description='Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code.',

View File

@ -1,52 +1,42 @@
import json
from collections import OrderedDict
from coreapi.codecs import BaseCodec
from coreapi.compat import force_bytes, urlparse
from drf_swagger.app_settings import swagger_settings
from openapi_codec import encode
from coreapi.compat import force_bytes
from future.utils import raise_from
from ruamel import yaml
from drf_swagger.app_settings import swagger_settings
from drf_swagger.errors import SwaggerValidationError
from . import openapi
class SwaggerValidationError(Exception):
def __init__(self, msg, validator_name, spec, *args) -> None:
super(SwaggerValidationError, self).__init__(msg, *args)
self.validator_name = validator_name
self.spec = spec
def __str__(self):
return str(self.validator_name) + ": " + super(SwaggerValidationError, self).__str__()
def _validate_flex(spec):
def _validate_flex(spec, codec):
from flex.core import parse as validate_flex
from flex.exceptions import ValidationError
try:
validate_flex(spec)
except ValidationError as ex:
raise SwaggerValidationError(str(ex), 'flex', spec) from ex
raise_from(SwaggerValidationError(str(ex), 'flex', spec, codec), ex)
def _validate_swagger_spec_validator(spec):
def _validate_swagger_spec_validator(spec, codec):
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
from swagger_spec_validator.common import SwaggerValidationError as SSVErr
try:
validate_ssv(spec)
except SSVErr as ex:
raise SwaggerValidationError(str(ex), 'swagger_spec_validator', spec) from ex
raise_from(SwaggerValidationError(str(ex), 'swagger_spec_validator', spec, codec), ex)
VALIDATORS = {
'flex': _validate_flex,
'swagger_spec_validator': _validate_swagger_spec_validator,
'ssv': _validate_swagger_spec_validator,
}
class _OpenAPICodec(BaseCodec):
class _OpenAPICodec(object):
format = 'openapi'
media_type = None
def __init__(self, validators):
self._validators = validators
@ -61,43 +51,30 @@ class _OpenAPICodec(BaseCodec):
spec = self.generate_swagger_object(document)
for validator in self.validators:
VALIDATORS[validator](spec)
return force_bytes(self._dump_spec(spec))
VALIDATORS[validator](spec, self)
return force_bytes(self._dump_dict(spec))
def _dump_spec(self, spec):
return NotImplementedError("override this method")
def encode_error(self, err):
return force_bytes(self._dump_dict(err))
def _dump_dict(self, spec):
raise NotImplementedError("override this method")
def generate_swagger_object(self, swagger):
"""
Generates root of the Swagger spec.
Generates the root Swagger object.
:param openapi.Swagger swagger:
:return OrderedDict: swagger spec as dict
"""
parsed_url = urlparse.urlparse(swagger.url)
spec = OrderedDict()
spec['swagger'] = '2.0'
spec['info'] = swagger.info.to_swagger(swagger.version)
if parsed_url.netloc:
spec['host'] = parsed_url.netloc
if parsed_url.scheme:
spec['schemes'] = [parsed_url.scheme]
spec['basePath'] = '/'
spec['paths'] = encode._get_paths_object(swagger)
spec['securityDefinitions'] = swagger_settings.SECURITY_DEFINITIONS
return spec
swagger.security_definitions = swagger_settings.SECURITY_DEFINITIONS
return swagger
class OpenAPICodecJson(_OpenAPICodec):
media_type = 'application/openapi+json'
media_type = 'application/json'
def _dump_spec(self, spec):
def _dump_dict(self, spec):
return json.dumps(spec)
@ -109,7 +86,7 @@ class SaneYamlDumper(yaml.SafeDumper):
return super(SaneYamlDumper, self).increase_indent(flow=flow, indentless=False, **kwargs)
@staticmethod
def represent_odict(dump, mapping, flow_style=None):
def represent_odict(dump, mapping, flow_style=None): # pragma: no cover
"""https://gist.github.com/miracle2k/3184458
Make PyYAML output an OrderedDict.
@ -142,11 +119,11 @@ class SaneYamlDumper(yaml.SafeDumper):
return node
SaneYamlDumper.add_representer(OrderedDict, SaneYamlDumper.represent_odict)
SaneYamlDumper.add_multi_representer(OrderedDict, SaneYamlDumper.represent_odict)
class OpenAPICodecYaml(_OpenAPICodec):
media_type = 'application/openapi+yaml'
media_type = 'application/yaml'
def _dump_spec(self, spec):
def _dump_dict(self, spec):
return yaml.dump(spec, Dumper=SaneYamlDumper, default_flow_style=False, encoding='utf-8')

View File

@ -0,0 +1,14 @@
class SwaggerError(Exception):
pass
class SwaggerValidationError(SwaggerError):
def __init__(self, msg, validator_name, spec, source_codec, *args):
super(SwaggerValidationError, self).__init__(msg, *args)
self.validator_name = validator_name
self.spec = spec
self.source_codec = source_codec
class SwaggerGenerationError(SwaggerError):
pass

View File

@ -1,15 +1,118 @@
from rest_framework.schemas import SchemaGenerator as _SchemaGenerator
from collections import defaultdict
import django.db.models
import uritemplate
from coreapi.compat import force_text
from rest_framework.schemas.generators import SchemaGenerator
from rest_framework.schemas.inspectors import get_pk_description
from drf_swagger.inspectors import SwaggerAutoSchema
from . import openapi
class OpenAPISchemaGenerator(_SchemaGenerator):
class OpenAPISchemaGenerator(object):
"""
This class iterates over all registered API endpoints and returns an appropriate OpenAPI 2.0 compliant schema.
Method implementations shamelessly stolen and adapted from rest_framework SchemaGenerator.
"""
def __init__(self, info, version, url=None, patterns=None, urlconf=None):
super(OpenAPISchemaGenerator, self).__init__(info.title, url, info.description, patterns, urlconf)
self._gen = SchemaGenerator(info.title, url, info.get('description', ''), patterns, urlconf)
self.info = info
self.version = version
self.endpoints = None
self.url = url
def get_schema(self, request=None, public=False):
document = super(OpenAPISchemaGenerator, self).get_schema(request, public)
swagger = openapi.Swagger.from_coreapi(document, self.info, self.version)
return swagger
"""Generate an openapi.Swagger representing the API schema."""
if self.endpoints is None:
inspector = self._gen.endpoint_inspector_cls(self._gen.patterns, self._gen.urlconf)
self.endpoints = inspector.get_api_endpoints()
self.get_endpoints(None if public else request)
paths = self.get_paths()
url = self._gen.url
if not url and request is not None:
url = request.build_absolute_uri()
# distribute_links(links)
return openapi.Swagger(
info=self.info, paths=paths,
_url=url, _version=self.version,
)
def get_endpoints(self, request):
"""Generate {path: (view_class, [(method, view)]) given (path, method, callback)."""
view_paths = defaultdict(list)
view_cls = {}
for path, method, callback in self.endpoints:
view = self._gen.create_view(callback, method, request)
path = self._gen.coerce_path(path, method, view)
view_paths[path].append((method, view))
view_cls[path] = callback.cls
self.endpoints = {path: (view_cls[path], methods) for path, methods in view_paths.items()}
def get_paths(self):
if not self.endpoints:
return []
prefix = self._gen.determine_path_prefix(self.endpoints.keys())
paths = {}
for path, (view_cls, methods) in self.endpoints.items():
path_parameters = self.get_path_parameters(path, view_cls)
operations = {}
for method, view in methods:
if not self._gen.has_view_permissions(path, method, view):
continue
schema = SwaggerAutoSchema(view)
operation_keys = self._gen.get_keys(path[len(prefix):], method, view)
operations[method.lower()] = schema.get_operation(operation_keys, path, method)
paths[path] = openapi.PathItem(parameters=path_parameters, **operations)
return openapi.Paths(paths=paths)
def get_path_parameters(self, path, view_cls):
"""Return a list of Parameter instances corresponding to any templated path variables.
:param str path: templated request path
:param type view_cls: the view class associated with the path
:return list[openapi.Parameter]: path parameters
"""
parameters = []
model = getattr(getattr(view_cls, 'queryset', None), 'model', None)
for variable in uritemplate.variables(path):
pattern = None
type = openapi.TYPE_STRING
description = None
if model is not None:
# Attempt to infer a field description if possible.
try:
model_field = model._meta.get_field(variable)
except Exception:
model_field = None
if model_field is not None and model_field.help_text:
description = force_text(model_field.help_text)
elif model_field is not None and model_field.primary_key:
description = get_pk_description(model, model_field)
if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable:
pattern = view_cls.lookup_value_regex
elif isinstance(model_field, django.db.models.AutoField):
type = openapi.TYPE_INTEGER
field = openapi.Parameter(
name=variable,
required=True,
in_=openapi.IN_PATH,
type=type,
pattern=pattern,
description=description,
)
parameters.append(field)
return parameters

View File

@ -1,5 +1,362 @@
import functools
from collections import OrderedDict
import coreschema
from django.core.validators import RegexValidator
from django.utils.encoding import force_text
from rest_framework import serializers
from rest_framework.schemas import AutoSchema
from rest_framework.schemas.utils import is_list_view
from drf_swagger.errors import SwaggerGenerationError
from . import openapi
class SwaggerAutoSchema(AutoSchema):
pass
def find_regex(regex_field):
regex_validator = None
for validator in regex_field.validators:
if isinstance(validator, RegexValidator):
if regex_validator is not None:
# bail if multiple validators are found - no obvious way to choose
return None
regex_validator = validator
# regex_validator.regex should be a compiled re object...
return getattr(getattr(regex_validator, 'regex', None), 'pattern', None)
class SwaggerAutoSchema(object):
def __init__(self, view):
super(SwaggerAutoSchema, self).__init__()
self._sch = AutoSchema()
self.view = view
self._sch.view = view
def get_operation(self, operation_keys, path, method):
"""Get an Operation for the given API endpoint (path, method).
This includes query, body parameters and response schemas.
:param tuple[str] operation_keys: an array of keys describing the hierarchical layout of this view in the API;
e.g. ('snippets', 'list'), ('snippets', 'retrieve'), etc.
:param str path: the view's path
:param str method: HTTP request method
:return openapi.Operation: the resulting Operation object
"""
body = self.get_request_body_parameters(path, method)
query = self.get_query_parameters(path, method)
parameters = body + query
parameters = [param for param in parameters if param is not None]
description = self.get_description(path, method)
responses = self.get_responses(path, method)
return openapi.Operation(
operation_id='_'.join(operation_keys),
description=description,
responses=responses,
parameters=parameters,
tags=[operation_keys[0]]
)
def get_request_body_parameters(self, path, method):
"""Return the request body parameters for this view.
This is either:
- a list with a single object Parameter with a Schema derived from the request serializer
- a list of primitive Parameters parsed as form data
:param str path: the view's path
:param str method: HTTP request method
:return list[Parameter]: a (potentially empty) list of openapi.Parameter in: either `body` or `formData`
"""
# only PUT, PATCH or POST can have a request body
if method not in ('PUT', 'PATCH', 'POST'):
return []
serializer = self.get_request_serializer(path, method)
if serializer is None:
return []
encoding = self._sch.get_encoding(path, method)
if 'form' in encoding:
return [
self.field_to_swagger(value, openapi.Parameter, name=key, in_=openapi.IN_FORM)
for key, value
in serializer.fields.items()
]
else:
schema = self.get_request_body_schema(path, method, serializer)
return [openapi.Parameter(name='data', in_=openapi.IN_BODY, schema=schema)]
def get_request_serializer(self, path, method):
"""Return the request serializer (used for parsing the request payload) for this endpoint.
:param str path: the view's path
:param str method: HTTP request method
:return serializers.Serializer: the request serializer
"""
# TODO: only GenericAPIViews have defined serializers;
# APIViews and plain ViewSets will need some kind of manual treatment
if not hasattr(self.view, 'get_serializer'):
return None
return self.view.get_serializer()
def get_request_body_schema(self, path, method, serializer):
"""Return the Schema for a given request's body data. Only applies to PUT, PATCH and POST requests.
:param str path: the view's path
:param str method: HTTP request method
:param serializer: the view's request serialzier
:return openapi.Schema: the request body schema
"""
return self.field_to_swagger(serializer, openapi.Schema)
def get_responses(self, path, method):
"""Get the possible responses for this view as a swagger Responses object.
:param str path: the view's path
:param str method: HTTP request method
:return Responses: the documented responses
"""
response_serializers = self.get_response_serializers(path, method)
return openapi.Responses(
responses=self.get_response_schemas(path, method, response_serializers)
)
def get_response_serializers(self, path, method):
"""Return the response codes that this view is expected to return, and the serializer for each response body.
The return value should be a dict where the keys are possible status codes, and values are either strings,
`Serializer`s or `openapi.Response` objects.
:param str path: the view's path
:param str method: HTTP request method
:return dict: the response serializers
"""
if method.lower() == 'post':
return {'201': ''}
if method.lower() == 'delete':
return {'204': ''}
return {'200': ''}
def get_response_schemas(self, path, method, response_serializers):
"""Return the `openapi.Response` objects calculated for this view.
:param str path: the view's path
:param str method: HTTP request method
:param dict response_serializers: result of get_response_serializers
:return dict[str, openapi.Response]: a dictionary of status code to Response object
"""
responses = {}
for status, serializer in response_serializers.items():
if isinstance(serializer, str):
response = openapi.Response(
description=serializer
)
elif isinstance(serializer, openapi.Response):
response = serializer
else:
response = openapi.Response(
description='',
schema=self.field_to_swagger(serializer, openapi.Schema)
)
responses[str(status)] = response
return responses
def get_query_parameters(self, path, method):
"""Return the query parameters accepted by this view.
:param str path: the view's path
:param str method: HTTP request method
:return list[openapi.Parameter]: the query parameters
"""
return self.get_filter_parameters(path, method) + self.get_pagination_parameters(path, method)
def get_filter_parameters(self, path, method):
"""Return the parameters added to the view by its filter backends.
:param str path: the view's path
:param str method: HTTP request method
:return list[openapi.Parameter]: the filter query parameters
"""
if not self._sch._allows_filters(path, method):
return []
fields = []
for filter_backend in self.view.filter_backends:
filter = filter_backend()
if hasattr(filter, 'get_schema_fields'):
fields += filter.get_schema_fields(self.view)
return [self.coreapi_field_to_parameter(field) for field in fields]
def get_pagination_parameters(self, path, method):
"""Return the parameters added to the view by its paginator.
:param str path: the view's path
:param str method: HTTP request method
:return list[openapi.Parameter]: the pagination query parameters
"""
if not is_list_view(path, method, self.view):
return []
paginator = getattr(self.view, 'paginator', None)
if paginator is None:
return []
return [
self.coreapi_field_to_parameter(field)
for field in paginator.get_schema_fields(self.view)
]
def coreapi_field_to_parameter(self, field):
"""Convert an instance of `coreapi.Field` to a swagger Parameter object.
:param coreapi.Field field: the coreapi field
:return openapi.Parameter: the equivalent openapi primitive Parameter
"""
location_to_in = {
'query': openapi.IN_QUERY,
'path': openapi.IN_PATH,
'form': openapi.IN_FORM,
'body': openapi.IN_FORM,
}
coreapi_types = {
coreschema.Integer: openapi.TYPE_INTEGER,
coreschema.Number: openapi.TYPE_NUMBER,
coreschema.String: openapi.TYPE_STRING,
coreschema.Boolean: openapi.TYPE_BOOLEAN,
}
return openapi.Parameter(
name=field.name,
in_=location_to_in[field.location],
type=coreapi_types.get(field.schema.__class__, openapi.TYPE_STRING),
required=field.required,
description=field.schema.description,
)
def get_description(self, path, method):
"""Return an operation description determined as appropriate from the view's method and class docstrings.
:param str path: the view's path
:param str method: HTTP request method
:return str: the operation description
"""
return self._sch.get_description(path, method)
def field_to_swagger(self, field, swagger_object_type, **kwargs):
"""Convert a drf Serializer or Field instance into a Swagger object.
:param rest_framework.serializers.Field field: the source field
:param type swagger_object_type: should be one of Schema, Parameter, Items
:param kwargs: extra attributes for constructing the object;
if swagger_object_type is Parameter, `name` and `in_` should be provided
:return Swagger,Parameter,Items: the swagger object
"""
assert swagger_object_type in (openapi.Schema, openapi.Parameter, openapi.Items)
title = force_text(field.label) if field.label else None
title = title if swagger_object_type == openapi.Schema else None # only Schema has title
title = None
description = force_text(field.help_text) if field.help_text else None
description = description if swagger_object_type != openapi.Items else None # Items has no description either
SwaggerType = functools.partial(swagger_object_type, title=title, description=description, **kwargs)
# arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
ChildSwaggerType = openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items
# ------ NESTED
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
child_schema = self.field_to_swagger(field.child, ChildSwaggerType)
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=child_schema,
)
elif isinstance(field, serializers.Serializer):
if swagger_object_type != openapi.Schema:
raise SwaggerGenerationError("cannot instantiate nested serializer as "
+ swagger_object_type.__name__)
return SwaggerType(
type=openapi.TYPE_OBJECT,
properties=OrderedDict(
(key, self.field_to_swagger(value, ChildSwaggerType))
for key, value
in field.fields.items()
)
)
elif isinstance(field, serializers.ManyRelatedField):
child_schema = self.field_to_swagger(field.child_relation, ChildSwaggerType)
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=child_schema,
unique_items=True, # is this OK?
)
elif isinstance(field, serializers.RelatedField):
# TODO: infer type for PrimaryKeyRelatedField?
return SwaggerType(type=openapi.TYPE_STRING)
# ------ CHOICES
elif isinstance(field, serializers.MultipleChoiceField):
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=ChildSwaggerType(
type=openapi.TYPE_STRING,
enum=list(field.choices.keys())
)
)
elif isinstance(field, serializers.ChoiceField):
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
# ------ BOOL
elif isinstance(field, serializers.BooleanField):
return SwaggerType(type=openapi.TYPE_BOOLEAN)
# ------ NUMERIC
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
# TODO: min_value max_value
return SwaggerType(type=openapi.TYPE_NUMBER)
elif isinstance(field, serializers.IntegerField):
# TODO: min_value max_value
return SwaggerType(type=openapi.TYPE_INTEGER)
# ------ STRING
elif isinstance(field, serializers.EmailField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL)
elif isinstance(field, serializers.RegexField):
return SwaggerType(type=openapi.TYPE_STRING, pattern=find_regex(field))
elif isinstance(field, serializers.SlugField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_SLUG)
elif isinstance(field, serializers.URLField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI)
elif isinstance(field, serializers.IPAddressField):
format = {'ipv4': openapi.FORMAT_IPV4, 'ipv6': openapi.FORMAT_IPV6}.get(field.protocol, None)
return SwaggerType(type=openapi.TYPE_STRING, format=format)
elif isinstance(field, serializers.CharField):
# TODO: min_length max_length (for all CharField subclasses above too)
return SwaggerType(type=openapi.TYPE_STRING)
elif isinstance(field, serializers.UUIDField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_UUID)
# ------ DATE & TIME
elif isinstance(field, serializers.DateField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATE)
elif isinstance(field, serializers.DateTimeField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATETIME)
# ------ OTHERS
elif isinstance(field, serializers.FileField):
# swagger 2.0 does not support specifics about file fields, so ImageFile gets no special treatment
# OpenAPI 3.0 does support it, so a future implementation could handle this better
# TODO: appropriate produces/consumes somehow/somewhere?
if swagger_object_type != openapi.Parameter:
raise SwaggerGenerationError("parameter of type file is supported only in formData Parameter")
return SwaggerType(type=openapi.TYPE_FILE)
elif isinstance(field, serializers.JSONField):
return SwaggerType(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_BINARY if field.binary else None
)
elif isinstance(field, serializers.DictField) and swagger_object_type == openapi.Schema:
child_schema = self.field_to_swagger(field.child, ChildSwaggerType)
return SwaggerType(
type=openapi.TYPE_OBJECT,
additional_properties=child_schema
)
# TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField?
# TODO: return info about required/allowed empty
# everything else gets string by default
return SwaggerType(type=openapi.TYPE_STRING)

View File

@ -0,0 +1,18 @@
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
from drf_swagger.errors import SwaggerValidationError
from .codecs import _OpenAPICodec
class SwaggerExceptionMiddleware(MiddlewareMixin):
def process_exception(self, request, exception):
if isinstance(exception, SwaggerValidationError):
err = {'errors': {exception.validator_name: str(exception)}}
codec = exception.source_codec
if isinstance(codec, _OpenAPICodec):
err = codec.encode_error(err)
content_type = codec.media_type
return HttpResponse(err, status=500, content_type=content_type)
return None

View File

@ -1,10 +1,105 @@
import warnings
from collections import OrderedDict
import coreapi
from coreapi.compat import urlparse
from future.utils import raise_from
from inflection import camelize
TYPE_OBJECT = "object"
TYPE_STRING = "string"
TYPE_NUMBER = "number"
TYPE_INTEGER = "integer"
TYPE_BOOLEAN = "boolean"
TYPE_ARRAY = "array"
TYPE_FILE = "file"
# officially supported by Swagger 2.0 spec
FORMAT_DATE = "date"
FORMAT_DATETIME = "date-time"
FORMAT_PASSWORD = "password"
FORMAT_BINARY = "binary"
FORMAT_BASE64 = "bytes"
FORMAT_FLOAT = "float"
FORMAT_DOUBLE = "double"
FORMAT_INT32 = "int32"
FORMAT_INT64 = "int64"
# defined in JSON-schema
FORMAT_EMAIL = "email"
FORMAT_IPV4 = "ipv4"
FORMAT_IPV6 = "ipv6"
FORMAT_URI = "uri"
# pulled out of my ass
FORMAT_UUID = "uuid"
FORMAT_SLUG = "slug"
IN_BODY = 'body'
IN_PATH = 'path'
IN_QUERY = 'query'
IN_FORM = 'formData'
IN_HEADER = 'header'
class Contact(object):
def make_swagger_name(attribute_name):
"""
Convert a python variable name into a Swagger spec attribute name.
In particular,
* if name starts with x_, return "x-{camelCase}"
* if name is 'ref', return "$ref"
* else return the name converted to camelCase, with trailing underscores stripped
:param str attribute_name: python attribute name
:return: swagger name
"""
if attribute_name == 'ref':
return "$ref"
if attribute_name.startswith("x_"):
return "x-" + camelize(attribute_name[2:], uppercase_first_letter=False)
return camelize(attribute_name.rstrip('_'), uppercase_first_letter=False)
class SwaggerDict(OrderedDict):
def __init__(self, **attrs):
super(SwaggerDict, self).__init__()
self._extras__ = attrs
if self.__class__ == SwaggerDict:
self._insert_extras__()
def __setattr__(self, key, value):
if key.startswith('_'):
super(SwaggerDict, self).__setattr__(key, value)
return
if value is not None:
self[make_swagger_name(key)] = value
def __getattr__(self, item):
if item.startswith('_'):
raise AttributeError
try:
return self[make_swagger_name(item)]
except KeyError as e:
raise_from(AttributeError("no attribute " + item), e)
def __delattr__(self, item):
if item.startswith('_'):
super(SwaggerDict, self).__delattr__(item)
return
del self[make_swagger_name(item)]
def _insert_extras__(self):
"""
From an ordering perspective, it is desired that extra attributes such as vendor extensions stay at the
bottom of the object. However, python2.7's OrderdDict craps out if you try to insert into it before calling
init. This means that subclasses must call super().__init__ as the first statement of their own __init__,
which would result in the extra attributes being added first. For this reason, we defer the insertion of the
attributes and require that subclasses call ._insert_extras__ at the end of their __init__ method.
"""
for attr, val in self._extras__.items():
setattr(self, attr, val)
class Contact(SwaggerDict):
"""Swagger Contact object
At least one of the following fields is required:
@ -12,47 +107,34 @@ class Contact(object):
:param str url: contact url
:param str email: contact e-mail
"""
def __init__(self, name=None, url=None, email=None):
def __init__(self, name=None, url=None, email=None, **extra):
super(Contact, self).__init__(**extra)
if name is None and url is None and email is None:
raise ValueError("one of name, url or email is requires for Swagger Contact object")
self.name = name
self.url = url
self.email = email
if name is None and url is None and email is None:
raise ValueError("one of name, url or email is requires for Swagger Contact object")
def to_swagger(self):
contact = OrderedDict()
if self.name is not None:
contact['name'] = self.name
if self.url is not None:
contact['url'] = self.url
if self.email is not None:
contact['email'] = self.email
return contact
self._insert_extras__()
class License(object):
class License(SwaggerDict):
"""Swagger License object
:param str name: Requird. License name
:param str url: link to detailed license information
"""
def __init__(self, name, url=None):
self.name = name
self.url = url
def __init__(self, name, url=None, **extra):
super(License, self).__init__(**extra)
if name is None:
raise ValueError("name is required for Swagger License object")
def to_swagger(self):
license = OrderedDict()
license['name'] = self.name
if self.url is not None:
license['url'] = self.url
return license
self.name = name
self.url = url
self._insert_extras__()
class Info(object):
class Info(SwaggerDict):
"""Swagger Info object
:param str title: Required. API title.
@ -62,7 +144,10 @@ class Info(object):
:param Contact contact: contact object
:param License license: license object
"""
def __init__(self, title, default_version, description=None, terms_of_service=None, contact=None, license=None):
def __init__(self, title, default_version, description=None, terms_of_service=None, contact=None, license=None,
**extra):
super(Info, self).__init__(**extra)
if title is None or default_version is None:
raise ValueError("title and version are required for Swagger info object")
if contact is not None and not isinstance(contact, Contact):
@ -70,67 +155,139 @@ class Info(object):
if license is not None and not isinstance(license, License):
raise ValueError("license must be a License object")
self.title = title
self.default_version = default_version
self._default_version = default_version
self.description = description
self.terms_of_service = terms_of_service
self.contact = contact
self.license = license
def to_swagger(self, version):
info = OrderedDict()
info['title'] = self.title
if self.description is not None:
info['description'] = self.description
if self.terms_of_service is not None:
info['termsOfService'] = self.terms_of_service
if self.contact is not None:
info['contact'] = self.contact.to_swagger()
if self.license is not None:
info['license'] = self.license.to_swagger()
info['version'] = version or self.default_version
return info
self._insert_extras__()
class Swagger(coreapi.Document):
@classmethod
def from_coreapi(cls, document, info, version):
"""
Create an openapi.Swagger from the fields of a coreapi.Document.
class Swagger(SwaggerDict):
def __init__(self, info=None, _url=None, _version=None, paths=None, **extra):
super(Swagger, self).__init__(**extra)
self.swagger = '2.0'
self.info = info
self.info.version = _version or info._default_version
self.paths = paths
:param coreapi.Document document: source coreapi.Document
:param openapi.Info info: Swagger info object
:param string version: API version string
:return: an openapi.Swagger
"""
if document.title and document.title != info.title:
warnings.warn("document title is overriden by Swagger Info")
if document.description and document.description != info.description:
warnings.warn("document description is overriden by Swagger Info")
return Swagger(
info=info,
version=version,
url=document.url,
media_type=document.media_type,
content=document.data
)
if _url:
url = urlparse.urlparse(_url)
if url.netloc:
self.host = url.netloc
if url.scheme:
self.schemes = [url.scheme]
def __init__(self, info=None, version=None, url=None, media_type=None, content=None):
super(Swagger, self).__init__(url, info.title, info.description, media_type, content)
self._info = info
self._version = version
@property
def info(self):
return self._info
@property
def version(self):
return self._version
self.base_path = '/'
self._insert_extras__()
class Field(coreapi.Field):
pass
class Paths(SwaggerDict):
def __init__(self, paths, **extra):
super(Paths, self).__init__(**extra)
for path, path_obj in paths.items():
assert path.startswith("/")
if path_obj is not None:
self[path] = path_obj
self._insert_extras__()
class Link(coreapi.Link):
pass
class PathItem(SwaggerDict):
def __init__(self, get=None, put=None, post=None, delete=None, options=None,
head=None, patch=None, parameters=None, **extra):
super(PathItem, self).__init__(**extra)
self.get = get
self.put = put
self.post = post
self.delete = delete
self.options = options
self.head = head
self.patch = patch
self.parameters = parameters
self._insert_extras__()
class Operation(SwaggerDict):
def __init__(self, operation_id, responses, parameters=None, consumes=None,
produces=None, description=None, tags=None, **extra):
super(Operation, self).__init__(**extra)
self.operation_id = operation_id
self.responses = responses
self.parameters = [param for param in parameters if param is not None]
self.consumes = consumes
self.produces = produces
self.description = description
self.tags = tags
self._insert_extras__()
class Items(SwaggerDict):
def __init__(self, type=None, format=None, enum=None, pattern=None, items=None, **extra):
super(Items, self).__init__(**extra)
self.type = type
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self._insert_extras__()
class Parameter(SwaggerDict):
def __init__(self, name, in_, description=None, required=None, schema=None,
type=None, format=None, enum=None, pattern=None, items=None, **extra):
super(Parameter, self).__init__(**extra)
if (not schema and not type) or (schema and type):
raise ValueError("either schema or type are required for Parameter object!")
self.name = name
self.in_ = in_
self.description = description
self.required = required
self.schema = schema
self.type = type
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self._insert_extras__()
class Schema(SwaggerDict):
def __init__(self, description=None, required=None, type=None, properties=None, additional_properties=None,
format=None, enum=None, pattern=None, items=None, **extra):
super(Schema, self).__init__(**extra)
self.description = description
self.required = required
self.type = type
self.properties = properties
self.additional_properties = additional_properties
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self._insert_extras__()
class Ref(SwaggerDict):
def __init__(self, ref):
super(Ref, self).__init__()
self.ref = ref
self._insert_extras__()
class Responses(SwaggerDict):
def __init__(self, responses, default=None, **extra):
super(Responses, self).__init__(**extra)
for status, response in responses.items():
if response is not None:
self[str(status)] = response
self.default = default
self._insert_extras__()
class Response(SwaggerDict):
def __init__(self, description, schema=None, examples=None, **extra):
super(Response, self).__init__(**extra)
self.description = description
self.schema = schema
self.examples = examples
self._insert_extras__()

View File

@ -45,17 +45,17 @@ class _UIRenderer(BaseRenderer):
charset = 'utf-8'
template = ''
def render(self, data, accepted_media_type=None, renderer_context=None):
self.set_context(renderer_context, data)
def render(self, swagger, accepted_media_type=None, renderer_context=None):
self.set_context(renderer_context, swagger)
return render(
renderer_context['request'],
self.template,
renderer_context
)
def set_context(self, renderer_context, data):
renderer_context['title'] = data.title
renderer_context['version'] = data.version
def set_context(self, renderer_context, swagger):
renderer_context['title'] = swagger.info.title
renderer_context['version'] = swagger.info.version
renderer_context['swagger_settings'] = json.dumps(self.get_swagger_ui_settings())
renderer_context['redoc_settings'] = json.dumps(self.get_redoc_settings())
renderer_context['USE_SESSION_AUTH'] = swagger_settings.USE_SESSION_AUTH

View File

@ -35,7 +35,7 @@
margin-right: 8px;
}
#django-session-auth.hidden {
.hidden {
display: none;
}
@ -126,14 +126,31 @@
</svg>
<div id="swagger-ui"></div>
<div id="spec-error" class="hidden alert alert-danger"></div>
<script src="{% static 'drf-swagger/swagger-ui-dist/swagger-ui-bundle.js' %}"></script>
<script src="{% static 'drf-swagger/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script>
<script src="{% static 'drf-swagger/insQ.min.js' %}"></script>
<script>
window.onload = function () {
"use strict";
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
var specURL = currentPath + '?format=openapi';
function patchSwaggerUi() {
var authWrapper = document.querySelector('.auth-wrapper');
var authorizeButton = document.querySelector('.auth-wrapper .authorize');
var djangoSessionAuth = document.querySelector('#django-session-auth');
if (document.querySelector('.auth-wrapper #django-session-auth')) {
console.log("session auth already patched");
return;
}
authWrapper.insertBefore(djangoSessionAuth, authorizeButton);
djangoSessionAuth.classList.remove("hidden");
var divider = document.createElement("div");
divider.classList.add("divider");
authWrapper.insertBefore(divider, authorizeButton);
}
function initSwaggerUi() {
var swaggerConfig = {
url: specURL,
dom_id: '#swagger-ui',
@ -158,22 +175,18 @@
}
}
window.ui = SwaggerUIBundle(swaggerConfig);
insertionQ('.auth-wrapper .authorize').every(function () {
var authWrapper = document.querySelector('.auth-wrapper');
var authorizeButton = document.querySelector('.auth-wrapper .authorize');
var djangoSessionAuth = document.querySelector('#django-session-auth');
authWrapper.insertBefore(djangoSessionAuth, authorizeButton);
djangoSessionAuth.classList.remove("hidden");
var divider = document.createElement("div");
divider.classList.add("divider");
authWrapper.insertBefore(divider, authorizeButton);
});
}
window.onload = function () {
insertionQ('.auth-wrapper .authorize').every(patchSwaggerUi);
initSwaggerUi();
};
</script>
<script src="{% static 'drf-swagger/swagger-ui-dist/swagger-ui-bundle.js' %}"></script>
<script src="{% static 'drf-swagger/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script>
<script src="{% static 'drf-swagger/insQ.min.js' %}"></script>
<div id="django-session-auth" class="hidden">
{% if USE_SESSION_AUTH %}
{% csrf_token %}

View File

@ -46,7 +46,8 @@ def deferred_never_cache(view_func):
return _wrapped_view_func
def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False, validators=None,
def get_schema_view(info, url=None, patterns=None, urlconf=None, public=False, validators=None,
generator_class=OpenAPISchemaGenerator,
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
"""
@ -58,13 +59,15 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False
:param str urlconf: passed to SchemaGenerator
:param bool public: if False, includes only endpoints the current user has access to
:param list validators: a list of validator names to apply on the generated schema; allowed values are `flex`, `ssv`
:param type generator_class: schema generator class to use; should be a subclass of OpenAPISchemaGenerator
:param tuple authentication_classes: authentication classes for the schema view itself
:param tuple permission_classes: permission classes for the schema view itself
:return: SchemaView class
"""
_public = public
_generator_class = generator_class
_auth_classes = authentication_classes
_perm_classes = permission_classes
_public = public
validators = validators or []
_spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS)
@ -72,7 +75,7 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False
_ignore_model_permissions = True
schema = None # exclude from schema
public = _public
generator_class = OpenAPISchemaGenerator
generator_class = _generator_class
authentication_classes = _auth_classes
permission_classes = _perm_classes
renderer_classes = _spec_renderers
@ -129,7 +132,7 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False
:return: a view instance
"""
assert renderer in UI_RENDERERS, "supported default renderers are " + ", ".join(UI_RENDERERS)
renderer_classes = (*UI_RENDERERS[renderer], *_spec_renderers)
renderer_classes = UI_RENDERERS[renderer] + _spec_renderers
return cls.as_cached_view(cache_timeout, cache_kwargs, renderer_classes=renderer_classes)

View File

View File

@ -0,0 +1,26 @@
# Generated by Django 2.0 on 2017-12-05 04:05
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='Main article headline', max_length=255, unique=True)),
('body', models.TextField(help_text='Article content', max_length=5000)),
('slug', models.SlugField(blank=True, help_text='Unique URL slug identifying the article', unique=True)),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_modified', models.DateTimeField(auto_now=True)),
('cover', models.ImageField(blank=True, upload_to='article/original/')),
],
),
]

View File

@ -0,0 +1,11 @@
from django.db import models
class Article(models.Model):
title = models.CharField(help_text="title model help_text", max_length=255, blank=False, unique=True)
body = models.TextField(help_text="article model help_text", max_length=5000, blank=False)
slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
cover = models.ImageField(upload_to='article/original/', blank=True)

View File

@ -0,0 +1,16 @@
from rest_framework import serializers
from articles.models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ('title', 'body', 'slug', 'date_created', 'date_modified')
read_only_fields = ('date_created', 'date_modified')
lookup_field = 'slug'
extra_kwargs = {'body': {'help_text': 'body serializer help_text'}}
class ImageUploadSerializer(serializers.Serializer):
upload = serializers.ImageField(help_text="image serializer help_text")

View File

@ -0,0 +1,11 @@
from django.conf.urls import include, url
from rest_framework.routers import SimpleRouter
from articles import views
router = SimpleRouter()
router.register('', views.ArticleViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
]

View File

@ -0,0 +1,60 @@
import datetime
from django_filters.rest_framework import DjangoFilterBackend, filters
from rest_framework import viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.parsers import MultiPartParser
from rest_framework.response import Response
from articles import serializers
from articles.models import Article
class ArticleViewSet(viewsets.ModelViewSet):
"""
ArticleViewSet class docstring
retrieve:
retrieve class docstring
destroy:
destroy class docstring
"""
queryset = Article.objects.all()
lookup_field = 'slug'
serializer_class = serializers.ArticleSerializer
pagination_class = LimitOffsetPagination
max_page_size = 5
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
filter_fields = ('title',)
ordering_fields = ('date_modified',)
ordering = ('username',)
@list_route(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)
articles = self.get_queryset().filter(date_created__range=(today_min, today_max)).all()
serializer = self.serializer_class(articles, many=True)
return Response(serializer.data)
@detail_route(
methods=['get', 'post'],
parser_classes=(MultiPartParser,),
serializer_class=serializers.ImageUploadSerializer,
)
def image(self, request, slug=None):
"""
image method docstring
"""
pass
def update(self, request, *args, **kwargs):
"""update method docstring"""
return super(ArticleViewSet, self).update(request, *args, **kwargs)
def destroy(self, request, *args, **kwargs):
"""destroy method docstring"""
return super(ArticleViewSet, self).destroy(request, *args, **kwargs)

Binary file not shown.

View File

@ -0,0 +1,27 @@
# Generated by Django 2.0 on 2017-12-05 04:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('snippets', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='snippet',
name='owner',
field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='snippets', to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
migrations.AlterField(
model_name='snippet',
name='code',
field=models.TextField(help_text='code model help text'),
),
]

View File

@ -9,11 +9,12 @@ STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
code = models.TextField(help_text="code model help text")
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
class Meta:
ordering = ('created', )
ordering = ('created',)

View File

@ -18,7 +18,8 @@ class SnippetSerializer(serializers.Serializer):
create: docstring for create from serializer classdoc
"""
id = serializers.IntegerField(read_only=True, help_text="id help text")
id = serializers.IntegerField(read_only=True, help_text="id serializer help text")
owner = serializers.ReadOnlyField(source='owner.username')
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'base_template': 'textarea.html'})
linenos = serializers.BooleanField(required=False)

View File

@ -1,4 +1,5 @@
from rest_framework import generics
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@ -8,9 +9,12 @@ class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def post(self, request, *args, **kwargs):
"""post method docstring"""
return super().post(request, *args, **kwargs)
return super(SnippetList, self).post(request, *args, **kwargs)
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
@ -28,8 +32,8 @@ class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
def patch(self, request, *args, **kwargs):
"""patch method docstring"""
return super().patch(request, *args, **kwargs)
return super(SnippetDetail, self).patch(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
"""delete method docstring"""
return super().patch(request, *args, **kwargs)
return super(SnippetDetail, self).patch(request, *args, **kwargs)

View File

@ -30,7 +30,9 @@ INSTALLED_APPS = [
'corsheaders',
'drf_swagger',
'snippets'
'snippets',
'users',
'articles',
]
MIDDLEWARE = [
@ -42,6 +44,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'drf_swagger.middleware.SwaggerExceptionMiddleware',
]
ROOT_URLCONF = 'testproj.urls'

View File

@ -1,91 +0,0 @@
import json
from django.test import TestCase
from ruamel import yaml
from drf_swagger import openapi, codecs
from drf_swagger.generators import OpenAPISchemaGenerator
class SchemaGeneratorTest(TestCase):
def setUp(self):
self.generator = OpenAPISchemaGenerator(
info=openapi.Info("Test generator", "v1"),
version="v2",
)
self.codec_json = codecs.OpenAPICodecJson(['flex', 'ssv'])
self.codec_yaml = codecs.OpenAPICodecYaml(['ssv', 'flex'])
def _validate_schema(self, 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)
def test_schema_generates_without_errors(self):
self.generator.get_schema(None, True)
def test_schema_is_valid(self):
swagger = self.generator.get_schema(None, True)
self.codec_yaml.encode(swagger)
def test_invalid_schema_fails(self):
bad_generator = OpenAPISchemaGenerator(
info=openapi.Info(
"Test generator", "v1",
contact=openapi.Contact(name=69, email=[])
),
version="v2",
)
swagger = bad_generator.get_schema(None, True)
with self.assertRaises(codecs.SwaggerValidationError):
self.codec_json.encode(swagger)
def test_json_codec_roundtrip(self):
swagger = self.generator.get_schema(None, True)
json_bytes = self.codec_json.encode(swagger)
self._validate_schema(json.loads(json_bytes.decode('utf-8')))
def test_yaml_codec_roundtrip(self):
swagger = self.generator.get_schema(None, True)
json_bytes = self.codec_yaml.encode(swagger)
self._validate_schema(yaml.safe_load(json_bytes.decode('utf-8')))
class SchemaTest(TestCase):
def setUp(self):
self.generator = OpenAPISchemaGenerator(
info=openapi.Info("Test generator", "v1"),
version="v2",
)
self.codec_json = codecs.OpenAPICodecJson(['flex', 'ssv'])
self.codec_yaml = codecs.OpenAPICodecYaml(['ssv', 'flex'])
self.swagger = self.generator.get_schema(None, True)
json_bytes = self.codec_yaml.encode(self.swagger)
self.swagger_dict = yaml.safe_load(json_bytes.decode('utf-8'))
def test_paths_not_empty(self):
self.assertTrue(bool(self.swagger_dict['paths']))
def test_appropriate_status_codes(self):
snippets_list = self.swagger_dict['paths']['/snippets/']
self.assertTrue('200' in snippets_list['get']['responses'])
self.assertTrue('201' in snippets_list['post']['responses'])
snippets_detail = self.swagger_dict['paths']['/snippets/{id}/']
self.assertTrue('200' in snippets_detail['get']['responses'])
self.assertTrue('200' in snippets_detail['put']['responses'])
self.assertTrue('200' in snippets_detail['patch']['responses'])
self.assertTrue('204' in snippets_detail['delete']['responses'])
def test_operation_docstrings(self):
snippets_list = self.swagger_dict['paths']['/snippets/']
self.assertEqual(snippets_list['get']['description'], "SnippetList classdoc")
self.assertEqual(snippets_list['post']['description'], "post method docstring")
snippets_detail = self.swagger_dict['paths']['/snippets/{id}/']
self.assertEqual(snippets_detail['get']['description'], "SnippetDetail classdoc")
self.assertEqual(snippets_detail['put']['description'], "put class docstring")
self.assertEqual(snippets_detail['patch']['description'], "patch method docstring")
self.assertEqual(snippets_detail['delete']['description'], "delete method docstring")

View File

@ -1,10 +1,10 @@
from django.conf.urls import url, include
from django.contrib import admin
from drf_swagger.views import get_schema_view
from drf_swagger import openapi
from rest_framework import permissions
from drf_swagger import openapi
from drf_swagger.views import get_schema_view
schema_view = get_schema_view(
openapi.Info(
title="Snippets API",
@ -14,16 +14,18 @@ schema_view = get_schema_view(
contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="BSD License"),
),
validators=['flex', 'ssv'],
public=False,
validators=['ssv', 'flex'],
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
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'^admin/', admin.site.urls),
url(r'^snippets/', include('snippets.urls')),
url(r'^articles/', include('articles.urls')),
url(r'^users/', include('users.urls')),
]

View File

View File

View File

@ -0,0 +1,12 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from snippets.models import Snippet
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ('id', 'username', 'snippets')

View File

@ -0,0 +1,8 @@
from django.conf.urls import url
from users import views
urlpatterns = [
url(r'^$', views.UserList.as_view()),
url(r'^(?P<pk>[0-9]+)/$', views.user_detail),
]

View File

@ -0,0 +1,24 @@
from django.contrib.auth.models import User
from rest_framework.decorators import api_view
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from rest_framework.views import APIView
from users.serializers import UserSerializer
class UserList(APIView):
"""UserList cbv classdoc"""
def get(self, request):
queryset = User.objects.all()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
@api_view(['GET'])
def user_detail(request, pk):
"""user_detail fbv docstring"""
user = get_object_or_404(User.objects, pk=pk)
serializer = UserSerializer(user)
return Response(serializer.data)

View File

@ -1,13 +1,14 @@
import pytest
from ruamel import yaml
from drf_swagger import openapi, codecs
from drf_swagger.generators import OpenAPISchemaGenerator
from ruamel import yaml
@pytest.fixture
def generator():
return OpenAPISchemaGenerator(
info=openapi.Info("Test generator", "v1"),
info=openapi.Info(title="Test generator", default_version="v1"),
version="v2",
)
@ -27,3 +28,28 @@ def swagger_dict():
swagger = generator().get_schema(None, True)
json_bytes = codec_yaml().encode(swagger)
return yaml.safe_load(json_bytes.decode('utf-8'))
@pytest.fixture
def validate_schema():
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)
return validate_schema
@pytest.fixture
def bad_settings():
from drf_swagger.app_settings import swagger_settings, SWAGGER_DEFAULTS
bad_security = {
'bad': {
'bad_attribute': 'should not be accepted'
}
}
SWAGGER_DEFAULTS['SECURITY_DEFINITIONS'].update(bad_security)
yield swagger_settings
del SWAGGER_DEFAULTS['SECURITY_DEFINITIONS']['bad']

View File

@ -0,0 +1,6 @@
def test_operation_docstrings(swagger_dict):
users_list = swagger_dict['paths']['/users/']
assert users_list['get']['description'] == "UserList cbv classdoc"
users_detail = swagger_dict['paths']['/users/{id}/']
assert users_detail['get']['description'] == "user_detail fbv docstring"

View File

@ -0,0 +1,22 @@
def test_appropriate_status_codes(swagger_dict):
snippets_list = swagger_dict['paths']['/snippets/']
assert '200' in snippets_list['get']['responses']
assert '201' in snippets_list['post']['responses']
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
assert '200' in snippets_detail['get']['responses']
assert '200' in snippets_detail['put']['responses']
assert '200' in snippets_detail['patch']['responses']
assert '204' in snippets_detail['delete']['responses']
def test_operation_docstrings(swagger_dict):
snippets_list = swagger_dict['paths']['/snippets/']
assert snippets_list['get']['description'] == "SnippetList classdoc"
assert snippets_list['post']['description'] == "post method docstring"
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
assert snippets_detail['get']['description'] == "SnippetDetail classdoc"
assert snippets_detail['put']['description'] == "put class docstring"
assert snippets_detail['patch']['description'] == "patch method docstring"
assert snippets_detail['delete']['description'] == "delete method docstring"

View File

@ -0,0 +1,29 @@
def test_appropriate_status_codes(swagger_dict):
articles_list = swagger_dict['paths']['/articles/']
assert '200' in articles_list['get']['responses']
assert '201' in articles_list['post']['responses']
articles_detail = swagger_dict['paths']['/articles/{slug}/']
assert '200' in articles_detail['get']['responses']
assert '200' in articles_detail['put']['responses']
assert '200' in articles_detail['patch']['responses']
assert '204' in articles_detail['delete']['responses']
def test_operation_docstrings(swagger_dict):
articles_list = swagger_dict['paths']['/articles/']
assert articles_list['get']['description'] == "ArticleViewSet class docstring"
assert articles_list['post']['description'] == "ArticleViewSet class docstring"
articles_detail = swagger_dict['paths']['/articles/{slug}/']
assert articles_detail['get']['description'] == "retrieve class docstring"
assert articles_detail['put']['description'] == "update method docstring"
assert articles_detail['patch']['description'] == "ArticleViewSet class docstring"
assert articles_detail['delete']['description'] == "destroy method docstring"
articles_today = swagger_dict['paths']['/articles/today/']
assert articles_today['get']['description'] == "ArticleViewSet class docstring"
articles_image = swagger_dict['paths']['/articles/{slug}/image/']
assert articles_image['get']['description'] == "image method docstring"
assert articles_image['post']['description'] == "image method docstring"

View File

@ -1,17 +1,10 @@
import json
import pytest
from drf_swagger import openapi, codecs
from drf_swagger.generators import OpenAPISchemaGenerator
from ruamel import yaml
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)
from drf_swagger import openapi, codecs
from drf_swagger.generators import OpenAPISchemaGenerator
def test_schema_generates_without_errors(generator):
@ -24,9 +17,10 @@ def test_schema_is_valid(generator, codec_yaml):
def test_invalid_schema_fails(codec_json):
# noinspection PyTypeChecker
bad_generator = OpenAPISchemaGenerator(
info=openapi.Info(
"Test generator", "v1",
title="Test generator", default_version="v1",
contact=openapi.Contact(name=69, email=[])
),
version="v2",
@ -37,13 +31,13 @@ def test_invalid_schema_fails(codec_json):
codec_json.encode(swagger)
def test_json_codec_roundtrip(codec_json, generator):
def test_json_codec_roundtrip(codec_json, generator, validate_schema):
swagger = generator.get_schema(None, True)
json_bytes = codec_json.encode(swagger)
validate_schema(json.loads(json_bytes.decode('utf-8')))
def test_yaml_codec_roundtrip(codec_yaml, generator):
def test_yaml_codec_roundtrip(codec_yaml, generator, validate_schema):
swagger = generator.get_schema(None, True)
json_bytes = codec_yaml.encode(swagger)
validate_schema(yaml.safe_load(json_bytes.decode('utf-8')))

View File

@ -1,24 +1,2 @@
def test_paths_not_empty(swagger_dict):
assert bool(swagger_dict['paths'])
def test_appropriate_status_codes(swagger_dict):
snippets_list = swagger_dict['paths']['/snippets/']
assert '200' in snippets_list['get']['responses']
assert '201' in snippets_list['post']['responses']
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
assert '200' in snippets_detail['get']['responses']
assert '200' in snippets_detail['put']['responses']
assert '200' in snippets_detail['patch']['responses']
assert '204' in snippets_detail['delete']['responses']
def test_operation_docstrings(swagger_dict):
snippets_list = swagger_dict['paths']['/snippets/']
assert snippets_list['get']['description'] == "SnippetList classdoc"
assert snippets_list['post']['description'] == "post method docstring"
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
assert snippets_detail['get']['description'] == "SnippetDetail classdoc"
assert snippets_detail['put']['description'] == "put class docstring"
assert snippets_detail['patch']['description'] == "patch method docstring"
assert snippets_detail['delete']['description'] == "delete method docstring"
assert len(swagger_dict['paths']) > 0

View File

@ -0,0 +1,39 @@
import json
from ruamel import yaml
def _validate_text_schema_view(client, validate_schema, path, loader):
response = client.get(path)
assert response.status_code == 200
validate_schema(loader(response.content.decode('utf-8')))
def _validate_ui_schema_view(client, path, string):
response = client.get(path)
assert response.status_code == 200
assert string in response.content.decode('utf-8')
def test_swagger_json(client, validate_schema):
_validate_text_schema_view(client, validate_schema, "/swagger.json", json.loads)
def test_swagger_yaml(client, validate_schema):
_validate_text_schema_view(client, validate_schema, "/swagger.yaml", yaml.safe_load)
def test_exception_middleware(client, bad_settings):
response = client.get('/swagger.json')
assert response.status_code == 500
assert 'errors' in json.loads(response.content.decode('utf-8'))
def test_swagger_ui(client, validate_schema):
_validate_ui_schema_view(client, '/swagger/', 'swagger-ui-dist/swagger-ui-bundle.js')
_validate_text_schema_view(client, validate_schema, '/swagger/?format=openapi', json.loads)
def test_redoc(client, validate_schema):
_validate_ui_schema_view(client, '/redoc/', 'redoc/redoc.min.js')
_validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads)

View File

@ -0,0 +1,53 @@
from drf_swagger import openapi
def test_vendor_extensions():
"""Any attribute starting with x_ should map to a vendor property of the form x-camelCase"""
sd = openapi.SwaggerDict(x_vendor_ext_1='test')
sd.x_vendor_ext_2 = 'test'
assert 'x-vendorExt1' in sd
assert sd.x_vendor_ext_1 == 'test'
assert sd['x-vendorExt2'] == 'test'
del sd.x_vendor_ext_1
assert 'x-vendorExt1' not in sd
def test_ref():
"""The attribute 'ref' maps to the swagger key '$ref'"""
sd = openapi.SwaggerDict(ref='reftest')
assert '$ref' in sd
assert sd['$ref'] == sd.ref == 'reftest'
del sd['$ref']
assert not hasattr(sd, 'ref')
def test_leading_underscore_ignored():
"""Attributes with a leading underscore are set on the object as-is and are not added to its dict form"""
sd = openapi.SwaggerDict(_private_attr_1='not_camelised')
initial_len = len(sd)
sd._nope = 'not camelised either'
assert len(sd) == initial_len
assert 'privateAttr1' not in sd
assert sd._private_attr_1 == 'not_camelised'
assert '_private_attr_1' not in sd
assert hasattr(sd, '_nope')
del sd._nope
assert not hasattr(sd, '_nope')
def test_trailing_underscore_stripped():
"""Trailing underscores are stripped when converting attribute names.
This allows, for example, python keywords to function as SwaggerDict attributes."""
sd = openapi.SwaggerDict(trailing_underscore_='trailing')
sd.in_ = 'trailing'
assert 'in' in sd
assert 'trailingUnderscore' in sd
assert sd.trailing_underscore == sd['in']
assert hasattr(sd, 'in___')
del sd.in_
assert 'in' not in sd
assert not hasattr(sd, 'in__')

44
tox.ini
View File

@ -1,20 +1,46 @@
[tox]
envlist = py35,py36,py37,flake8
envlist =
py27-drf37,
py{34,35,36,37}-drf37,
py36-drfmaster,
flake8
[travis:env]
DRF =
3.7: drf37
master: drfmaster
[testenv]
deps=
-rrequirements.txt
-rrequirements_validation.txt
-rrequirements_test.txt
commands=
pytest --cov-append --cov=drf_swagger
deps =
drf37: djangorestframework>=3.7.3,<3.8
# py27 is tested with Django <2.0 (Django 2.0 no longer supports python 2)
py27: Django>=1.11,<2.0
# main testing configurations
py{34,35,36,37}-drf37: Django>=1.11,<2.1
# py3 with the latest build of Django and django-rest-framework to get early warning of compatibility issues
drfmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
drfmaster: https://github.com/django/django/archive/master.tar.gz
# other dependencies
-rrequirements/base.txt
-rrequirements/validation.txt
-rrequirements/test.txt
commands =
pytest --cov-config .coveragerc --cov-append --cov
[testenv:py36-drfmaster]
pip_pre = True
[testenv:flake8]
skip_install = true
deps=
deps =
flake8
commands=
flake8 drf_swagger testproj
flake8 src/drf_swagger testproj tests setup.py
[flake8]
max-line-length = 120