Skip to content

Commit

Permalink
Add more tests and cleanup coverage reports (#13)
Browse files Browse the repository at this point in the history
Closes #11
  • Loading branch information
axnsan12 committed Dec 12, 2017
1 parent 53ac55a commit 8883894
Show file tree
Hide file tree
Showing 29 changed files with 385 additions and 134 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ You want to contribute some code? Great! Here are a few steps to get you started
$ virtualenv venv
$ source venv/bin/activate
(venv) $ pip install -e .[validation,test]
(venv) $ pip install -r requirements/dev.txt
(venv) $ pip install -e .[validation]
(venv) $ pip install -rrequirements/dev.txt -rrequirements/test.txt
#. Make your changes and check them against the test project

Expand Down
12 changes: 12 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,18 @@ This method is currently the only way to get both syntactic and semantic validat
The other validators only provide JSON schema-level validation, but miss things like duplicate operation names,
improper content types, etc

5. Code generation
==================

You can use the specification outputted by this library together with
`swagger-codegen <https://github.com/swagger-api/swagger-codegen>`_ to generate client code in your language of choice:

.. code:: console
$ docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate -i /local/tests/reference.yaml -l javascript -o /local/.codegen/js
See the github page linked above for more details.

**********
Background
**********
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drf-swagger",
"dependencies": {
"swagger-ui-dist": "^3.5.0"
"swagger-ui-dist": "^3.6.1"
}
}
4 changes: 3 additions & 1 deletion requirements/test.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# pytest runner + plugins
pytest-django>=3.1.2
pytest>=2.9
pytest-pythonpath>=0.7.1
pytest-cov>=2.5.1
# latest pip version of pytest-django is more than a year old and does not support Django 2.0
git+https://github.com/pytest-dev/pytest-django.git@94cccb956435dd7a719606744ee7608397e1eafb
datadiff==2.0.0

# test project requirements
Expand Down
3 changes: 0 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def read_req(req_file):

requirements = ['djangorestframework>=3.7.0'] + read_req('base.txt')
requirements_validation = read_req('validation.txt')
requirements_test = read_req('test.txt')

