diff --git a/.coveragerc b/.coveragerc index 996f08c2..30e8e4c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,6 +14,7 @@ exclude_lines = # Don't complain if tests don't hit defensive assertion code: raise AssertionError + raise ImproperlyConfigured raise TypeError raise NotImplementedError warnings.warn diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ec46a5fd..3cd4082b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -27,9 +27,9 @@ Pull requests You want to contribute some code? Great! Here are a few steps to get you started: -#. Fork the repository on GitHub -#. 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 +#. **Fork the repository on GitHub** +#. **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** .. 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 -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 @@ -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) $ cat createsuperuser.py | python manage.py shell (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. - If your change modifies the expected schema output, you should download the new generated ``swagger.yaml``, diff it - against the old reference output in ``tests/reference.yaml``, and replace it after checking that no unexpected - changes appeared. + If your change modifies the expected schema output, you should regenerate the reference schema at + ``tests/reference.yaml``: -#. 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 @@ -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 (venv) $ tox -#. Update documentation +#. **Update documentation** 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 @@ -77,10 +84,11 @@ You want to contribute some code? Great! Here are a few steps to get you started (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 easily. -#. Your code must pass all the required travis jobs before it is merged. As of now, this includes running on - Python 2.7, 3.4, 3.5 and 3.6, and building the docs succesfully. +#. **Your code must pass all the required travis jobs before it is merged** + + As of now, this consists of running on Python 2.7, 3.4, 3.5 and 3.6, and building the docs succesfully. diff --git a/README.rst b/README.rst index 2b01cbb0..643e0e1d 100644 --- a/README.rst +++ b/README.rst @@ -180,62 +180,67 @@ The possible settings and their default values are as follows: .. code:: python - SWAGGER_SETTINGS = { - # default inspector classes, see advanced documentation - 'DEFAULT_AUTO_SCHEMA_CLASS': 'drf_yasg.inspectors.SwaggerAutoSchema', - 'DEFAULT_FIELD_INSPECTORS': [ - 'drf_yasg.inspectors.CamelCaseJSONFilter', - 'drf_yasg.inspectors.ReferencingSerializerInspector', - 'drf_yasg.inspectors.RelatedFieldInspector', - 'drf_yasg.inspectors.ChoiceFieldInspector', - 'drf_yasg.inspectors.FileFieldInspector', - 'drf_yasg.inspectors.DictFieldInspector', - 'drf_yasg.inspectors.SimpleFieldInspector', - 'drf_yasg.inspectors.StringDefaultFieldInspector', - ], - 'DEFAULT_FILTER_INSPECTORS': [ - 'drf_yasg.inspectors.CoreAPICompatInspector', - ], - 'DEFAULT_PAGINATOR_INSPECTORS': [ - 'drf_yasg.inspectors.DjangoRestResponsePagination', - 'drf_yasg.inspectors.CoreAPICompatInspector', - ], - - '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 - 'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button - - # Swagger security definitions to include in the schema; - # see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-definitions-object - 'SECURITY_DEFINITIONS': { - 'basic': { - 'type': 'basic' - } - }, - - # url to an external Swagger validation service; defaults to 'http://online.swagger.io/validator/' - # set to None to disable the schema validation badge in the UI - 'VALIDATOR_URL': '', - - # swagger-ui configuration settings, see https://github.com/swagger-api/swagger-ui/blob/112bca906553a937ac67adc2e500bdeed96d067b/docs/usage/configuration.md#parameters - 'OPERATIONS_SORTER': None, - 'TAGS_SORTER': None, - 'DOC_EXPANSION': 'list', - 'DEEP_LINKING': False, - 'SHOW_EXTENSIONS': True, - 'DEFAULT_MODEL_RENDERING': 'model', - 'DEFAULT_MODEL_DEPTH': 3, - } + SWAGGER_SETTINGS = { + # default inspector classes, see advanced documentation + 'DEFAULT_AUTO_SCHEMA_CLASS': 'drf_yasg.inspectors.SwaggerAutoSchema', + 'DEFAULT_FIELD_INSPECTORS': [ + 'drf_yasg.inspectors.CamelCaseJSONFilter', + 'drf_yasg.inspectors.ReferencingSerializerInspector', + 'drf_yasg.inspectors.RelatedFieldInspector', + 'drf_yasg.inspectors.ChoiceFieldInspector', + 'drf_yasg.inspectors.FileFieldInspector', + 'drf_yasg.inspectors.DictFieldInspector', + 'drf_yasg.inspectors.SimpleFieldInspector', + 'drf_yasg.inspectors.StringDefaultFieldInspector', + ], + 'DEFAULT_FILTER_INSPECTORS': [ + 'drf_yasg.inspectors.CoreAPICompatInspector', + ], + 'DEFAULT_PAGINATOR_INSPECTORS': [ + 'drf_yasg.inspectors.DjangoRestResponsePagination', + '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 + '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 + + # Swagger security definitions to include in the schema; + # see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-definitions-object + 'SECURITY_DEFINITIONS': { + 'basic': { + 'type': 'basic' + } + }, + + # url to an external Swagger validation service; defaults to 'http://online.swagger.io/validator/' + # set to None to disable the schema validation badge in the UI + 'VALIDATOR_URL': '', + + # swagger-ui configuration settings, see https://github.com/swagger-api/swagger-ui/blob/112bca906553a937ac67adc2e500bdeed96d067b/docs/usage/configuration.md#parameters + 'OPERATIONS_SORTER': None, + 'TAGS_SORTER': None, + 'DOC_EXPANSION': 'list', + 'DEEP_LINKING': False, + 'SHOW_EXTENSIONS': True, + 'DEFAULT_MODEL_RENDERING': 'model', + 'DEFAULT_MODEL_DEPTH': 3, + } .. code:: python - REDOC_SETTINGS = { - # ReDoc UI configuration settings, see https://github.com/Rebilly/ReDoc#redoc-tag-attributes - 'LAZY_RENDERING': True, - 'HIDE_HOSTNAME': False, - 'EXPAND_RESPONSES': 'all', - 'PATH_IN_MIDDLE': False, - } + REDOC_SETTINGS = { + # ReDoc UI configuration settings, see https://github.com/Rebilly/ReDoc#redoc-tag-attributes + 'LAZY_RENDERING': True, + 'HIDE_HOSTNAME': False, + 'EXPAND_RESPONSES': 'all', + 'PATH_IN_MIDDLE': False, + } 3. Caching ========== diff --git a/docs/changelog.rst b/docs/changelog.rst index 79bbbc08..c02b793b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,13 @@ Changelog ######### +********* +**1.1.1** +********* + +- **ADDED:** :ref:`generate_swagger management command ` + (:issue:`29`, :pr:`31`, thanks to :ghuser:`beaugunderson`) + ********* **1.1.0** ********* diff --git a/docs/custom_spec.rst b/docs/custom_spec.rst index 1b23b79c..4c3a5b4d 100644 --- a/docs/custom_spec.rst +++ b/docs/custom_spec.rst @@ -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 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'}) 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 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) 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 method of each operation: - .. code:: python + .. code-block:: python class UserList(APIView): @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: - .. code:: python + .. code-block:: python class ArticleViewSet(viewsets.ModelViewSet): # 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 ``swagger_auto_schema`` in combination with Django's ``method_decorator``: - .. code:: python + .. code-block:: python @method_decorator(name='list', decorator=swagger_auto_schema( 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 override an ``@api_view`` as described above: - .. code:: python + .. code-block:: python decorated_login_view = \ 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.: -.. code:: python +.. code-block:: python 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: -.. code:: python +.. code-block:: python 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 implemented like so: -.. code:: python +.. code-block:: python class DjangoFilterDescriptionInspector(CoreAPICompatInspector): 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 :class:`.Schema` objects: -.. code:: python +.. code-block:: python class NoSchemaTitleInspector(FieldInspector): def process_result(self, result, method_name, obj, **kwargs): diff --git a/docs/rendering.rst b/docs/rendering.rst index 9d78bded..13786f02 100644 --- a/docs/rendering.rst +++ b/docs/rendering.rst @@ -2,6 +2,7 @@ Serving the schema ################## + ************************************************ ``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.: -.. code:: python +.. code-block:: python 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 :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 ... + diff --git a/docs/settings.rst b/docs/settings.rst index afd3ebf0..b156daa7 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -15,7 +15,7 @@ Example: **settings.py** -.. code:: python +.. code-block:: python SWAGGER_SETTINGS = { '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| \ ``]`` +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 ============= @@ -124,7 +143,7 @@ See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#sec **Default**: -.. code:: python +.. code-block:: python 'basic': { 'type': 'basic' diff --git a/src/drf_yasg/app_settings.py b/src/drf_yasg/app_settings.py index a6ae8e64..48427650 100644 --- a/src/drf_yasg/app_settings.py +++ b/src/drf_yasg/app_settings.py @@ -22,6 +22,9 @@ 'drf_yasg.inspectors.CoreAPICompatInspector', ], + 'DEFAULT_INFO': None, + 'DEFAULT_API_URL': '', + 'USE_SESSION_AUTH': True, 'SECURITY_DEFINITIONS': { 'basic': { @@ -53,6 +56,7 @@ 'DEFAULT_FIELD_INSPECTORS', 'DEFAULT_FILTER_INSPECTORS', 'DEFAULT_PAGINATOR_INSPECTORS', + 'DEFAULT_INFO', ] diff --git a/src/drf_yasg/generators.py b/src/drf_yasg/generators.py index 0f3d8d79..c981052c 100644 --- a/src/drf_yasg/generators.py +++ b/src/drf_yasg/generators.py @@ -59,12 +59,12 @@ class OpenAPISchemaGenerator(object): """ 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 str version: API version string, takes preedence over the version in `info` - :param str url: API + :param str version: API version string; can be omitted to use `info.default_version` + :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 urlconf: if patterns is not given, use this urlconf to enumerate patterns; if not given, the default urlconf is used diff --git a/src/drf_yasg/management/__init__.py b/src/drf_yasg/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/drf_yasg/management/commands/__init__.py b/src/drf_yasg/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/drf_yasg/management/commands/generate_swagger.py b/src/drf_yasg/management/commands/generate_swagger.py new file mode 100644 index 00000000..5cc290f1 --- /dev/null +++ b/src/drf_yasg/management/commands/generate_swagger.py @@ -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) diff --git a/src/drf_yasg/views.py b/src/drf_yasg/views.py index 2d877d39..99536d80 100644 --- a/src/drf_yasg/views.py +++ b/src/drf_yasg/views.py @@ -10,6 +10,7 @@ from rest_framework.settings import api_settings from rest_framework.views import APIView +from drf_yasg.app_settings import swagger_settings from .generators import OpenAPISchemaGenerator from .renderers import ( SwaggerJSONRenderer, SwaggerYAMLRenderer, SwaggerUIRenderer, ReDocRenderer, OpenAPIRenderer, @@ -46,14 +47,14 @@ def callback(response): 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, authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES, permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES): """ 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 patterns: 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 _auth_classes = authentication_classes _perm_classes = permission_classes + info = info or swagger_settings.DEFAULT_INFO validators = validators or [] _spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS) diff --git a/testproj/testproj/settings.py b/testproj/testproj/settings.py index 64ad1b83..5e643a9e 100644 --- a/testproj/testproj/settings.py +++ b/testproj/testproj/settings.py @@ -107,6 +107,8 @@ 'LOGIN_URL': '/admin/login', 'LOGOUT_URL': '/admin/logout', 'VALIDATOR_URL': 'http://localhost:8189', + + 'DEFAULT_INFO': 'testproj.urls.swagger_info' } # Internationalization diff --git a/testproj/testproj/urls.py b/testproj/testproj/urls.py index efff18ff..ab95e122 100644 --- a/testproj/testproj/urls.py +++ b/testproj/testproj/urls.py @@ -6,15 +6,16 @@ from drf_yasg import openapi from drf_yasg.views import get_schema_view +swagger_info = openapi.Info( + title="Snippets API", + default_version='v1', + description="Test description", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), +) + SchemaView = get_schema_view( - openapi.Info( - title="Snippets API", - default_version='v1', - description="Test description", - terms_of_service="https://www.google.com/policies/terms/", - contact=openapi.Contact(email="contact@snippets.local"), - license=openapi.License(name="BSD License"), - ), validators=['ssv', 'flex'], public=True, permission_classes=(permissions.AllowAny,), diff --git a/tests/conftest.py b/tests/conftest.py index a7449a1c..6569fa7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,12 +4,13 @@ from collections import OrderedDict import pytest +from datadiff.tools import assert_equal from django.contrib.auth.models import User from rest_framework.test import APIRequestFactory from rest_framework.views import APIView 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 @@ -63,6 +64,22 @@ def validate_schema(swagger): 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 def swagger_settings(settings): swagger_settings = copy.deepcopy(settings.SWAGGER_SETTINGS) diff --git a/tests/test_management.py b/tests/test_management.py new file mode 100644 index 00000000..4e0cda6e --- /dev/null +++ b/tests/test_management.py @@ -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) diff --git a/tests/test_reference_schema.py b/tests/test_reference_schema.py index cf04cd10..d423f4ac 100644 --- a/tests/test_reference_schema.py +++ b/tests/test_reference_schema.py @@ -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 -def test_reference_schema(swagger_dict, reference_schema): - swagger_dict = OrderedDict(swagger_dict) - 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)) +def test_reference_schema(swagger_dict, reference_schema, compare_schemas): + compare_schemas(swagger_dict, reference_schema) class NoOpFieldInspector(FieldInspector): @@ -34,7 +21,7 @@ class NoOpPaginatorInspector(PaginatorInspector): 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 def set_inspectors(inspectors, setting_name): @@ -43,4 +30,4 @@ def set_inspectors(inspectors, setting_name): set_inspectors([NoOpFieldInspector, NoOpSerializerInspector], 'DEFAULT_FIELD_INSPECTORS') set_inspectors([NoOpFilterInspector], 'DEFAULT_FILTER_INSPECTORS') set_inspectors([NoOpPaginatorInspector], 'DEFAULT_PAGINATOR_INSPECTORS') - test_reference_schema(swagger_dict, reference_schema) + compare_schemas(swagger_dict, reference_schema)