diff --git a/docs/changelog.rst b/docs/changelog.rst index 59b5dd4..4a2cdb9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -52,7 +52,7 @@ Changelog - **ADDED:** added ``DEFAULT_GENERATOR_CLASS`` setting and ``--generator-class`` argument to the ``generate_swagger`` management command (:issue:`140`) - **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`) ********* diff --git a/src/drf_yasg/inspectors/base.py b/src/drf_yasg/inspectors/base.py index 23fe9d4..35cdc75 100644 --- a/src/drf_yasg/inspectors/base.py +++ b/src/drf_yasg/inspectors/base.py @@ -2,10 +2,9 @@ import inspect import logging from rest_framework import serializers -from rest_framework.utils import encoders, json 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 NotHandled = object() @@ -135,6 +134,19 @@ class FieldInspector(BaseInspector): super(FieldInspector, self).__init__(view, path, method, components, request) 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): """Convert a drf Serializer or Field instance into a Swagger object. @@ -205,46 +217,29 @@ class FieldInspector(BaseInspector): instance_kwargs['required'] = field.required if 'default' not in instance_kwargs and swagger_object_type != openapi.Items: - 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' 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 + default = get_field_default(field) + if default not in (None, serializers.empty): + instance_kwargs['default'] = default if instance_kwargs.get('type', None) != openapi.TYPE_ARRAY: instance_kwargs.setdefault('title', title) - instance_kwargs.setdefault('description', description) + if description is not None: + instance_kwargs.setdefault('description', description) instance_kwargs.update(kwargs) if existing_object is not None: assert isinstance(existing_object, swagger_object_type) - for attr, val in sorted(instance_kwargs.items()): - setattr(existing_object, attr, val) - return existing_object + for key, val in sorted(instance_kwargs.items()): + setattr(existing_object, key, val) + 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 child_swagger_type = openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items diff --git a/src/drf_yasg/inspectors/field.py b/src/drf_yasg/inspectors/field.py index bd025b1..e543eee 100644 --- a/src/drf_yasg/inspectors/field.py +++ b/src/drf_yasg/inspectors/field.py @@ -31,19 +31,6 @@ class InlineSerializerInspector(SerializerInspector): #: whether to output :class:`.Schema` definitions inline or into the ``definitions`` section 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): 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 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 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" 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, - ignore_unresolved=True) + definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS) + return openapi.SchemaRef(definitions, ref_name, ignore_unresolved=True) return NotHandled diff --git a/src/drf_yasg/utils.py b/src/drf_yasg/utils.py index 85f85af..42eae6b 100644 --- a/src/drf_yasg/utils.py +++ b/src/drf_yasg/utils.py @@ -8,6 +8,7 @@ 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.utils import encoders, json from rest_framework.views import APIView logger = logging.getLogger(__name__) @@ -354,3 +355,38 @@ def force_real_str(s, encoding='utf-8', strings_only=False, errors='strict'): s = '' + 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 diff --git a/testproj/snippets/serializers.py b/testproj/snippets/serializers.py index 8f89a7a..fc7a04e 100644 --- a/testproj/snippets/serializers.py +++ b/testproj/snippets/serializers.py @@ -23,12 +23,34 @@ class ExampleProjectSerializer(serializers.Serializer): 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): """SnippetSerializer classdoc create: docstring for create from serializer classdoc """ id = serializers.IntegerField(read_only=True, help_text="id serializer help text") + created = UnixTimestampField(read_only=True) owner = serializers.PrimaryKeyRelatedField( queryset=get_user_model().objects.all(), default=serializers.CurrentUserDefault(), diff --git a/testproj/users/serializers.py b/testproj/users/serializers.py index 2289273..c51ec39 100644 --- a/testproj/users/serializers.py +++ b/testproj/users/serializers.py @@ -5,7 +5,7 @@ from drf_yasg.utils import swagger_serializer_method from snippets.models import Snippet try: - import typing + import typing # noqa: F401 from .method_serializers_with_typing import MethodFieldExampleSerializer except ImportError: from .method_serializers_without_typing import MethodFieldExampleSerializer diff --git a/tests/reference.yaml b/tests/reference.yaml index 4a04bc1..9761850 100644 --- a/tests/reference.yaml +++ b/tests/reference.yaml @@ -947,6 +947,12 @@ definitions: description: id serializer help text type: integer readOnly: true + created: + title: Client date time suu + type: string + format: integer + readOnly: true + description: Date time in unix timestamp format owner: title: Owner description: The ID of the user that created this snippet; if none is provided, diff --git a/tox.ini b/tox.ini index d3d8832..a76c4b1 100644 --- a/tox.ini +++ b/tox.ini @@ -57,7 +57,7 @@ exclude = **/migrations/* ignore = F405 [isort] -skip = .eggs,.tox,docs,env,venv +skip = .eggs,.tox,docs,env,venv,node_modules skip_glob = **/migrations/* not_skip = __init__.py atomic = true