setup(
name='drf-swagger',
Expand All @@ -25,10 +24,8 @@ def read_req(req_file):
package_dir={'': 'src'},
include_package_data=True,
install_requires=requirements,
tests_require=requirements_test,
extras_require={
'validation': requirements_validation,
'test': requirements_test,
},
license='BSD License',
description='Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code.',
Expand Down
43 changes: 38 additions & 5 deletions src/drf_swagger/app_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.conf import settings
from rest_framework.settings import APISettings
from rest_framework.settings import perform_import

SWAGGER_DEFAULTS = {
'USE_SESSION_AUTH': True,
Expand Down Expand Up @@ -30,16 +30,49 @@

IMPORT_STRINGS = []


class AppSettings(object):
"""
Stolen from Django Rest Framework, removed caching for easier testing
"""

def __init__(self, user_settings, defaults, import_strings=None):
self._user_settings = user_settings
self.defaults = defaults
self.import_strings = import_strings or []

@property
def user_settings(self):
return getattr(settings, self._user_settings, {})

def __getattr__(self, attr):
if attr not in self.defaults:
raise AttributeError("Invalid setting: '%s'" % attr) # pragma: no cover

try:
# Check if present in user settings
val = self.user_settings[attr]
except KeyError:
# Fall back to defaults
val = self.defaults[attr]

# Coerce import strings into classes
if attr in self.import_strings:
val = perform_import(val, attr)

return val


#:
swagger_settings = APISettings(
user_settings=getattr(settings, 'SWAGGER_SETTINGS', {}),
swagger_settings = AppSettings(
user_settings='SWAGGER_SETTINGS',
defaults=SWAGGER_DEFAULTS,
import_strings=IMPORT_STRINGS,
)

#:
redoc_settings = APISettings(
user_settings=getattr(settings, 'REDOC_SETTINGS', {}),
redoc_settings = AppSettings(
user_settings='REDOC_SETTINGS',
defaults=REDOC_DEFAULTS,
import_strings=IMPORT_STRINGS,
)
5 changes: 1 addition & 4 deletions src/drf_swagger/codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ def _validate_swagger_spec_validator(spec, codec):
class _OpenAPICodec(object):
media_type = None

#: Allows easier mocking of settings
settings = swagger_settings

def __init__(self, validators):
self._validators = validators

Expand Down Expand Up @@ -89,7 +86,7 @@ def generate_swagger_object(self, swagger):
:return: swagger spec as dict
:rtype: OrderedDict
"""
swagger.security_definitions = self.settings.SECURITY_DEFINITIONS
swagger.security_definitions = swagger_settings.SECURITY_DEFINITIONS
return swagger


Expand Down
9 changes: 5 additions & 4 deletions src/drf_swagger/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ def create_view(self, callback, method, request=None):
view = self._gen.create_view(callback, method, request)
overrides = getattr(callback, 'swagger_auto_schema', None)
if overrides is not None:
# decorated function based view must have its decorator information passed on to th re-instantiated view
# decorated function based view must have its decorator information passed on to the re-instantiated view
for method, _ in overrides.items():
view_method = getattr(view, method, None)
if view_method is not None:
if view_method is not None: # pragma: no cover
setattr(view_method.__func__, 'swagger_auto_schema', overrides)
return view

Expand Down Expand Up @@ -137,7 +137,8 @@ def get_paths(self, endpoints, components):
schema = auto_schema_cls(view, path, method, overrides, components)
operations[method.lower()] = schema.get_operation(operation_keys)

paths[path] = openapi.PathItem(parameters=path_parameters, **operations)
if operations:
paths[path] = openapi.PathItem(parameters=path_parameters, **operations)

return openapi.Paths(paths=paths)

Expand Down Expand Up @@ -178,7 +179,7 @@ def get_path_parameters(self, path, view_cls):
try:
model_field = model._meta.get_field(variable)
except Exception:
model_field = None
model_field = None # pragma: no cover

if model_field is not None and model_field.help_text:
description = force_text(model_field.help_text)
Expand Down
4 changes: 2 additions & 2 deletions src/drf_swagger/inspectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ def add_manual_parameters(self, parameters):
parameters = OrderedDict(((param.name, param.in_), param) for param in parameters)
manual_parameters = self.overrides.get('manual_parameters', None) or []

if any(param.in_ == openapi.IN_BODY for param in manual_parameters):
if any(param.in_ == openapi.IN_BODY for param in manual_parameters): # pragma: no cover
raise SwaggerGenerationError("specify the body parameter as a Schema or Serializer in request_body")
if any(param.in_ == openapi.IN_FORM for param in manual_parameters):
if any(param.in_ == openapi.IN_FORM for param in manual_parameters): # pragma: no cover
if any(param.in_ == openapi.IN_BODY for param in parameters.values()):
raise SwaggerGenerationError("cannot add form parameters when the request has a request schema; "
"did you forget to set an appropriate parser class on the view?")
Expand Down
5 changes: 2 additions & 3 deletions src/drf_swagger/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from future.utils import raise_from
from inflection import camelize


TYPE_OBJECT = "object" #:
TYPE_STRING = "string" #:
TYPE_NUMBER = "number" #:
Expand Down Expand Up @@ -212,7 +211,7 @@ 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:
if path_obj is not None: # pragma: no cover
self[path] = path_obj
self._insert_extras__()

Expand Down Expand Up @@ -403,7 +402,7 @@ def __init__(self, responses, default=None, **extra):
"""
super(Responses, self).__init__(**extra)
for status, response in responses.items():
if response is not None:
if response is not None: # pragma: no cover
self[str(status)] = response
self.default = default
self._insert_extras__()
Expand Down
24 changes: 12 additions & 12 deletions src/drf_swagger/static/drf-swagger/swagger-ui-dist/package.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
{
"_from": "swagger-ui-dist",
"_id": "swagger-ui-dist@3.5.0",
"_from": "swagger-ui-dist@3.6.1",
"_id": "swagger-ui-dist@3.6.1",
"_inBundle": false,
"_integrity": "sha1-JuvzMRqaYP6dFwYS7tUoKmW8kiY=",
"_integrity": "sha1-uzQgV/h2COTs2DlGMDSJxjYicgY=",
"_location": "/swagger-ui-dist",
"_phantomChildren": {},
"_requested": {
"type": "tag",
"type": "version",
"registry": true,
"raw": "swagger-ui-dist",
"raw": "swagger-ui-dist@3.6.1",
"name": "swagger-ui-dist",
"escapedName": "swagger-ui-dist",
"rawSpec": "",
"rawSpec": "3.6.1",
"saveSpec": null,
"fetchSpec": "latest"
"fetchSpec": "3.6.1"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.5.0.tgz",
"_shasum": "26ebf3311a9a60fe9d170612eed5282a65bc9226",
"_spec": "swagger-ui-dist",
"_where": "C:\\Projects\\drf_openapi",
"_resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.6.1.tgz",
"_shasum": "bb342057f87608e4ecd83946303489c636227206",
"_spec": "swagger-ui-dist@3.6.1",
"_where": "C:\\Projects\\drf-swagger",
"bugs": {
"url": "https://github.com/swagger-api/swagger-ui/issues"
},
Expand Down Expand Up @@ -68,5 +68,5 @@
"type": "git",
"url": "git+ssh://git@github.com/swagger-api/swagger-ui.git"
},
"version": "3.5.0"
"version": "3.6.1"
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions src/drf_swagger/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
it will automatically be converted into a :class:`.Schema`
"""

def decorator(view_method):
data = {
'auto_schema': auto_schema,
Expand Down Expand Up @@ -165,6 +166,10 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
def SwaggerType(**instance_kwargs):
if swagger_object_type == openapi.Parameter:
instance_kwargs['required'] = field.required
if swagger_object_type != openapi.Items:
default = getattr(field, 'default', serializers.empty)
if default is not serializers.empty:
instance_kwargs['default'] = default
instance_kwargs.update(kwargs)
return swagger_object_type(title=title, description=description, **instance_kwargs)

Expand Down Expand Up @@ -249,7 +254,7 @@ def make_schema_definition():
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)
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_SLUG, pattern=find_regex(field))
elif isinstance(field, serializers.URLField):
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI)
elif isinstance(field, serializers.IPAddressField):
Expand All @@ -272,19 +277,14 @@ def make_schema_definition():
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 = serializer_field_to_swagger(field.child, ChildSwaggerType, definitions)
return SwaggerType(
type=openapi.TYPE_OBJECT,
additional_properties=child_schema
)

# TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField?
# TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField? JSONField

# everything else gets string by default
return SwaggerType(type=openapi.TYPE_STRING)
Expand All @@ -302,7 +302,7 @@ def find_regex(regex_field):
if isinstance(validator, RegexValidator):
if regex_validator is not None:
# bail if multiple validators are found - no obvious way to choose
return None
return None # pragma: no cover
regex_validator = validator

# regex_validator.regex should be a compiled re object...
Expand Down
13 changes: 12 additions & 1 deletion testproj/articles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@


class ArticleSerializer(serializers.ModelSerializer):
references = serializers.DictField(
help_text="this is a really bad example",
child=serializers.URLField(help_text="but i needed to test these 2 fields somehow"),
)
uuid = serializers.UUIDField(help_text="should articles have UUIDs?")

class Meta:
model = Article
fields = ('title', 'body', 'slug', 'date_created', 'date_modified')
fields = ('title', 'body', 'slug', 'date_created', 'date_modified', 'references', 'uuid')
read_only_fields = ('date_created', 'date_modified')
lookup_field = 'slug'
extra_kwargs = {'body': {'help_text': 'body serializer help_text'}}


class ImageUploadSerializer(serializers.Serializer):
what_am_i_doing = serializers.RegexField(regex=r"^69$", help_text="test")
image_styles = serializers.ListSerializer(
child=serializers.ChoiceField(choices=['wide', 'tall', 'thumb', 'social']),
help_text="Parameter with Items"
)
upload = serializers.ImageField(help_text="image serializer help_text")
3 changes: 2 additions & 1 deletion testproj/snippets/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ class SnippetSerializer(serializers.Serializer):
code = serializers.CharField(style={'base_template': 'textarea.html'})
linenos = serializers.BooleanField(required=False)
language = LanguageSerializer(help_text="Sample help text for language")
style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['friendly'])
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer())
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField")

def create(self, validated_data):
"""
Expand Down
6 changes: 6 additions & 0 deletions testproj/testproj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@
},
]

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}

SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login',
'LOGOUT_URL': '/admin/logout',
Expand Down

0 comments on commit 8883894

Please sign in to comment.