Merge pull request #64 from axnsan12/release/1.4.1

Release version 1.4.1
openapi3 1.4.1
Cristi Vîjdea 2018-02-21 05:17:57 +02:00 committed by GitHub
commit 10c7e22940
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 159 additions and 87 deletions

View File

@ -2,6 +2,17 @@
Changelog
#########
*********
**1.4.1**
*********
- **FIXED:** the ``coerce_to_string`` is now respected when setting the type, default value and min/max values of
``DecimalField`` in the OpenAPI schema (:issue:`62`)
- **FIXED:** error responses from web UI views are now rendered with ``TemplateHTMLRenderer`` instead of throwing
confusing errors (:issue:`58`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.10.0
- **IMPROVED:** updated ``ReDoc`` to version 1.21.0
*********
**1.4.0**
*********

View File

@ -162,6 +162,18 @@ autodoc_mock_imports = []
nitpick_ignore = [
('py:class', 'object'),
('py:class', 'bool'),
('py:class', 'dict'),
('py:class', 'list'),
('py:class', 'str'),
('py:class', 'int'),
('py:class', 'bytes'),
('py:class', 'tuple'),
('py:class', 'callable'),
('py:class', 'type'),
('py:class', 'OrderedDict'),
('py:class', 'None'),
('py:class', 'Exception'),
('py:class', 'collections.OrderedDict'),
@ -174,29 +186,17 @@ nitpick_ignore = [
('py:class', 'OpenAPICodecJson'),
('py:class', 'OpenAPISchemaGenerator'),
('py:obj', 'bool'),
('py:obj', 'dict'),
('py:obj', 'list'),
('py:obj', 'str'),
('py:obj', 'int'),
('py:obj', 'bytes'),
('py:obj', 'tuple'),
('py:obj', 'callable'),
('py:obj', 'type'),
('py:obj', 'OrderedDict'),
('py:obj', 'None'),
('py:obj', 'coreapi.Field'),
('py:obj', 'BaseFilterBackend'),
('py:obj', 'BasePagination'),
('py:obj', 'Request'),
('py:obj', 'rest_framework.request.Request'),
('py:obj', 'rest_framework.serializers.Field'),
('py:obj', 'serializers.Field'),
('py:obj', 'serializers.BaseSerializer'),
('py:obj', 'Serializer'),
('py:obj', 'BaseSerializer'),
('py:obj', 'APIView'),
('py:class', 'coreapi.Field'),
('py:class', 'BaseFilterBackend'),
('py:class', 'BasePagination'),
('py:class', 'Request'),
('py:class', 'rest_framework.request.Request'),
('py:class', 'rest_framework.serializers.Field'),
('py:class', 'serializers.Field'),
('py:class', 'serializers.BaseSerializer'),
('py:class', 'Serializer'),
('py:class', 'BaseSerializer'),
('py:class', 'APIView'),
]
# even though the package should be already installed, the sphinx build on RTD

87
package-lock.json generated
View File

@ -4,9 +4,9 @@
"lockfileVersion": 1,
"dependencies": {
"argparse": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
"integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "1.0.3"
}
@ -38,9 +38,9 @@
}
},
"commander": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
"integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
"optional": true
},
"core-js": {
@ -73,9 +73,9 @@
"integrity": "sha1-8TyUAhQdoJ50rfTmN5jXkiBEOPI="
},
"es6-promise": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.2.tgz",
"integrity": "sha512-LSas5vsuA6Q4nEdf9wokY5/AJYXry98i0IzXsv49rYsgDGDNDPbqAYR1Pe23iFxygfbGZNR/5VrHXBCh2BhvUQ=="
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
"integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ=="
},
"esprima": {
"version": "4.0.0",
@ -126,7 +126,7 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz",
"integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==",
"requires": {
"argparse": "1.0.9",
"argparse": "1.0.10",
"esprima": "4.0.0"
}
},
@ -145,10 +145,10 @@
"requires": {
"call-me-maybe": "1.0.1",
"debug": "3.1.0",
"es6-promise": "4.2.2",
"es6-promise": "4.2.4",
"js-yaml": "3.10.0",
"ono": "4.0.3",
"z-schema": "3.19.0"
"z-schema": "3.19.1"
}
},
"lodash.get": {
@ -185,9 +185,12 @@
}
},
"openapi-sampler": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-0.4.3.tgz",
"integrity": "sha512-Ml6o1gt++ZQ4JKL344YRo/fX05yuM6C+l/mGVX2yjhu1BRKyrRK4Z46uBTKSVaag1xINBFwYG7dZdz/10AmPzA=="
"version": "1.0.0-beta.8",
"resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.8.tgz",
"integrity": "sha1-v0P/R3N/xOH5iNDiCC1JeI9B3q0=",
"requires": {
"json-pointer": "0.6.0"
}
},
"perfect-scrollbar": {
"version": "0.8.1",
@ -195,36 +198,36 @@
"integrity": "sha512-RNC5tX/JMRYR+qVdJTEAWnRxw0Yf9lvbO8lTuAOvgDODkiA8lveTSkvrNMhmaGKEyimJpJl+myb/syVS9YyPuw=="
},
"prismjs": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.10.0.tgz",
"integrity": "sha1-d+UYfCrmsyU/zDEwKc8l/lN3hyE=",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.11.0.tgz",
"integrity": "sha1-KXrvM+t5Qhv9sZJzpQkspRWXDSk=",
"requires": {
"clipboard": "1.7.1"
}
},
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
},
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz",
"integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==",
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "1.0.7",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"redoc": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-1.20.0.tgz",
"integrity": "sha1-1c16xoQKJ8/7RzvSiYAFUq+CHq8=",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-1.21.0.tgz",
"integrity": "sha1-RY8E7b7MqyVbQORhZA0eRG949N0=",
"requires": {
"core-js": "2.5.3",
"dropkickjs": "2.1.10",
@ -234,9 +237,9 @@
"json-schema-ref-parser": "3.3.1",
"lunr": "1.0.0",
"mark.js": "8.11.1",
"openapi-sampler": "0.4.3",
"openapi-sampler": "1.0.0-beta.8",
"perfect-scrollbar": "0.8.1",
"prismjs": "1.10.0",
"prismjs": "1.11.0",
"remarkable": "1.7.1",
"scrollparent": "2.0.1",
"slugify": "1.2.9",
@ -298,7 +301,7 @@
"requires": {
"builtin-status-codes": "3.0.0",
"inherits": "2.0.3",
"readable-stream": "2.3.3",
"readable-stream": "2.3.4",
"to-arraybuffer": "1.0.1",
"xtend": "4.0.1"
}
@ -312,9 +315,9 @@
}
},
"swagger-ui-dist": {
"version": "3.9.3",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.9.3.tgz",
"integrity": "sha1-yrqR6FUNfSRkoIRWvZNtfEMPDdM="
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.10.0.tgz",
"integrity": "sha1-ilrzP/ImPHFaFD9z8qjUfVOZQ+8="
},
"tiny-emitter": {
"version": "2.0.2",
@ -348,9 +351,9 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"validator": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.2.0.tgz",
"integrity": "sha512-6Ij4Eo0KM4LkR0d0IegOwluG5453uqT5QyF5SV5Ezvm8/zmkKI/L4eoraafZGlZPC9guLkwKzgypcw8VGWWnGA=="
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz",
"integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA=="
},
"xtend": {
"version": "4.0.1",
@ -358,14 +361,14 @@
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
},
"z-schema": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.19.0.tgz",
"integrity": "sha512-V94f3ODuluBS4kQLLjNhwoMek0dyIXCsvNu/A17dAyJ6sMhT5KkJQwSn07R0naByLIXJWMDk+ruMfI/3G3hS4Q==",
"version": "3.19.1",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.19.1.tgz",
"integrity": "sha512-jPNzqmOu3+AGbb4krDODqo4QBzwUGDVzyfGyy1HtWaUnafltQotatSpxxWd6Mp0iSZOUwHU5sqKYi+U8HsHMkg==",
"requires": {
"commander": "2.13.0",
"commander": "2.14.1",
"lodash.get": "4.4.2",
"lodash.isequal": "4.5.0",
"validator": "9.2.0"
"validator": "9.4.1"
}
},
"zone.js": {

View File

@ -1,8 +1,8 @@
{
"name": "drf-yasg",
"dependencies": {
"redoc": "^1.20.0",
"swagger-ui-dist": "^3.9.3"
"redoc": "^1.21.0",
"swagger-ui-dist": "^3.10.0"
},
"repository": {
"type": "git",

View File

@ -1,5 +1,5 @@
# used by the 'docs' tox env for building the documentation
Sphinx>=1.6.5
Sphinx>=1.7.0
sphinx_rtd_theme>=0.2.4
Pillow>=4.3.0
readme_renderer>=17.2

View File

@ -6,7 +6,7 @@ from rest_framework import serializers
from rest_framework.utils import encoders, json
from .. import openapi
from ..utils import is_list_view
from ..utils import decimal_as_float, is_list_view
#: Sentinel value that inspectors must return to signal that they do not know how to handle an object
NotHandled = object()
@ -224,6 +224,8 @@ class FieldInspector(BaseInspector):
# JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists
default = json.loads(json.dumps(default, cls=encoders.JSONEncoder))
if decimal_as_float(field):
default = float(default)
except Exception: # pragma: no cover
logger.warning("'default' on schema for %s will not be set because "
"to_representation raised an exception", field, exc_info=True)

View File

@ -1,5 +1,6 @@
import operator
from collections import OrderedDict
from decimal import Decimal
from django.core import validators
from django.db import models
@ -8,7 +9,7 @@ from rest_framework.settings import api_settings as rest_framework_settings
from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import filter_none
from ..utils import decimal_as_float, filter_none
from .base import FieldInspector, NotHandled, SerializerInspector
@ -258,18 +259,29 @@ def find_limits(field):
if isinstance(field, field_class)
]
if isinstance(field, serializers.DecimalField) and not decimal_as_float(field):
return limits
for validator in field.validators:
if not hasattr(validator, 'limit_value'):
continue
limit_value = validator.limit_value
if isinstance(limit_value, Decimal) and decimal_as_float(field):
limit_value = float(limit_value)
for validator_class, attr, improves in applicable_limits:
if isinstance(validator, validator_class):
if attr not in limits or improves(validator.limit_value, limits[attr]):
limits[attr] = validator.limit_value
if attr not in limits or improves(limit_value, limits[attr]):
limits[attr] = limit_value
return OrderedDict(sorted(limits.items()))
def decimal_field_type(field):
return openapi.TYPE_NUMBER if decimal_as_float(field) else openapi.TYPE_STRING
model_field_to_basic_type = [
(models.AutoField, (openapi.TYPE_INTEGER, None)),
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
@ -277,7 +289,7 @@ model_field_to_basic_type = [
(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.DecimalField, (decimal_field_type, openapi.FORMAT_DECIMAL)),
(models.DurationField, (openapi.TYPE_INTEGER, None)),
(models.FloatField, (openapi.TYPE_NUMBER, None)),
(models.IntegerField, (openapi.TYPE_INTEGER, None)),
@ -300,9 +312,11 @@ serializer_field_to_basic_type = [
(serializers.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
(serializers.RegexField, (openapi.TYPE_STRING, None)),
(serializers.CharField, (openapi.TYPE_STRING, None)),
((serializers.BooleanField, serializers.NullBooleanField), (openapi.TYPE_BOOLEAN, None)),
(serializers.BooleanField, (openapi.TYPE_BOOLEAN, None)),
(serializers.NullBooleanField, (openapi.TYPE_BOOLEAN, None)),
(serializers.IntegerField, (openapi.TYPE_INTEGER, None)),
((serializers.FloatField, serializers.DecimalField), (openapi.TYPE_NUMBER, None)),
(serializers.FloatField, (openapi.TYPE_NUMBER, None)),
(serializers.DecimalField, (decimal_field_type, openapi.FORMAT_DECIMAL)),
(serializers.DurationField, (openapi.TYPE_NUMBER, None)), # ?
(serializers.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
(serializers.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
@ -326,6 +340,8 @@ def get_basic_type_info(field):
for field_class, type_format in basic_type_info:
if isinstance(field, field_class):
swagger_type, format = type_format
if callable(swagger_type):
swagger_type = swagger_type(field)
if callable(format):
format = format(field)
break

View File

@ -35,6 +35,7 @@ FORMAT_URI = "uri" #:
# pulled out of my ass
FORMAT_UUID = "uuid" #:
FORMAT_SLUG = "slug" #:
FORMAT_DECIMAL = "decimal"
IN_BODY = 'body' #:
IN_PATH = 'path' #:

View File

@ -1,7 +1,9 @@
from django.shortcuts import render, resolve_url
from rest_framework.renderers import BaseRenderer
from rest_framework.renderers import BaseRenderer, TemplateHTMLRenderer
from rest_framework.utils import json
from drf_yasg.openapi import Swagger
from .app_settings import redoc_settings, swagger_settings
from .codecs import VALIDATORS, OpenAPICodecJson, OpenAPICodecYaml
@ -51,6 +53,11 @@ class _UIRenderer(BaseRenderer):
template = ''
def render(self, swagger, accepted_media_type=None, renderer_context=None):
if not isinstance(swagger, Swagger):
# if `swagger` is not a ``Swagger`` object, it means we somehow got a non-success ``Response``
# in that case, it's probably better to let the default ``TemplateHTMLRenderer`` render it
# see https://github.com/axnsan12/drf-yasg/issues/58
return TemplateHTMLRenderer().render(swagger, accepted_media_type, renderer_context)
self.set_context(renderer_context, swagger)
return render(
renderer_context['request'],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,9 +2,11 @@ import inspect
import logging
from collections import OrderedDict
from django.db import models
from rest_framework import serializers, status
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.request import is_form_media_type
from rest_framework.settings import api_settings as rest_framework_settings
from rest_framework.views import APIView
logger = logging.getLogger(__name__)
@ -118,6 +120,8 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
_methods = methods
if methods or method:
assert available_methods or http_method_names, "`method` or `methods` can only be specified " \
"on @detail_route or @api_view views"
assert bool(methods) != bool(method), "specify either method or methods"
assert not isinstance(methods, str), "`methods` expects to receive a list of methods;" \
" use `method` for a single argument"
@ -273,3 +277,15 @@ def get_produces(renderer_classes):
media_types = [renderer.media_type for renderer in renderer_classes or []]
media_types = [encoding for encoding in media_types if 'html' not in encoding]
return media_types
def decimal_as_float(field):
"""
Returns true if ``field`` is a django-rest-framework DecimalField and its ``coerce_to_string`` attribute or the
``COERCE_DECIMAL_TO_STRING`` setting is set to ``False``.
:rtype: bool
"""
if isinstance(field, serializers.DecimalField) or isinstance(field, models.DecimalField):
return not getattr(field, 'coerce_to_string', rest_framework_settings.COERCE_DECIMAL_TO_STRING)
return False

View File

@ -1,11 +1,13 @@
from decimal import Decimal
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.compat import MinValueValidator
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet
class LanguageSerializer(serializers.Serializer):
name = serializers.ChoiceField(
choices=LANGUAGE_CHOICES, default='python', help_text='The name of the programming language')
@ -14,7 +16,6 @@ class LanguageSerializer(serializers.Serializer):
class ExampleProjectSerializer(serializers.Serializer):
project_name = serializers.CharField(help_text='Name of the project')
github_repo = serializers.CharField(required=True, help_text='Github repository of the project')
@ -49,6 +50,10 @@ class SnippetSerializer(serializers.Serializer):
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)
rate_as_string = serializers.DecimalField(max_digits=6, decimal_places=3, default=Decimal('0.0'),
validators=[MinValueValidator(Decimal('0.0'))])
rate = serializers.DecimalField(max_digits=6, decimal_places=3, default=Decimal('0.0'), coerce_to_string=False,
validators=[MinValueValidator(Decimal('0.0'))])
def create(self, validated_data):
"""

View File

@ -1036,6 +1036,17 @@ definitions:
type: number
readOnly: true
default: 6.9
rateAsString:
title: Rate as string
type: string
format: decimal
default: '0.000'
rate:
title: Rate
type: number
format: decimal
default: 0.0
minimum: 0.0
UserSerializerrr:
required:
- username