Make swagger_schema_fields work on serializer Fields

Closes #167.
openapi3
Cristi Vîjdea 2018-08-07 19:44:47 +03:00
parent 65aac1da2c
commit 37c00ab3fb
8 changed files with 99 additions and 56 deletions

View File

@ -52,7 +52,7 @@ Changelog
- **ADDED:** added ``DEFAULT_GENERATOR_CLASS`` setting and ``--generator-class`` argument to the ``generate_swagger`` - **ADDED:** added ``DEFAULT_GENERATOR_CLASS`` setting and ``--generator-class`` argument to the ``generate_swagger``
management command (:issue:`140`) management command (:issue:`140`)
- **FIXED:** fixed wrongly required ``'count'`` response field on ``CursorPagination`` (:issue:`141`) - **FIXED:** fixed wrongly required ``'count'`` response field on ``CursorPagination`` (:issue:`141`)
- **FIXED:** fixed some cases where ``swagger_extra_fields`` would not be handlded (:pr:`142`) - **FIXED:** fixed some cases where ``swagger_schema_fields`` would not be handlded (:pr:`142`)
- **FIXED:** fixed crash when encountering ``coreapi.Fields``\ s without a ``schema`` (:issue:`143`) - **FIXED:** fixed crash when encountering ``coreapi.Fields``\ s without a ``schema`` (:issue:`143`)
********* *********

View File

@ -2,10 +2,9 @@ import inspect
import logging import logging
from rest_framework import serializers from rest_framework import serializers
from rest_framework.utils import encoders, json
from .. import openapi from .. import openapi
from ..utils import decimal_as_float, force_real_str, is_list_view from ..utils import force_real_str, get_field_default, is_list_view
#: Sentinel value that inspectors must return to signal that they do not know how to handle an object #: Sentinel value that inspectors must return to signal that they do not know how to handle an object
NotHandled = object() NotHandled = object()
@ -135,6 +134,19 @@ class FieldInspector(BaseInspector):
super(FieldInspector, self).__init__(view, path, method, components, request) super(FieldInspector, self).__init__(view, path, method, components, request)
self.field_inspectors = field_inspectors self.field_inspectors = field_inspectors
def add_manual_fields(self, serializer_or_field, schema):
"""Set fields from the ``swagger_schem_fields`` attribute on the Meta class. This method is called
only for serializers or fields that are converted into ``openapi.Schema`` objects.
:param serializer_or_field: serializer or field instance
:param openapi.Schema schema: the schema object to be modified in-place
"""
meta = getattr(serializer_or_field, 'Meta', None)
swagger_schema_fields = getattr(meta, 'swagger_schema_fields', {})
if swagger_schema_fields:
for attr, val in swagger_schema_fields.items():
setattr(schema, attr, val)
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
"""Convert a drf Serializer or Field instance into a Swagger object. """Convert a drf Serializer or Field instance into a Swagger object.
@ -205,46 +217,29 @@ class FieldInspector(BaseInspector):
instance_kwargs['required'] = field.required instance_kwargs['required'] = field.required
if 'default' not in instance_kwargs and swagger_object_type != openapi.Items: if 'default' not in instance_kwargs and swagger_object_type != openapi.Items:
default = getattr(field, 'default', serializers.empty) default = get_field_default(field)
if default is not serializers.empty: if default not in (None, serializers.empty):
if callable(default):
try:
if hasattr(default, 'set_context'):
default.set_context(field)
default = default()
except Exception: # pragma: no cover
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 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)
default = None
if default is not None:
instance_kwargs['default'] = default instance_kwargs['default'] = default
if instance_kwargs.get('type', None) != openapi.TYPE_ARRAY: if instance_kwargs.get('type', None) != openapi.TYPE_ARRAY:
instance_kwargs.setdefault('title', title) instance_kwargs.setdefault('title', title)
if description is not None:
instance_kwargs.setdefault('description', description) instance_kwargs.setdefault('description', description)
instance_kwargs.update(kwargs) instance_kwargs.update(kwargs)
if existing_object is not None: if existing_object is not None:
assert isinstance(existing_object, swagger_object_type) assert isinstance(existing_object, swagger_object_type)
for attr, val in sorted(instance_kwargs.items()): for key, val in sorted(instance_kwargs.items()):
setattr(existing_object, attr, val) setattr(existing_object, key, val)
return existing_object result = existing_object
else:
result = swagger_object_type(**instance_kwargs)
return swagger_object_type(**instance_kwargs) # Provide an option to add manual paremeters to a schema
# for example, to add examples
if swagger_object_type == openapi.Schema:
self.add_manual_fields(field, result)
return result
# arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements # arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
child_swagger_type = openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items child_swagger_type = openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items

View File

