Add 'generate_swagger' management command (#31)

Closes #29.
openapi3
Beau Gunderson 2017-12-27 11:00:24 -08:00 committed by Cristi Vîjdea
parent 9f14114520
commit 1f190744cd
18 changed files with 408 additions and 107 deletions

View File

@ -14,6 +14,7 @@ exclude_lines =
# Don't complain if tests don't hit defensive assertion code: # Don't complain if tests don't hit defensive assertion code:
raise AssertionError raise AssertionError
raise ImproperlyConfigured
raise TypeError raise TypeError
raise NotImplementedError raise NotImplementedError
warnings.warn warnings.warn

View File

@ -27,9 +27,9 @@ Pull requests
You want to contribute some code? Great! Here are a few steps to get you started: You want to contribute some code? Great! Here are a few steps to get you started:
#. Fork the repository on GitHub #. **Fork the repository on GitHub**
#. Clone your fork and create a branch for the code you want to add #. **Clone your fork and create a branch for the code you want to add**
#. Create a new virtualenv and install the package in development mode #. **Create a new virtualenv and install the package in development mode**
.. code:: console .. code:: console
@ -38,7 +38,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
(venv) $ pip install -e .[validation] (venv) $ pip install -e .[validation]
(venv) $ pip install -rrequirements/dev.txt -rrequirements/test.txt (venv) $ pip install -rrequirements/dev.txt -rrequirements/test.txt
#. Make your changes and check them against the test project #. **Make your changes and check them against the test project**
.. code:: console .. code:: console
@ -46,17 +46,24 @@ You want to contribute some code? Great! Here are a few steps to get you started
(venv) $ python manage.py migrate (venv) $ python manage.py migrate
(venv) $ cat createsuperuser.py | python manage.py shell (venv) $ cat createsuperuser.py | python manage.py shell
(venv) $ python manage.py runserver (venv) $ python manage.py runserver
(venv) $ curl localhost:8000/swagger.yaml (venv) $ firefox localhost:8000/swagger/
#. Update the tests if necessary #. **Update the tests if necessary**
You can find them in the ``tests`` directory. You can find them in the ``tests`` directory.
If your change modifies the expected schema output, you should download the new generated ``swagger.yaml``, diff it If your change modifies the expected schema output, you should regenerate the reference schema at
against the old reference output in ``tests/reference.yaml``, and replace it after checking that no unexpected ``tests/reference.yaml``:
changes appeared.
#. Run tests. The project is setup to use tox and pytest for testing .. code:: console
(venv) $ cd testproj
(venv) $ python manage.py generate_swagger ../tests/reference.yaml --overwrite --user admin --url http://test.local:8002/
After checking the git diff to verify that no unexpected changes appeared, you should commit the new
``reference.yaml`` together with your changes.
#. **Run tests. The project is setup to use tox and pytest for testing**
.. code:: console .. code:: console
@ -65,7 +72,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
# (optional) run tests for other python versions in separate environments # (optional) run tests for other python versions in separate environments
(venv) $ tox (venv) $ tox
#. Update documentation #. **Update documentation**
If the change modifies behaviour or adds new features, you should update the documentation and ``README.rst`` If the change modifies behaviour or adds new features, you should update the documentation and ``README.rst``
accordingly. Documentation is written in reStructuredText and built using Sphinx. You can find the sources in the accordingly. Documentation is written in reStructuredText and built using Sphinx. You can find the sources in the
@ -77,10 +84,11 @@ You want to contribute some code? Great! Here are a few steps to get you started
(venv) $ tox -e docs (venv) $ tox -e docs
#. Push your branch and submit a pull request to the master branch on GitHub #. **Push your branch and submit a pull request to the master branch on GitHub**
Incomplete/Work In Progress pull requests are encouraged, because they allow you to get feedback and help more Incomplete/Work In Progress pull requests are encouraged, because they allow you to get feedback and help more
easily. easily.
#. Your code must pass all the required travis jobs before it is merged. As of now, this includes running on #. **Your code must pass all the required travis jobs before it is merged**
Python 2.7, 3.4, 3.5 and 3.6, and building the docs succesfully.
As of now, this consists of running on Python 2.7, 3.4, 3.5 and 3.6, and building the docs succesfully.

View File

@ -201,6 +201,11 @@ The possible settings and their default values are as follows:
'drf_yasg.inspectors.CoreAPICompatInspector', 'drf_yasg.inspectors.CoreAPICompatInspector',
], ],
# default api Info if none is otherwise given; should be an import string to an openapi.Info object
'DEFAULT_INFO': None,
# default API url if none is otherwise given
'DEFAULT_API_URL': '',
'USE_SESSION_AUTH': True, # add Django Login and Django Logout buttons, CSRF token to swagger UI page 'USE_SESSION_AUTH': True, # add Django Login and Django Logout buttons, CSRF token to swagger UI page
'LOGIN_URL': getattr(django.conf.settings, 'LOGIN_URL', None), # URL for the login button 'LOGIN_URL': getattr(django.conf.settings, 'LOGIN_URL', None), # URL for the login button
'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button 'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button

