diff --git a/src/drf_yasg/generators.py b/src/drf_yasg/generators.py index 522d27d..3f1979f 100644 --- a/src/drf_yasg/generators.py +++ b/src/drf_yasg/generators.py @@ -1,15 +1,13 @@ import re from collections import defaultdict, OrderedDict -import django.db.models import uritemplate -from coreapi.compat import force_text from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator -from rest_framework.schemas.inspectors import get_pk_description from . import openapi from .inspectors import SwaggerAutoSchema from .openapi import ReferenceResolver +from .utils import inspect_model_field, get_model_field PATH_PARAMETER_RE = re.compile(r'{(?P\w+)}') @@ -82,9 +80,9 @@ class OpenAPISchemaGenerator(object): :return: the generated Swagger specification :rtype: openapi.Swagger """ - endpoints = self.get_endpoints(None if public else request) + endpoints = self.get_endpoints(request) components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS) - paths = self.get_paths(endpoints, components) + paths = self.get_paths(endpoints, components, public) url = self._gen.url if not url and request is not None: @@ -114,9 +112,9 @@ class OpenAPISchemaGenerator(object): return view def get_endpoints(self, request=None): - """Iterate over all the registered endpoints in the API. + """Iterate over all the registered endpoints in the API and return a fake view with the right parameters. - :param rest_framework.request.Request request: used for returning only endpoints available to the given request + :param rest_framework.request.Request request: request to bind to the endpoint views :return: {path: (view_class, list[(http_method, view_instance)]) :rtype: dict """ @@ -151,11 +149,12 @@ class OpenAPISchemaGenerator(object): """ return self._gen.get_keys(subpath, method, view) - def get_paths(self, endpoints, components): + def get_paths(self, endpoints, components, public): """Generate the Swagger Paths for the API from the given endpoints. :param dict endpoints: endpoints as returned by get_endpoints :param ReferenceResolver components: resolver/container for Swagger References + :param bool public: if True, all endpoints are included regardless of access through `request` :rtype: openapi.Paths """ if not endpoints: @@ -169,7 +168,7 @@ class OpenAPISchemaGenerator(object): 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): + if not public and not self._gen.has_view_permissions(path, method, view): continue operation_keys = self.get_operation_keys(path[len(prefix):], method, view) @@ -209,36 +208,20 @@ class OpenAPISchemaGenerator(object): :rtype: list[openapi.Parameter] """ parameters = [] + queryset = getattr(view_cls, 'queryset', None) 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: # pragma: no cover - 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 + model, model_field = get_model_field(queryset, variable) + attrs = inspect_model_field(model, model_field) + if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable: + attrs['pattern'] = view_cls.lookup_value_regex field = openapi.Parameter( name=variable, required=True, in_=openapi.IN_PATH, - type=type, - pattern=pattern, - description=description, + **attrs ) parameters.append(field) diff --git a/src/drf_yasg/openapi.py b/src/drf_yasg/openapi.py index 7e6e34b..8b3d3b0 100644 --- a/src/drf_yasg/openapi.py +++ b/src/drf_yasg/openapi.py @@ -309,6 +309,7 @@ class Items(SwaggerDict): :param .Items items: only valid if `type` is ``array`` """ super(Items, self).__init__(**extra) + assert type is not None, "type is required!" self.type = type self.format = format self.enum = enum @@ -372,6 +373,7 @@ class Schema(SwaggerDict): # common error raise AssertionError( "the `requires` attribute of schema must be an array of required properties, not a boolean!") + assert type is not None, "type is required!" self.description = description self.required = required self.type = type diff --git a/src/drf_yasg/templates/drf-yasg/swagger-ui.html b/src/drf_yasg/templates/drf-yasg/swagger-ui.html index 12ac96a..0a2ad2a 100644 --- a/src/drf_yasg/templates/drf-yasg/swagger-ui.html +++ b/src/drf_yasg/templates/drf-yasg/swagger-ui.html @@ -163,7 +163,14 @@ plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], - layout: "StandaloneLayout" + layout: "StandaloneLayout", + filter: true, + requestInterceptor: function(request) { + console.log(request); + var headers = request.headers || {}; + headers["X-CSRFToken"] = document.querySelector("[name=csrfmiddlewaretoken]").value; + return request; + } }; var swaggerSettings = {}; diff --git a/src/drf_yasg/utils.py b/src/drf_yasg/utils.py index 3a74ae8..01a2642 100644 --- a/src/drf_yasg/utils.py +++ b/src/drf_yasg/utils.py @@ -1,14 +1,20 @@ +import logging from collections import OrderedDict from django.core.validators import RegexValidator +from django.db import models from django.utils.encoding import force_text from rest_framework import serializers from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin +from rest_framework.schemas.inspectors import get_pk_description from rest_framework.settings import api_settings +from rest_framework.utils import json, encoders from . import openapi from .errors import SwaggerGenerationError +logger = logging.getLogger(__name__) + #: used to forcibly remove the body of a request via :func:`.swagger_auto_schema` no_body = object() @@ -157,6 +163,87 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod return decorator +def get_model_field(queryset, field_name): + """Try to get information about a model and model field from a queryset. + + :param queryset: the queryset + :param field_name: the target field name + :returns: the model and target field from the queryset as a 2-tuple; both elements can be ``None`` + :rtype: tuple + """ + model = getattr(queryset, 'model', None) + try: + model_field = model._meta.get_field(field_name) + except Exception: # pragma: no cover + model_field = None + + return model, model_field + + +model_field_to_swagger_type = [ + (models.AutoField, (openapi.TYPE_INTEGER, None)), + (models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)), + (models.BooleanField, (openapi.TYPE_BOOLEAN, None)), + (models.NullBooleanField, (openapi.TYPE_BOOLEAN, None)), + (models.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)), + (models.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)), + (models.DecimalField, (openapi.TYPE_NUMBER, None)), + (models.DurationField, (openapi.TYPE_INTEGER, None)), + (models.FloatField, (openapi.TYPE_NUMBER, None)), + (models.IntegerField, (openapi.TYPE_INTEGER, None)), + (models.IPAddressField, (openapi.TYPE_STRING, openapi.FORMAT_IPV4)), + (models.GenericIPAddressField, (openapi.TYPE_STRING, openapi.FORMAT_IPV6)), + (models.SlugField, (openapi.TYPE_STRING, openapi.FORMAT_SLUG)), + (models.TextField, (openapi.TYPE_STRING, None)), + (models.TimeField, (openapi.TYPE_STRING, None)), + (models.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)), + (models.CharField, (openapi.TYPE_STRING, None)), +] + + +def inspect_model_field(model, model_field): + """Extract information from a django model field instance. + + :param model: the django model + :param model_field: a field on the model + :return: description, type, format and pattern extracted from the model field + :rtype: OrderedDict + """ + if model is not None and model_field is not None: + for model_field_class, tf in model_field_to_swagger_type: + if isinstance(model_field, model_field_class): + swagger_type, format = tf + break + else: + swagger_type, format = None, None + + if format is None or format == openapi.FORMAT_SLUG: + pattern = find_regex(model_field) + else: + pattern = None + + if model_field.help_text: + description = force_text(model_field.help_text) + elif model_field.primary_key: + description = get_pk_description(model, model_field) + else: + description = None + else: + description = None + swagger_type = None + format = None + pattern = None + + result = OrderedDict([ + ('description', description), + ('type', swagger_type or openapi.TYPE_STRING), + ('format', format), + ('pattern', pattern) + ]) + # TODO: filter none + return result + + def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **kwargs): """Convert a drf Serializer or Field instance into a Swagger object. @@ -176,17 +263,50 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=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 - def SwaggerType(**instance_kwargs): + def SwaggerType(existing_object=None, **instance_kwargs): if swagger_object_type == openapi.Parameter and 'required' not in instance_kwargs: instance_kwargs['required'] = field.required if swagger_object_type != openapi.Items and 'default' not in instance_kwargs: default = getattr(field, 'default', serializers.empty) if default is not serializers.empty: - instance_kwargs['default'] = default + if callable(default): + try: + if hasattr(default, 'set_context'): + default.set_context(field) + default = default() + except Exception as e: + logger.warning("default for %s is callable but it raised an exception when " + "called; 'default' field will not be added to schema", field, exc_info=True) + default = None + + if default is not None: + try: + default = field.to_representation(default) + # JSON roundtrip ensures that the value is valid JSON; + # for example, sets get transformed into lists + default = json.loads(json.dumps(default, cls=encoders.JSONEncoder)) + except Exception: + logger.warning("'default' on schema for %s will not be set because " + "to_representation raised an exception", field, exc_info=True) + default = None + + if default is not None: + instance_kwargs['default'] = default + if swagger_object_type == openapi.Schema and 'read_only' not in instance_kwargs: if field.read_only: instance_kwargs['read_only'] = True instance_kwargs.update(kwargs) + instance_kwargs.pop('title', None) + instance_kwargs.pop('description', None) + + if existing_object is not None: + existing_object.title = title + existing_object.description = description + for attr, val in instance_kwargs.items(): + setattr(existing_object, attr, val) + return existing_object + return swagger_object_type(title=title, description=description, **instance_kwargs) # arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements @@ -238,8 +358,29 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** items=child_schema, unique_items=True, # is this OK? ) + elif isinstance(field, serializers.PrimaryKeyRelatedField): + if field.pk_field: + result = serializer_field_to_swagger(field.pk_field, swagger_object_type, definitions, **kwargs) + return SwaggerType(existing_object=result) + + attrs = {'type': openapi.TYPE_STRING} + try: + model = field.queryset.model + pk_field = model._meta.pk + except Exception: + logger.warning("an exception was raised when attempting to extract the primary key related to %s; " + "falling back to plain string" % field, exc_info=True) + else: + attrs.update(inspect_model_field(model, pk_field)) + + return SwaggerType(**attrs) + elif isinstance(field, serializers.HyperlinkedRelatedField): + return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI) + elif isinstance(field, serializers.SlugRelatedField): + model, model_field = get_model_field(field.queryset, field.slug_field) + attrs = inspect_model_field(model, model_field) + return SwaggerType(**attrs) elif isinstance(field, serializers.RelatedField): - # TODO: infer type for PrimaryKeyRelatedField? return SwaggerType(type=openapi.TYPE_STRING) # ------ CHOICES elif isinstance(field, serializers.MultipleChoiceField): @@ -253,7 +394,7 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** elif isinstance(field, serializers.ChoiceField): return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys())) # ------ BOOL - elif isinstance(field, serializers.BooleanField): + elif isinstance(field, (serializers.BooleanField, serializers.NullBooleanField)): return SwaggerType(type=openapi.TYPE_BOOLEAN) # ------ NUMERIC elif isinstance(field, (serializers.DecimalField, serializers.FloatField)): @@ -262,6 +403,8 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** elif isinstance(field, serializers.IntegerField): # TODO: min_value max_value return SwaggerType(type=openapi.TYPE_INTEGER) + elif isinstance(field, serializers.DurationField): + return SwaggerType(type=openapi.TYPE_INTEGER) # ------ STRING elif isinstance(field, serializers.EmailField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL) @@ -308,8 +451,10 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** type=openapi.TYPE_OBJECT, additional_properties=child_schema ) + elif isinstance(field, serializers.ModelField): + return SwaggerType(type=openapi.TYPE_STRING) - # TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField? JSONField + # TODO unhandled fields: TimeField HiddenField JSONField # everything else gets string by default return SwaggerType(type=openapi.TYPE_STRING) diff --git a/testproj/articles/migrations/0001_initial.py b/testproj/articles/migrations/0001_initial.py index 75452fd..6d82083 100644 --- a/testproj/articles/migrations/0001_initial.py +++ b/testproj/articles/migrations/0001_initial.py @@ -1,6 +1,8 @@ -# Generated by Django 2.0 on 2017-12-05 04:05 +# Generated by Django 2.0 on 2017-12-23 09:07 +from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -8,6 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -15,12 +18,13 @@ class Migration(migrations.Migration): 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)), + ('title', models.CharField(help_text='title model help_text', max_length=255, unique=True)), + ('body', models.TextField(help_text='article model help_text', max_length=5000)), + ('slug', models.SlugField(blank=True, help_text='slug model help_text', 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/')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)), ], ), ] diff --git a/testproj/articles/models.py b/testproj/articles/models.py index 3031616..24878b8 100644 --- a/testproj/articles/models.py +++ b/testproj/articles/models.py @@ -7,5 +7,6 @@ class Article(models.Model): 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) + author = models.ForeignKey('auth.User', related_name='articles', on_delete=models.CASCADE) cover = models.ImageField(upload_to='article/original/', blank=True) diff --git a/testproj/articles/serializers.py b/testproj/articles/serializers.py index a01ad8c..ee90e2c 100644 --- a/testproj/articles/serializers.py +++ b/testproj/articles/serializers.py @@ -7,17 +7,26 @@ 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"), + read_only=True, ) - uuid = serializers.UUIDField(help_text="should articles have UUIDs?") + uuid = serializers.UUIDField(help_text="should articles have UUIDs?", read_only=True) cover_name = serializers.FileField(use_url=False, source='cover', read_only=True) class Meta: model = Article - fields = ('title', 'body', 'slug', 'date_created', 'date_modified', + fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified', 'references', 'uuid', 'cover', 'cover_name') - read_only_fields = ('date_created', 'date_modified') + read_only_fields = ('date_created', 'date_modified', + 'references', 'uuid', 'cover_name') lookup_field = 'slug' - extra_kwargs = {'body': {'help_text': 'body serializer help_text'}} + extra_kwargs = { + 'body': {'help_text': 'body serializer help_text'}, + 'author': { + 'default': serializers.CurrentUserDefault(), + 'help_text': "The ID of the user that created this article; if none is provided, " + "defaults to the currently logged in user." + }, + } class ImageUploadSerializer(serializers.Serializer): diff --git a/testproj/articles/views.py b/testproj/articles/views.py index 3a15d9d..860e681 100644 --- a/testproj/articles/views.py +++ b/testproj/articles/views.py @@ -20,6 +20,11 @@ class NoPagingAutoSchema(SwaggerAutoSchema): return False +class ArticlePagination(LimitOffsetPagination): + default_limit = 5 + max_limit = 25 + + @method_decorator(name='list', decorator=swagger_auto_schema( operation_description="description from swagger_auto_schema via method_decorator" )) @@ -41,12 +46,11 @@ class ArticleViewSet(viewsets.ModelViewSet): lookup_value_regex = r'[a-z0-9]+(?:-[a-z0-9]+)' serializer_class = serializers.ArticleSerializer - pagination_class = LimitOffsetPagination - max_page_size = 5 + pagination_class = ArticlePagination filter_backends = (DjangoFilterBackend, OrderingFilter) filter_fields = ('title',) - ordering_fields = ('date_modified',) - ordering = ('username',) + ordering_fields = ('date_modified', 'date_created') + ordering = ('date_created',) @swagger_auto_schema(auto_schema=NoPagingAutoSchema) @list_route(methods=['get']) diff --git a/testproj/db.sqlite3 b/testproj/db.sqlite3 index 40d2009..532719e 100644 Binary files a/testproj/db.sqlite3 and b/testproj/db.sqlite3 differ diff --git a/testproj/snippets/migrations/0001_initial.py b/testproj/snippets/migrations/0001_initial.py index ad6451d..57ae038 100644 --- a/testproj/snippets/migrations/0001_initial.py +++ b/testproj/snippets/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.7 on 2017-11-29 21:28 -from __future__ import unicode_literals +# Generated by Django 2.0 on 2017-12-23 09:07 +from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -10,6 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -19,10 +20,11 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True)), ('title', models.CharField(blank=True, default='', max_length=100)), - ('code', models.TextField()), + ('code', models.TextField(help_text='code model help text')), ('linenos', models.BooleanField(default=False)), ('language', models.CharField(choices=[('abap', 'ABAP'), ('abnf', 'ABNF'), ('ada', 'Ada'), ('adl', 'ADL'), ('agda', 'Agda'), ('aheui', 'Aheui'), ('ahk', 'autohotkey'), ('alloy', 'Alloy'), ('ampl', 'Ampl'), ('antlr', 'ANTLR'), ('antlr-as', 'ANTLR With ActionScript Target'), ('antlr-cpp', 'ANTLR With CPP Target'), ('antlr-csharp', 'ANTLR With C# Target'), ('antlr-java', 'ANTLR With Java Target'), ('antlr-objc', 'ANTLR With ObjectiveC Target'), ('antlr-perl', 'ANTLR With Perl Target'), ('antlr-python', 'ANTLR With Python Target'), ('antlr-ruby', 'ANTLR With Ruby Target'), ('apacheconf', 'ApacheConf'), ('apl', 'APL'), ('applescript', 'AppleScript'), ('arduino', 'Arduino'), ('as', 'ActionScript'), ('as3', 'ActionScript 3'), ('aspectj', 'AspectJ'), ('aspx-cs', 'aspx-cs'), ('aspx-vb', 'aspx-vb'), ('asy', 'Asymptote'), ('at', 'AmbientTalk'), ('autoit', 'AutoIt'), ('awk', 'Awk'), ('basemake', 'Base Makefile'), ('bash', 'Bash'), ('bat', 'Batchfile'), ('bbcode', 'BBCode'), ('bc', 'BC'), ('befunge', 'Befunge'), ('bib', 'BibTeX'), ('blitzbasic', 'BlitzBasic'), ('blitzmax', 'BlitzMax'), ('bnf', 'BNF'), ('boo', 'Boo'), ('boogie', 'Boogie'), ('brainfuck', 'Brainfuck'), ('bro', 'Bro'), ('bst', 'BST'), ('bugs', 'BUGS'), ('c', 'C'), ('c-objdump', 'c-objdump'), ('ca65', 'ca65 assembler'), ('cadl', 'cADL'), ('camkes', 'CAmkES'), ('capdl', 'CapDL'), ('capnp', "Cap'n Proto"), ('cbmbas', 'CBM BASIC V2'), ('ceylon', 'Ceylon'), ('cfc', 'Coldfusion CFC'), ('cfengine3', 'CFEngine3'), ('cfm', 'Coldfusion HTML'), ('cfs', 'cfstatement'), ('chai', 'ChaiScript'), ('chapel', 'Chapel'), ('cheetah', 'Cheetah'), ('cirru', 'Cirru'), ('clay', 'Clay'), ('clean', 'Clean'), ('clojure', 'Clojure'), ('clojurescript', 'ClojureScript'), ('cmake', 'CMake'), ('cobol', 'COBOL'), ('cobolfree', 'COBOLFree'), ('coffee-script', 'CoffeeScript'), ('common-lisp', 'Common Lisp'), ('componentpascal', 'Component Pascal'), ('console', 'Bash Session'), ('control', 'Debian Control file'), ('coq', 'Coq'), ('cpp', 'C++'), ('cpp-objdump', 'cpp-objdump'), ('cpsa', 'CPSA'), ('cr', 'Crystal'), ('crmsh', 'Crmsh'), ('croc', 'Croc'), ('cryptol', 'Cryptol'), ('csharp', 'C#'), ('csound', 'Csound Orchestra'), ('csound-document', 'Csound Document'), ('csound-score', 'Csound Score'), ('css', 'CSS'), ('css+django', 'CSS+Django/Jinja'), ('css+erb', 'CSS+Ruby'), ('css+genshitext', 'CSS+Genshi Text'), ('css+lasso', 'CSS+Lasso'), ('css+mako', 'CSS+Mako'), ('css+mozpreproc', 'CSS+mozpreproc'), ('css+myghty', 'CSS+Myghty'), ('css+php', 'CSS+PHP'), ('css+smarty', 'CSS+Smarty'), ('cucumber', 'Gherkin'), ('cuda', 'CUDA'), ('cypher', 'Cypher'), ('cython', 'Cython'), ('d', 'D'), ('d-objdump', 'd-objdump'), ('dart', 'Dart'), ('delphi', 'Delphi'), ('dg', 'dg'), ('diff', 'Diff'), ('django', 'Django/Jinja'), ('docker', 'Docker'), ('doscon', 'MSDOS Session'), ('dpatch', 'Darcs Patch'), ('dtd', 'DTD'), ('duel', 'Duel'), ('dylan', 'Dylan'), ('dylan-console', 'Dylan session'), ('dylan-lid', 'DylanLID'), ('earl-grey', 'Earl Grey'), ('easytrieve', 'Easytrieve'), ('ebnf', 'EBNF'), ('ec', 'eC'), ('ecl', 'ECL'), ('eiffel', 'Eiffel'), ('elixir', 'Elixir'), ('elm', 'Elm'), ('emacs', 'EmacsLisp'), ('erb', 'ERB'), ('erl', 'Erlang erl session'), ('erlang', 'Erlang'), ('evoque', 'Evoque'), ('extempore', 'xtlang'), ('ezhil', 'Ezhil'), ('factor', 'Factor'), ('fan', 'Fantom'), ('fancy', 'Fancy'), ('felix', 'Felix'), ('fish', 'Fish'), ('flatline', 'Flatline'), ('forth', 'Forth'), ('fortran', 'Fortran'), ('fortranfixed', 'FortranFixed'), ('foxpro', 'FoxPro'), ('fsharp', 'FSharp'), ('gap', 'GAP'), ('gas', 'GAS'), ('genshi', 'Genshi'), ('genshitext', 'Genshi Text'), ('glsl', 'GLSL'), ('gnuplot', 'Gnuplot'), ('go', 'Go'), ('golo', 'Golo'), ('gooddata-cl', 'GoodData-CL'), ('gosu', 'Gosu'), ('groff', 'Groff'), ('groovy', 'Groovy'), ('gst', 'Gosu Template'), ('haml', 'Haml'), ('handlebars', 'Handlebars'), ('haskell', 'Haskell'), ('haxeml', 'Hxml'), ('hexdump', 'Hexdump'), ('hsail', 'HSAIL'), ('html', 'HTML'), ('html+cheetah', 'HTML+Cheetah'), ('html+django', 'HTML+Django/Jinja'), ('html+evoque', 'HTML+Evoque'), ('html+genshi', 'HTML+Genshi'), ('html+handlebars', 'HTML+Handlebars'), ('html+lasso', 'HTML+Lasso'), ('html+mako', 'HTML+Mako'), ('html+myghty', 'HTML+Myghty'), ('html+ng2', 'HTML + Angular2'), ('html+php', 'HTML+PHP'), ('html+smarty', 'HTML+Smarty'), ('html+twig', 'HTML+Twig'), ('html+velocity', 'HTML+Velocity'), ('http', 'HTTP'), ('hx', 'Haxe'), ('hybris', 'Hybris'), ('hylang', 'Hy'), ('i6t', 'Inform 6 template'), ('idl', 'IDL'), ('idris', 'Idris'), ('iex', 'Elixir iex session'), ('igor', 'Igor'), ('inform6', 'Inform 6'), ('inform7', 'Inform 7'), ('ini', 'INI'), ('io', 'Io'), ('ioke', 'Ioke'), ('irc', 'IRC logs'), ('isabelle', 'Isabelle'), ('j', 'J'), ('jags', 'JAGS'), ('jasmin', 'Jasmin'), ('java', 'Java'), ('javascript+mozpreproc', 'Javascript+mozpreproc'), ('jcl', 'JCL'), ('jlcon', 'Julia console'), ('js', 'JavaScript'), ('js+cheetah', 'JavaScript+Cheetah'), ('js+django', 'JavaScript+Django/Jinja'), ('js+erb', 'JavaScript+Ruby'), ('js+genshitext', 'JavaScript+Genshi Text'), ('js+lasso', 'JavaScript+Lasso'), ('js+mako', 'JavaScript+Mako'), ('js+myghty', 'JavaScript+Myghty'), ('js+php', 'JavaScript+PHP'), ('js+smarty', 'JavaScript+Smarty'), ('jsgf', 'JSGF'), ('json', 'JSON'), ('json-object', 'JSONBareObject'), ('jsonld', 'JSON-LD'), ('jsp', 'Java Server Page'), ('julia', 'Julia'), ('juttle', 'Juttle'), ('kal', 'Kal'), ('kconfig', 'Kconfig'), ('koka', 'Koka'), ('kotlin', 'Kotlin'), ('lagda', 'Literate Agda'), ('lasso', 'Lasso'), ('lcry', 'Literate Cryptol'), ('lean', 'Lean'), ('less', 'LessCss'), ('lhs', 'Literate Haskell'), ('lidr', 'Literate Idris'), ('lighty', 'Lighttpd configuration file'), ('limbo', 'Limbo'), ('liquid', 'liquid'), ('live-script', 'LiveScript'), ('llvm', 'LLVM'), ('logos', 'Logos'), ('logtalk', 'Logtalk'), ('lsl', 'LSL'), ('lua', 'Lua'), ('make', 'Makefile'), ('mako', 'Mako'), ('maql', 'MAQL'), ('mask', 'Mask'), ('mason', 'Mason'), ('mathematica', 'Mathematica'), ('matlab', 'Matlab'), ('matlabsession', 'Matlab session'), ('md', 'markdown'), ('minid', 'MiniD'), ('modelica', 'Modelica'), ('modula2', 'Modula-2'), ('monkey', 'Monkey'), ('monte', 'Monte'), ('moocode', 'MOOCode'), ('moon', 'MoonScript'), ('mozhashpreproc', 'mozhashpreproc'), ('mozpercentpreproc', 'mozpercentpreproc'), ('mql', 'MQL'), ('mscgen', 'Mscgen'), ('mupad', 'MuPAD'), ('mxml', 'MXML'), ('myghty', 'Myghty'), ('mysql', 'MySQL'), ('nasm', 'NASM'), ('ncl', 'NCL'), ('nemerle', 'Nemerle'), ('nesc', 'nesC'), ('newlisp', 'NewLisp'), ('newspeak', 'Newspeak'), ('ng2', 'Angular2'), ('nginx', 'Nginx configuration file'), ('nim', 'Nimrod'), ('nit', 'Nit'), ('nixos', 'Nix'), ('nsis', 'NSIS'), ('numpy', 'NumPy'), ('nusmv', 'NuSMV'), ('objdump', 'objdump'), ('objdump-nasm', 'objdump-nasm'), ('objective-c', 'Objective-C'), ('objective-c++', 'Objective-C++'), ('objective-j', 'Objective-J'), ('ocaml', 'OCaml'), ('octave', 'Octave'), ('odin', 'ODIN'), ('ooc', 'Ooc'), ('opa', 'Opa'), ('openedge', 'OpenEdge ABL'), ('pacmanconf', 'PacmanConf'), ('pan', 'Pan'), ('parasail', 'ParaSail'), ('pawn', 'Pawn'), ('perl', 'Perl'), ('perl6', 'Perl6'), ('php', 'PHP'), ('pig', 'Pig'), ('pike', 'Pike'), ('pkgconfig', 'PkgConfig'), ('plpgsql', 'PL/pgSQL'), ('postgresql', 'PostgreSQL SQL dialect'), ('postscript', 'PostScript'), ('pot', 'Gettext Catalog'), ('pov', 'POVRay'), ('powershell', 'PowerShell'), ('praat', 'Praat'), ('prolog', 'Prolog'), ('properties', 'Properties'), ('protobuf', 'Protocol Buffer'), ('ps1con', 'PowerShell Session'), ('psql', 'PostgreSQL console (psql)'), ('pug', 'Pug'), ('puppet', 'Puppet'), ('py3tb', 'Python 3.0 Traceback'), ('pycon', 'Python console session'), ('pypylog', 'PyPy Log'), ('pytb', 'Python Traceback'), ('python', 'Python'), ('python3', 'Python 3'), ('qbasic', 'QBasic'), ('qml', 'QML'), ('qvto', 'QVTO'), ('racket', 'Racket'), ('ragel', 'Ragel'), ('ragel-c', 'Ragel in C Host'), ('ragel-cpp', 'Ragel in CPP Host'), ('ragel-d', 'Ragel in D Host'), ('ragel-em', 'Embedded Ragel'), ('ragel-java', 'Ragel in Java Host'), ('ragel-objc', 'Ragel in Objective C Host'), ('ragel-ruby', 'Ragel in Ruby Host'), ('raw', 'Raw token data'), ('rb', 'Ruby'), ('rbcon', 'Ruby irb session'), ('rconsole', 'RConsole'), ('rd', 'Rd'), ('rebol', 'REBOL'), ('red', 'Red'), ('redcode', 'Redcode'), ('registry', 'reg'), ('resource', 'ResourceBundle'), ('rexx', 'Rexx'), ('rhtml', 'RHTML'), ('rnc', 'Relax-NG Compact'), ('roboconf-graph', 'Roboconf Graph'), ('roboconf-instances', 'Roboconf Instances'), ('robotframework', 'RobotFramework'), ('rql', 'RQL'), ('rsl', 'RSL'), ('rst', 'reStructuredText'), ('rts', 'TrafficScript'), ('rust', 'Rust'), ('sas', 'SAS'), ('sass', 'Sass'), ('sc', 'SuperCollider'), ('scala', 'Scala'), ('scaml', 'Scaml'), ('scheme', 'Scheme'), ('scilab', 'Scilab'), ('scss', 'SCSS'), ('shen', 'Shen'), ('silver', 'Silver'), ('slim', 'Slim'), ('smali', 'Smali'), ('smalltalk', 'Smalltalk'), ('smarty', 'Smarty'), ('sml', 'Standard ML'), ('snobol', 'Snobol'), ('snowball', 'Snowball'), ('sourceslist', 'Debian Sourcelist'), ('sp', 'SourcePawn'), ('sparql', 'SPARQL'), ('spec', 'RPMSpec'), ('splus', 'S'), ('sql', 'SQL'), ('sqlite3', 'sqlite3con'), ('squidconf', 'SquidConf'), ('ssp', 'Scalate Server Page'), ('stan', 'Stan'), ('stata', 'Stata'), ('swift', 'Swift'), ('swig', 'SWIG'), ('systemverilog', 'systemverilog'), ('tads3', 'TADS 3'), ('tap', 'TAP'), ('tasm', 'TASM'), ('tcl', 'Tcl'), ('tcsh', 'Tcsh'), ('tcshcon', 'Tcsh Session'), ('tea', 'Tea'), ('termcap', 'Termcap'), ('terminfo', 'Terminfo'), ('terraform', 'Terraform'), ('tex', 'TeX'), ('text', 'Text only'), ('thrift', 'Thrift'), ('todotxt', 'Todotxt'), ('trac-wiki', 'MoinMoin/Trac Wiki markup'), ('treetop', 'Treetop'), ('ts', 'TypeScript'), ('tsql', 'Transact-SQL'), ('turtle', 'Turtle'), ('twig', 'Twig'), ('typoscript', 'TypoScript'), ('typoscriptcssdata', 'TypoScriptCssData'), ('typoscripthtmldata', 'TypoScriptHtmlData'), ('urbiscript', 'UrbiScript'), ('vala', 'Vala'), ('vb.net', 'VB.net'), ('vcl', 'VCL'), ('vclsnippets', 'VCLSnippets'), ('vctreestatus', 'VCTreeStatus'), ('velocity', 'Velocity'), ('verilog', 'verilog'), ('vgl', 'VGL'), ('vhdl', 'vhdl'), ('vim', 'VimL'), ('wdiff', 'WDiff'), ('whiley', 'Whiley'), ('x10', 'X10'), ('xml', 'XML'), ('xml+cheetah', 'XML+Cheetah'), ('xml+django', 'XML+Django/Jinja'), ('xml+erb', 'XML+Ruby'), ('xml+evoque', 'XML+Evoque'), ('xml+lasso', 'XML+Lasso'), ('xml+mako', 'XML+Mako'), ('xml+myghty', 'XML+Myghty'), ('xml+php', 'XML+PHP'), ('xml+smarty', 'XML+Smarty'), ('xml+velocity', 'XML+Velocity'), ('xquery', 'XQuery'), ('xslt', 'XSLT'), ('xtend', 'Xtend'), ('xul+mozpreproc', 'XUL+mozpreproc'), ('yaml', 'YAML'), ('yaml+jinja', 'YAML+Jinja'), ('zephir', 'Zephir')], default='python', max_length=100)), ('style', models.CharField(choices=[('abap', 'abap'), ('algol', 'algol'), ('algol_nu', 'algol_nu'), ('arduino', 'arduino'), ('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful'), ('default', 'default'), ('emacs', 'emacs'), ('friendly', 'friendly'), ('fruity', 'fruity'), ('igor', 'igor'), ('lovelace', 'lovelace'), ('manni', 'manni'), ('monokai', 'monokai'), ('murphy', 'murphy'), ('native', 'native'), ('paraiso-dark', 'paraiso-dark'), ('paraiso-light', 'paraiso-light'), ('pastie', 'pastie'), ('perldoc', 'perldoc'), ('rainbow_dash', 'rainbow_dash'), ('rrt', 'rrt'), ('tango', 'tango'), ('trac', 'trac'), ('vim', 'vim'), ('vs', 'vs'), ('xcode', 'xcode')], default='friendly', max_length=100)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snippets', to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ('created',), diff --git a/testproj/snippets/migrations/0002_auto_20171205_0505.py b/testproj/snippets/migrations/0002_auto_20171205_0505.py deleted file mode 100644 index 59b29c8..0000000 --- a/testproj/snippets/migrations/0002_auto_20171205_0505.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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'), - ), - ] diff --git a/testproj/snippets/serializers.py b/testproj/snippets/serializers.py index c149917..eaa5798 100644 --- a/testproj/snippets/serializers.py +++ b/testproj/snippets/serializers.py @@ -1,3 +1,4 @@ +from django.contrib.auth import get_user_model from rest_framework import serializers from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES @@ -23,20 +24,35 @@ class SnippetSerializer(serializers.Serializer): create: docstring for create from serializer classdoc """ id = serializers.IntegerField(read_only=True, help_text="id serializer help text") - owner = serializers.ReadOnlyField(source='owner.username') + owner = serializers.PrimaryKeyRelatedField( + queryset=get_user_model().objects.all(), + default=serializers.CurrentUserDefault(), + help_text="The ID of the user that created this snippet; if none is provided, " + "defaults to the currently logged in user." + ) + owner_as_string = serializers.PrimaryKeyRelatedField( + help_text="The ID of the user that created this snippet.", + pk_field=serializers.CharField(help_text="this help text should not show up"), + read_only=True, + source='owner', + ) title = serializers.CharField(required=False, allow_blank=True, max_length=100) code = serializers.CharField(style={'base_template': 'textarea.html'}) linenos = serializers.BooleanField(required=False) language = LanguageSerializer(help_text="Sample help text for language") 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") + example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True) + difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField", + read_only=True, default=lambda: 6.9) def create(self, validated_data): """ Create and return a new `Snippet` instance, given the validated data. """ + del validated_data['styles'] + del validated_data['lines'] + del validated_data['difficulty_factor'] return Snippet.objects.create(**validated_data) def update(self, instance, validated_data): diff --git a/testproj/users/serializers.py b/testproj/users/serializers.py index 65694ae..87cc87c 100644 --- a/testproj/users/serializers.py +++ b/testproj/users/serializers.py @@ -6,14 +6,16 @@ from snippets.models import Snippet class UserSerializerrr(serializers.ModelSerializer): snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all()) + article_slugs = serializers.SlugRelatedField(read_only=True, slug_field='slug', many=True, source='articlessss') last_connected_ip = serializers.IPAddressField(help_text="i'm out of ideas", protocol='ipv4', read_only=True) last_connected_at = serializers.DateField(help_text="really?", read_only=True) class Meta: model = User - fields = ('id', 'username', 'email', 'snippets', 'last_connected_ip', 'last_connected_at') + fields = ('id', 'username', 'email', 'articles', 'snippets', + 'last_connected_ip', 'last_connected_at', 'article_slugs') class UserListQuerySerializer(serializers.Serializer): - username = serializers.CharField(help_text="this field is generated from a query_serializer") - is_staff = serializers.BooleanField(help_text="this one too!") + username = serializers.CharField(help_text="this field is generated from a query_serializer", required=False) + is_staff = serializers.BooleanField(help_text="this one too!", required=False) diff --git a/tests/conftest.py b/tests/conftest.py index d4efdb1..ff80891 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,9 @@ import json import os import pytest +from django.contrib.auth.models import User +from rest_framework.test import APIRequestFactory +from rest_framework.views import APIView from ruamel import yaml from drf_yasg import openapi, codecs @@ -10,11 +13,16 @@ from drf_yasg.generators import OpenAPISchemaGenerator @pytest.fixture -def generator(): - return OpenAPISchemaGenerator( - info=openapi.Info(title="Test generator", default_version="v1"), - version="v2", - ) +def mock_schema_request(db): + from rest_framework.test import force_authenticate + + factory = APIRequestFactory() + user = User.objects.create_user(username='admin', is_staff=True, is_superuser=True) + + request = factory.get('/swagger.json') + force_authenticate(request, user=user) + request = APIView().initialize_request(request) + return request @pytest.fixture @@ -28,19 +36,22 @@ def codec_yaml(): @pytest.fixture -def swagger(generator): - return generator.get_schema(None, True) +def swagger(mock_schema_request): + generator = OpenAPISchemaGenerator( + info=openapi.Info(title="Test generator", default_version="v1"), + version="v2", + ) + return generator.get_schema(mock_schema_request, True) @pytest.fixture -def swagger_dict(generator): - swagger = generator.get_schema(None, True) +def swagger_dict(swagger): json_bytes = codec_json().encode(swagger) return json.loads(json_bytes.decode('utf-8')) @pytest.fixture -def validate_schema(): +def validate_schema(db): def validate_schema(swagger): from flex.core import parse as validate_flex from swagger_spec_validator.validator20 import validate_spec as validate_ssv diff --git a/tests/reference.yaml b/tests/reference.yaml index ac7f253..b36e1ff 100644 --- a/tests/reference.yaml +++ b/tests/reference.yaml @@ -178,6 +178,7 @@ paths: description: slug model help_text required: true type: string + format: slug pattern: '[a-z0-9]+(?:-[a-z0-9]+)' /articles/{slug}/image/: get: @@ -231,6 +232,7 @@ paths: description: slug model help_text required: true type: string + format: slug pattern: '[a-z0-9]+(?:-[a-z0-9]+)' /plain/: get: @@ -355,12 +357,12 @@ paths: - name: username in: query description: this field is generated from a query_serializer - required: true + required: false type: string - name: is_staff in: query description: this one too! - required: true + required: false type: boolean responses: '200': @@ -459,13 +461,16 @@ definitions: required: - title - body - - references - - uuid type: object properties: title: description: title model help_text type: string + author: + description: The ID of the user that created this article; if none is provided, + defaults to the currently logged in user. + type: integer + default: 1 body: description: body serializer help_text type: string @@ -489,14 +494,16 @@ definitions: description: but i needed to test these 2 fields somehow type: string format: uri + readOnly: true uuid: description: should articles have UUIDs? type: string format: uuid + readOnly: true cover: type: string - format: uri readOnly: true + format: uri cover_name: type: string readOnly: true @@ -516,8 +523,6 @@ definitions: required: - code - language - - example_projects - - difficulty_factor type: object properties: id: @@ -525,6 +530,12 @@ definitions: type: integer readOnly: true owner: + description: The ID of the user that created this snippet; if none is provided, + defaults to the currently logged in user. + type: integer + default: 1 + owner_as_string: + description: The ID of the user that created this snippet. type: string readOnly: true title: @@ -1020,12 +1031,16 @@ definitions: type: array items: $ref: '#/definitions/Project' + readOnly: true difficulty_factor: description: this is here just to test FloatField type: number + default: 6.9 + readOnly: true UserSerializerrr: required: - username + - articles - snippets type: object properties: @@ -1039,10 +1054,15 @@ definitions: email: type: string format: email + articles: + type: array + items: + type: integer + uniqueItems: true snippets: type: array items: - type: string + type: integer uniqueItems: true last_connected_ip: description: i'm out of ideas @@ -1054,6 +1074,13 @@ definitions: type: string format: date readOnly: true + article_slugs: + type: array + items: + type: string + readOnly: true + uniqueItems: true + readOnly: true securityDefinitions: basic: type: basic diff --git a/tests/test_reference_schema.py b/tests/test_reference_schema.py index 5ffc982..d3bd8a0 100644 --- a/tests/test_reference_schema.py +++ b/tests/test_reference_schema.py @@ -2,7 +2,6 @@ from datadiff.tools import assert_equal def test_reference_schema(swagger_dict, reference_schema): - # formatted better than pytest diff swagger_dict = dict(swagger_dict) reference_schema = dict(reference_schema) ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions'] @@ -10,4 +9,5 @@ def test_reference_schema(swagger_dict, reference_schema): swagger_dict.pop(attr, None) reference_schema.pop(attr, None) + # formatted better than pytest diff assert_equal(swagger_dict, reference_schema) diff --git a/tests/test_schema_generator.py b/tests/test_schema_generator.py index 5be3455..62b4f80 100644 --- a/tests/test_schema_generator.py +++ b/tests/test_schema_generator.py @@ -7,16 +7,11 @@ from drf_yasg import openapi, codecs from drf_yasg.generators import OpenAPISchemaGenerator -def test_schema_generates_without_errors(generator): - generator.get_schema(None, True) - - -def test_schema_is_valid(generator, codec_yaml): - swagger = generator.get_schema(request=None, public=False) +def test_schema_is_valid(swagger, codec_yaml): codec_yaml.encode(swagger) -def test_invalid_schema_fails(codec_json): +def test_invalid_schema_fails(codec_json, mock_schema_request): # noinspection PyTypeChecker bad_generator = OpenAPISchemaGenerator( info=openapi.Info( @@ -26,40 +21,37 @@ def test_invalid_schema_fails(codec_json): version="v2", ) - swagger = bad_generator.get_schema(None, True) + swagger = bad_generator.get_schema(mock_schema_request, True) with pytest.raises(codecs.SwaggerValidationError): codec_json.encode(swagger) -def test_json_codec_roundtrip(codec_json, generator, validate_schema): - swagger = generator.get_schema(None, True) +def test_json_codec_roundtrip(codec_json, swagger, validate_schema): json_bytes = codec_json.encode(swagger) validate_schema(json.loads(json_bytes.decode('utf-8'))) -def test_yaml_codec_roundtrip(codec_yaml, generator, validate_schema): - swagger = generator.get_schema(None, True) +def test_yaml_codec_roundtrip(codec_yaml, swagger, validate_schema): yaml_bytes = codec_yaml.encode(swagger) assert b'omap' not in yaml_bytes # ensure no ugly !!omap is outputted assert b'&id' not in yaml_bytes and b'*id' not in yaml_bytes # ensure no YAML references are generated validate_schema(yaml.safe_load(yaml_bytes.decode('utf-8'))) -def test_yaml_and_json_match(codec_yaml, codec_json, generator): - swagger = generator.get_schema(None, True) +def test_yaml_and_json_match(codec_yaml, codec_json, swagger): yaml_schema = yaml.safe_load(codec_yaml.encode(swagger).decode('utf-8')) json_schema = json.loads(codec_json.encode(swagger).decode('utf-8')) assert yaml_schema == json_schema -def test_basepath_only(): +def test_basepath_only(mock_schema_request): generator = OpenAPISchemaGenerator( info=openapi.Info(title="Test generator", default_version="v1"), version="v2", url='/basepath/', ) - swagger = generator.get_schema(None, public=True) + swagger = generator.get_schema(mock_schema_request, public=True) assert 'host' not in swagger assert 'schemes' not in swagger assert swagger['basePath'] == '/' # base path is not implemented for now diff --git a/tox.ini b/tox.ini index a254d67..5d5c09f 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ deps = -rrequirements/test.txt commands = - pytest --cov-config .coveragerc --cov-append --cov + pytest --cov-config .coveragerc --cov-append --cov {posargs} [testenv:py36-drfmaster] pip_pre = True