@ -31,19 +31,6 @@ class InlineSerializerInspector(SerializerInspector):
#: whether to output :class:`.Schema` definitions inline or into the ``definitions`` section #: whether to output :class:`.Schema` definitions inline or into the ``definitions`` section
use_definitions = False use_definitions = False
def add_manual_fields(self, serializer, schema):
"""Set fields from the ``swagger_schem_fields`` attribute on the serializer's Meta class. This method is called
only for serializers that are converted into ``openapi.Schema`` objects.
:param serializer: serializer instance
:param openapi.Schema schema: the schema object to be modified in-place
"""
serializer_meta = getattr(serializer, 'Meta', None)
swagger_schema_fields = getattr(serializer_meta, 'swagger_schema_fields', {})
if swagger_schema_fields:
for attr, val in swagger_schema_fields.items():
setattr(schema, attr, val)
def get_schema(self, serializer): def get_schema(self, serializer):
return self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions) return self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions)
@ -124,9 +111,6 @@ class InlineSerializerInspector(SerializerInspector):
# it is better to just remove title from inline models # it is better to just remove title from inline models
del result.title del result.title
# Provide an option to add manual paremeters to a schema
# for example, to add examples
self.add_manual_fields(field, result)
return result return result
if not ref_name or not use_references: if not ref_name or not use_references:
@ -696,9 +680,9 @@ else:
assert use_references is True, "Can not create schema for RecursiveField when use_references is False" assert use_references is True, "Can not create schema for RecursiveField when use_references is False"
ref_name = get_serializer_ref_name(field.proxied) ref_name = get_serializer_ref_name(field.proxied)
assert ref_name is not None, "Can not create RecursiveField schema for inline ModelSerializer" assert ref_name is not None, "Can't create RecursiveField schema for inline " + str(type(field.proxied))
return openapi.SchemaRef(self.components.with_scope(openapi.SCHEMA_DEFINITIONS), ref_name, definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS)
ignore_unresolved=True) return openapi.SchemaRef(definitions, ref_name, ignore_unresolved=True)
return NotHandled return NotHandled

View File

@ -8,6 +8,7 @@ from rest_framework import serializers, status
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.request import is_form_media_type from rest_framework.request import is_form_media_type
from rest_framework.settings import api_settings as rest_framework_settings from rest_framework.settings import api_settings as rest_framework_settings
from rest_framework.utils import encoders, json
from rest_framework.views import APIView from rest_framework.views import APIView
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -354,3 +355,38 @@ def force_real_str(s, encoding='utf-8', strings_only=False, errors='strict'):
s = '' + s s = '' + s
return s return s
def get_field_default(field):
"""
Get the default value for a field, converted to a JSON-compatible value while properly handling callables.
:param field: field instance
:return: default value
"""
default = getattr(field, 'default', serializers.empty)
if default is not serializers.empty:
if callable(default):
try:
if hasattr(default, 'set_context'):
default.set_context(field)
default = default()
except Exception: # pragma: no cover
logger.warning("default for %s is callable but it raised an exception when "
"called; 'default' will not be set on schema", field, exc_info=True)
default = serializers.empty
if default is not serializers.empty:
try:
default = field.to_representation(default)
# 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)
default = serializers.empty
return default

View File

@ -23,12 +23,34 @@ class ExampleProjectSerializer(serializers.Serializer):
ref_name = 'Project' ref_name = 'Project'
class UnixTimestampField(serializers.DateTimeField):
def to_representation(self, value):
""" Return epoch time for a datetime object or ``None``"""
from django.utils.dateformat import format
try:
return int(format(value, 'U'))
except (AttributeError, TypeError):
return None
def to_internal_value(self, value):
import datetime
return datetime.datetime.fromtimestamp(int(value))
class Meta:
swagger_schema_fields = {
'format': 'integer',
'title': 'Client date time suu',
'description': 'Date time in unix timestamp format',
}
class SnippetSerializer(serializers.Serializer): class SnippetSerializer(serializers.Serializer):
"""SnippetSerializer classdoc """SnippetSerializer classdoc
create: docstring for create from serializer classdoc create: docstring for create from serializer classdoc
""" """
id = serializers.IntegerField(read_only=True, help_text="id serializer help text") id = serializers.IntegerField(read_only=True, help_text="id serializer help text")
created = UnixTimestampField(read_only=True)
owner = serializers.PrimaryKeyRelatedField( owner = serializers.PrimaryKeyRelatedField(
queryset=get_user_model().objects.all(), queryset=get_user_model().objects.all(),
default=serializers.CurrentUserDefault(), default=serializers.CurrentUserDefault(),

View File

@ -5,7 +5,7 @@ from drf_yasg.utils import swagger_serializer_method
from snippets.models import Snippet from snippets.models import Snippet
try: try:
import typing import typing # noqa: F401
from .method_serializers_with_typing import MethodFieldExampleSerializer from .method_serializers_with_typing import MethodFieldExampleSerializer
except ImportError: except ImportError:
from .method_serializers_without_typing import MethodFieldExampleSerializer from .method_serializers_without_typing import MethodFieldExampleSerializer

View File

@ -947,6 +947,12 @@ definitions:
description: id serializer help text description: id serializer help text
type: integer type: integer
readOnly: true readOnly: true
created:
title: Client date time suu
type: string
format: integer
readOnly: true
description: Date time in unix timestamp format
owner: owner:
title: Owner title: Owner
description: The ID of the user that created this snippet; if none is provided, description: The ID of the user that created this snippet; if none is provided,

View File

@ -57,7 +57,7 @@ exclude = **/migrations/*
ignore = F405 ignore = F405
[isort] [isort]
skip = .eggs,.tox,docs,env,venv skip = .eggs,.tox,docs,env,venv,node_modules
skip_glob = **/migrations/* skip_glob = **/migrations/*
not_skip = __init__.py not_skip = __init__.py
atomic = true atomic = true