View File

@ -3,6 +3,13 @@ Changelog
######### #########
*********
**1.1.1**
*********
- **ADDED:** :ref:`generate_swagger management command <management-command>`
(:issue:`29`, :pr:`31`, thanks to :ghuser:`beaugunderson`)
********* *********
**1.1.0** **1.1.0**
********* *********

View File

@ -138,7 +138,7 @@ The ``@swagger_auto_schema`` decorator
You can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decorator on view functions to override You can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decorator on view functions to override
some properties of the generated :class:`.Operation`. For example, in a ``ViewSet``, some properties of the generated :class:`.Operation`. For example, in a ``ViewSet``,
.. code:: python .. code-block:: python
@swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'}) @swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'})
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
@ -153,7 +153,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
* for function based ``@api_view``\ s, because the same view can handle multiple methods, and thus represent multiple * for function based ``@api_view``\ s, because the same view can handle multiple methods, and thus represent multiple
operations, you have to add the decorator multiple times if you want to override different operations: operations, you have to add the decorator multiple times if you want to override different operations:
.. code:: python .. code-block:: python
test_param = openapi.Parameter('test', openapi.IN_QUERY, description="test manual param", type=openapi.TYPE_BOOLEAN) test_param = openapi.Parameter('test', openapi.IN_QUERY, description="test manual param", type=openapi.TYPE_BOOLEAN)
user_response = openapi.Response('response description', UserSerializer) user_response = openapi.Response('response description', UserSerializer)
@ -169,7 +169,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
* for class based ``APIView``, ``GenericAPIView`` and non-``ViewSet`` derivatives, you have to decorate the respective * for class based ``APIView``, ``GenericAPIView`` and non-``ViewSet`` derivatives, you have to decorate the respective
method of each operation: method of each operation:
.. code:: python .. code-block:: python
class UserList(APIView): class UserList(APIView):
@swagger_auto_schema(responses={200: UserSerializer(many=True)}) @swagger_auto_schema(responses={200: UserSerializer(many=True)})
@ -186,7 +186,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
respond to multiple HTTP methods and thus have multiple operations that must be decorated separately: respond to multiple HTTP methods and thus have multiple operations that must be decorated separately:
.. code:: python .. code-block:: python
class ArticleViewSet(viewsets.ModelViewSet): class ArticleViewSet(viewsets.ModelViewSet):
# method or 'methods' can be skipped because the list_route only handles a single method (GET) # method or 'methods' can be skipped because the list_route only handles a single method (GET)
@ -214,7 +214,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
If you want to customize the generation of a method you are not implementing yourself, you can use If you want to customize the generation of a method you are not implementing yourself, you can use
``swagger_auto_schema`` in combination with Django's ``method_decorator``: ``swagger_auto_schema`` in combination with Django's ``method_decorator``:
.. code:: python .. code-block:: python
@method_decorator(name='list', decorator=swagger_auto_schema( @method_decorator(name='list', decorator=swagger_auto_schema(
operation_description="description from swagger_auto_schema via method_decorator" operation_description="description from swagger_auto_schema via method_decorator"
@ -229,7 +229,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
You can go even further and directly decorate the result of ``as_view``, in the same manner you would You can go even further and directly decorate the result of ``as_view``, in the same manner you would
override an ``@api_view`` as described above: override an ``@api_view`` as described above:
.. code:: python .. code-block:: python
decorated_login_view = \ decorated_login_view = \
swagger_auto_schema( swagger_auto_schema(
@ -256,7 +256,7 @@ Serializer ``Meta`` nested class
You can define some per-serializer options by adding a ``Meta`` class to your serializer, e.g.: You can define some per-serializer options by adding a ``Meta`` class to your serializer, e.g.:
.. code:: python .. code-block:: python
class WhateverSerializer(Serializer): class WhateverSerializer(Serializer):
... ...
@ -288,7 +288,7 @@ class-level attribute named ``swagger_schema`` on the view class, or
For example, to generate all operation IDs as camel case, you could do: For example, to generate all operation IDs as camel case, you could do:
.. code:: python .. code-block:: python
from inflection import camelize from inflection import camelize
@ -331,7 +331,7 @@ For customizing behavior related to specific field, serializer, filter or pagina
A :class:`~.inspectors.FilterInspector` that adds a description to all ``DjangoFilterBackend`` parameters could be A :class:`~.inspectors.FilterInspector` that adds a description to all ``DjangoFilterBackend`` parameters could be
implemented like so: implemented like so:
.. code:: python .. code-block:: python
class DjangoFilterDescriptionInspector(CoreAPICompatInspector): class DjangoFilterDescriptionInspector(CoreAPICompatInspector):
def get_filter_parameters(self, filter_backend): def get_filter_parameters(self, filter_backend):
@ -357,7 +357,7 @@ implemented like so:
A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``title`` attribute from all generated A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``title`` attribute from all generated
:class:`.Schema` objects: :class:`.Schema` objects:
.. code:: python .. code-block:: python
class NoSchemaTitleInspector(FieldInspector): class NoSchemaTitleInspector(FieldInspector):
def process_result(self, result, method_name, obj, **kwargs): def process_result(self, result, method_name, obj, **kwargs):

View File

@ -2,6 +2,7 @@
Serving the schema Serving the schema
################## ##################
************************************************ ************************************************
``get_schema_view`` and the ``SchemaView`` class ``get_schema_view`` and the ``SchemaView`` class
************************************************ ************************************************
@ -14,7 +15,7 @@ in the README for a usage example.
You can also subclass :class:`.SchemaView` by extending the return value of :func:`.get_schema_view`, e.g.: You can also subclass :class:`.SchemaView` by extending the return value of :func:`.get_schema_view`, e.g.:
.. code:: python .. code-block:: python
SchemaView = get_schema_view(info, ...) SchemaView = get_schema_view(info, ...)
@ -33,3 +34,27 @@ codec and the view.
You can use your custom renderer classes as kwargs to :meth:`.SchemaView.as_cached_view` or by subclassing You can use your custom renderer classes as kwargs to :meth:`.SchemaView.as_cached_view` or by subclassing
:class:`.SchemaView`. :class:`.SchemaView`.
.. _management-command:
******************
Management command
******************
.. versionadded:: 1.1.1
If you only need a swagger spec file in YAML or JSON format, you can use the ``generate_swagger`` management command
to get it without having to start the web server:
.. code-block:: console
$ python manage.py generate_swagger swagger.json
See the command help for more advanced options:
.. code-block:: console
$ python manage.py generate_swagger --help
usage: manage.py generate_swagger [-h] [--version] [-v {0,1,2,3}]
... more options ...

View File

@ -15,7 +15,7 @@ Example:
**settings.py** **settings.py**
.. code:: python .. code-block:: python
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
@ -91,6 +91,25 @@ Paginator inspectors given to :func:`@swagger_auto_schema <.swagger_auto_schema>
:class:`'drf_yasg.inspectors.CoreAPICompatInspector' <.inspectors.CoreAPICompatInspector>`, |br| \ :class:`'drf_yasg.inspectors.CoreAPICompatInspector' <.inspectors.CoreAPICompatInspector>`, |br| \
``]`` ``]``
Swagger document attributes
===========================
DEFAULT_INFO
------------
An import string to an :class:`.openapi.Info` object. This will be used when running the ``generate_swagger``
management command, or if no ``info`` argument is passed to ``get_schema_view``.
**Default**: :python:`None`
DEFAULT_API_URL
---------------
A string representing the default API URL. This will be used to populate the ``host``, ``schemes`` and ``basePath``
attributes of the Swagger document if no API URL is otherwise provided.
**Default**: :python:`''`
Authorization Authorization
============= =============
@ -124,7 +143,7 @@ See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#sec
**Default**: **Default**:
.. code:: python .. code-block:: python
'basic': { 'basic': {
'type': 'basic' 'type': 'basic'

View File

@ -22,6 +22,9 @@ SWAGGER_DEFAULTS = {
'drf_yasg.inspectors.CoreAPICompatInspector', 'drf_yasg.inspectors.CoreAPICompatInspector',
], ],
'DEFAULT_INFO': None,
'DEFAULT_API_URL': '',
'USE_SESSION_AUTH': True, 'USE_SESSION_AUTH': True,
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
'basic': { 'basic': {
@ -53,6 +56,7 @@ IMPORT_STRINGS = [
'DEFAULT_FIELD_INSPECTORS', 'DEFAULT_FIELD_INSPECTORS',
'DEFAULT_FILTER_INSPECTORS', 'DEFAULT_FILTER_INSPECTORS',
'DEFAULT_PAGINATOR_INSPECTORS', 'DEFAULT_PAGINATOR_INSPECTORS',
'DEFAULT_INFO',
] ]

View File

@ -59,12 +59,12 @@ class OpenAPISchemaGenerator(object):
""" """
endpoint_enumerator_class = EndpointEnumerator endpoint_enumerator_class = EndpointEnumerator
def __init__(self, info, version, url=None, patterns=None, urlconf=None): def __init__(self, info, version='', url=swagger_settings.DEFAULT_API_URL, patterns=None, urlconf=None):
""" """
:param .Info info: information about the API :param .Info info: information about the API
:param str version: API version string, takes preedence over the version in `info` :param str version: API version string; can be omitted to use `info.default_version`
:param str url: API :param str url: API url; can be empty to remove URL info from the result
:param patterns: if given, only these patterns will be enumerated for inclusion in the API spec :param patterns: if given, only these patterns will be enumerated for inclusion in the API spec
:param urlconf: if patterns is not given, use this urlconf to enumerate patterns; :param urlconf: if patterns is not given, use this urlconf to enumerate patterns;
if not given, the default urlconf is used if not given, the default urlconf is used

View File

@ -0,0 +1,132 @@
import json
import logging
import os
from collections import OrderedDict
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand
from rest_framework.test import APIRequestFactory, force_authenticate
from rest_framework.views import APIView
from ... import openapi
from ...app_settings import swagger_settings
from ...codecs import OpenAPICodecJson, OpenAPICodecYaml
from ...generators import OpenAPISchemaGenerator
class Command(BaseCommand):
help = 'Write the Swagger schema to disk in JSON or YAML format.'
def add_arguments(self, parser):
parser.add_argument(
'output_file', metavar='output-file',
nargs='?',
default='-',
type=str,
help='Output path for generated swagger document, or "-" for stdout.'
)
parser.add_argument(
'-o', '--overwrite',
default=False, action='store_true',
help='Overwrite the output file if it already exists. '
'Default behavior is to stop if the output file exists.'
)
parser.add_argument(
'-f', '--format', dest='format',
default='', choices=('json', 'yaml'),
type=str,
help='Output format. If not given, it is guessed from the output file extension and defaults to json.'
)
parser.add_argument(
'-u', '--url', dest='api_url',
default='',
type=str,
help='Base API URL - sets the host, scheme and basePath attributes of the generated document.'
)
parser.add_argument(
'-m', '--mock-request', dest='mock',
default=False, action='store_true',
help='Use a mock request when generating the swagger schema. This is useful if your views or serializers'
'depend on context from a request in order to function.'
)
parser.add_argument(
'--user', dest='user',
default='',
help='Username of an existing user to use for mocked authentication. This option implies --mock-request.'
)
parser.add_argument(
'-p', '--private',
default=False, action="store_true",
help='Hides endpoints not accesible to the target user. If --user is not given, only shows endpoints that '
'are accesible to unauthenticated users.\n'
'This has the same effect as passing public=False to get_schema_view() or '
'OpenAPISchemaGenerator.get_schema().\n'
'This option implies --mock-request.'
)
def write_schema(self, schema, stream, format):
if format == 'json':
codec = OpenAPICodecJson(validators=[])
swagger_json = codec.encode(schema)
swagger_json = json.loads(swagger_json.decode('utf-8'), object_pairs_hook=OrderedDict)
pretty_json = json.dumps(swagger_json, indent=4, ensure_ascii=True)
stream.write(pretty_json)
elif format == 'yaml':
codec = OpenAPICodecYaml(validators=[])
swagger_yaml = codec.encode(schema).decode('utf-8')
# YAML is already pretty!
stream.write(swagger_yaml)
else: # pragma: no cover
raise ValueError("unknown format %s" % format)
def get_mock_request(self, url, format, user=None):
factory = APIRequestFactory()
request = factory.get(url + '/swagger.' + format)
if user is not None:
force_authenticate(request, user=user)
request = APIView().initialize_request(request)
return request
def handle(self, output_file, overwrite, format, api_url, mock, user, private, *args, **options):
# disable logs of WARNING and below
logging.disable(logging.WARNING)
info = getattr(swagger_settings, 'DEFAULT_INFO', None)
if not isinstance(info, openapi.Info):
raise ImproperlyConfigured(
'settings.SWAGGER_SETTINGS["DEFAULT_INFO"] should be an '
'import string pointing to an openapi.Info object'
)
if not format:
if os.path.splitext(output_file)[1] in ('.yml', '.yaml'):
format = 'yaml'
format = format or 'json'
api_url = api_url or swagger_settings.DEFAULT_API_URL
user = User.objects.get(username=user) if user else None
mock = mock or private or (user is not None)
if mock and not api_url:
raise ImproperlyConfigured(
'--mock-request requires an API url; either provide '
'the --url argument or set the DEFAULT_API_URL setting'
)
request = self.get_mock_request(api_url, format, user) if mock else None
generator = OpenAPISchemaGenerator(
info=info,
url=api_url
)
schema = generator.get_schema(request=request, public=not private)
if output_file == '-':
self.write_schema(schema, self.stdout, format)
else:
flags = os.O_CREAT | os.O_WRONLY
flags = flags | (os.O_TRUNC if overwrite else os.O_EXCL)
with os.fdopen(os.open(output_file, flags), "w") as stream:
self.write_schema(schema, stream, format)

View File

@ -10,6 +10,7 @@ from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.views import APIView from rest_framework.views import APIView
from drf_yasg.app_settings import swagger_settings
from .generators import OpenAPISchemaGenerator from .generators import OpenAPISchemaGenerator
from .renderers import ( from .renderers import (
SwaggerJSONRenderer, SwaggerYAMLRenderer, SwaggerUIRenderer, ReDocRenderer, OpenAPIRenderer, SwaggerJSONRenderer, SwaggerYAMLRenderer, SwaggerUIRenderer, ReDocRenderer, OpenAPIRenderer,
@ -46,14 +47,14 @@ def deferred_never_cache(view_func):
return _wrapped_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=None, url=None, patterns=None, urlconf=None, public=False, validators=None,
generator_class=OpenAPISchemaGenerator, generator_class=OpenAPISchemaGenerator,
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES, authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES): permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
""" """
Create a SchemaView class with default renderers and generators. Create a SchemaView class with default renderers and generators.
:param .Info info: Required. Swagger API Info object :param .Info info: Swagger API Info object; if omitted, defaults to `DEFAULT_INFO`
:param str url: API base url; if left blank will be deduced from the location the view is served at :param str url: API base url; if left blank will be deduced from the location the view is served at
:param patterns: passed to SchemaGenerator :param patterns: passed to SchemaGenerator
:param urlconf: passed to SchemaGenerator :param urlconf: passed to SchemaGenerator
@ -69,6 +70,7 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, public=False, v
_generator_class = generator_class _generator_class = generator_class
_auth_classes = authentication_classes _auth_classes = authentication_classes
_perm_classes = permission_classes _perm_classes = permission_classes
info = info or swagger_settings.DEFAULT_INFO
validators = validators or [] validators = validators or []
_spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS) _spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS)

View File

@ -107,6 +107,8 @@ SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login', 'LOGIN_URL': '/admin/login',
'LOGOUT_URL': '/admin/logout', 'LOGOUT_URL': '/admin/logout',
'VALIDATOR_URL': 'http://localhost:8189', 'VALIDATOR_URL': 'http://localhost:8189',
'DEFAULT_INFO': 'testproj.urls.swagger_info'
} }
# Internationalization # Internationalization

View File

@ -6,15 +6,16 @@ from rest_framework.decorators import api_view
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
SchemaView = get_schema_view( swagger_info = openapi.Info(
openapi.Info(
title="Snippets API", title="Snippets API",
default_version='v1', default_version='v1',
description="Test description", description="Test description",
terms_of_service="https://www.google.com/policies/terms/", terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@snippets.local"), contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="BSD License"), license=openapi.License(name="BSD License"),
), )
SchemaView = get_schema_view(
validators=['ssv', 'flex'], validators=['ssv', 'flex'],
public=True, public=True,
permission_classes=(permissions.AllowAny,), permission_classes=(permissions.AllowAny,),

View File

@ -4,12 +4,13 @@ import os
from collections import OrderedDict from collections import OrderedDict
import pytest import pytest
from datadiff.tools import assert_equal
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView from rest_framework.views import APIView
from drf_yasg import openapi, codecs from drf_yasg import openapi, codecs
from drf_yasg.codecs import yaml_sane_load from drf_yasg.codecs import yaml_sane_load, yaml_sane_dump
from drf_yasg.generators import OpenAPISchemaGenerator from drf_yasg.generators import OpenAPISchemaGenerator
@ -63,6 +64,22 @@ def validate_schema(db):
return validate_schema return validate_schema
@pytest.fixture
def compare_schemas():
def compare_schemas(schema1, schema2):
schema1 = OrderedDict(schema1)
schema2 = OrderedDict(schema2)
ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions']
for attr in ignore:
schema1.pop(attr, None)
schema2.pop(attr, None)
# print diff between YAML strings because it's prettier
assert_equal(yaml_sane_dump(schema1, binary=False), yaml_sane_dump(schema2, binary=False))
return compare_schemas
@pytest.fixture @pytest.fixture
def swagger_settings(settings): def swagger_settings(settings):
swagger_settings = copy.deepcopy(settings.SWAGGER_SETTINGS) swagger_settings = copy.deepcopy(settings.SWAGGER_SETTINGS)

View File

@ -0,0 +1,91 @@
import json
import os
import random
import string
import tempfile
from collections import OrderedDict
import pytest
from django.contrib.auth.models import User
from django.core.management import call_command
from six import StringIO
from drf_yasg.codecs import yaml_sane_load
def call_generate_swagger(output_file='-', overwrite=False, format='', api_url='',
mock=False, user='', private=False, **kwargs):
out = StringIO()
call_command(
'generate_swagger', stdout=out,
output_file=output_file, overwrite=overwrite, format=format,
api_url=api_url, mock=mock, user=user, private=private,
**kwargs
)
return out.getvalue()
def test_reference_schema(db, reference_schema):
User.objects.create_superuser('admin', 'admin@admin.admin', 'blabla')
output = call_generate_swagger(format='yaml', api_url='http://test.local:8002/', user='admin')
output_schema = yaml_sane_load(output)
assert output_schema == reference_schema
def test_non_public(db):
output = call_generate_swagger(format='yaml', api_url='http://test.local:8002/', private=True)
output_schema = yaml_sane_load(output)
assert len(output_schema['paths']) == 0
def test_no_mock(db):
output = call_generate_swagger()
output_schema = json.loads(output, object_pairs_hook=OrderedDict)
assert len(output_schema['paths']) > 0
def silentremove(filename):
try:
os.remove(filename)
except OSError:
pass
def test_file_output(db):
prefix = os.path.join(tempfile.gettempdir(), tempfile.gettempprefix())
name = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
yaml_file = prefix + name + '.yaml'
json_file = prefix + name + '.json'
other_file = prefix + name + '.txt'
try:
# when called with output file nothing should be written to stdout
assert call_generate_swagger(output_file=yaml_file) == ''
assert call_generate_swagger(output_file=json_file) == ''
assert call_generate_swagger(output_file=other_file) == ''
with pytest.raises(OSError):
# a second call should fail because file exists
call_generate_swagger(output_file=yaml_file)
# a second call with overwrite should still succeed
assert call_generate_swagger(output_file=json_file, overwrite=True) == ''
with open(yaml_file) as f:
content = f.read()
# YAML is a superset of JSON - that means we have to check that
# the file is really YAML and not just JSON parsed by the YAML parser
with pytest.raises(ValueError):
json.loads(content)
output_yaml = yaml_sane_load(content)
with open(json_file) as f:
output_json = json.load(f, object_pairs_hook=OrderedDict)
with open(other_file) as f:
output_other = json.load(f, object_pairs_hook=OrderedDict)
assert output_yaml == output_json == output_other
finally:
silentremove(yaml_file)
silentremove(json_file)
silentremove(other_file)

View File

@ -1,21 +1,8 @@
from collections import OrderedDict
from datadiff.tools import assert_equal
from drf_yasg.codecs import yaml_sane_dump
from drf_yasg.inspectors import FieldInspector, SerializerInspector, PaginatorInspector, FilterInspector from drf_yasg.inspectors import FieldInspector, SerializerInspector, PaginatorInspector, FilterInspector
def test_reference_schema(swagger_dict, reference_schema): def test_reference_schema(swagger_dict, reference_schema, compare_schemas):
swagger_dict = OrderedDict(swagger_dict) compare_schemas(swagger_dict, reference_schema)
reference_schema = OrderedDict(reference_schema)
ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions']
for attr in ignore:
swagger_dict.pop(attr, None)
reference_schema.pop(attr, None)
# print diff between YAML strings because it's prettier
assert_equal(yaml_sane_dump(swagger_dict, binary=False), yaml_sane_dump(reference_schema, binary=False))
class NoOpFieldInspector(FieldInspector): class NoOpFieldInspector(FieldInspector):
@ -34,7 +21,7 @@ class NoOpPaginatorInspector(PaginatorInspector):
pass pass
def test_noop_inspectors(swagger_settings, swagger_dict, reference_schema): def test_noop_inspectors(swagger_settings, swagger_dict, reference_schema, compare_schemas):
from drf_yasg import app_settings from drf_yasg import app_settings
def set_inspectors(inspectors, setting_name): def set_inspectors(inspectors, setting_name):
@ -43,4 +30,4 @@ def test_noop_inspectors(swagger_settings, swagger_dict, reference_schema):
set_inspectors([NoOpFieldInspector, NoOpSerializerInspector], 'DEFAULT_FIELD_INSPECTORS') set_inspectors([NoOpFieldInspector, NoOpSerializerInspector], 'DEFAULT_FIELD_INSPECTORS')
set_inspectors([NoOpFilterInspector], 'DEFAULT_FILTER_INSPECTORS') set_inspectors([NoOpFilterInspector], 'DEFAULT_FILTER_INSPECTORS')
set_inspectors([NoOpPaginatorInspector], 'DEFAULT_PAGINATOR_INSPECTORS') set_inspectors([NoOpPaginatorInspector], 'DEFAULT_PAGINATOR_INSPECTORS')
test_reference_schema(swagger_dict, reference_schema) compare_schemas(swagger_dict, reference_schema)