Skip to content

Commit

Permalink
support for SerializerMethodField (axnsan12#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
therefromhere committed Aug 7, 2018
1 parent 16b6ed7 commit fcc65aa
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 3 deletions.
10 changes: 10 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
Changelog
#########

*********
**1.9.x**
*********

*Release date: TODO, 2018*

- **IMPROVED:** added support for SerializerMethodField, with ``swagger_serializer_method`` decorator for the
method field; with which you can provide a serializer class; and support for Python 3.5 style type hinting of the
method field return type (:issue:`137`)


**********
**1.9.2**
Expand Down
46 changes: 46 additions & 0 deletions docs/custom_spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,49 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
replacing/decorating methods on the base class itself.


*********************************
Support for SerializerMethodField
*********************************

Schema generation of ``serializers.SerializerMethodField`` supported in two ways:

1) The decorator ``swagger_serializer_method(serializer)`` for the use case where the serializer method
is using a serializer. e.g.:


.. code-block:: python
from drf_yasg.utils import swagger_serializer_method
class OtherStuffSerializer(serializers.Serializer):
foo = serializers.CharField()
class ParentSerializer(serializers.Serializer):
other_stuff = serializers.SerializerMethodField()
@swagger_serializer_method(serializer=OtherStuffSerializer)
def get_other_stuff(self, obj):
return OtherStuffSerializer().data
Note that the serializer parameter can be either be a serializer class or instance


2) For simple cases where the method is returning one of the supported types,
`Python 3 type hinting`_ of the serializer method return value can be used. e.g.:

.. code-block:: python
class SomeSerializer(serializers.Serializer):
some_number = serializers.SerializerMethodField()
def get_some_number(self, obj) -> float:
return 1.0
********************************
Serializer ``Meta`` nested class
********************************
Expand Down Expand Up @@ -333,3 +376,6 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
Another caveat that stems from this is that any serializer named "``NestedSerializer``" will be forced inline
unless it has a ``ref_name`` set explicitly.


.. _Python 3 type hinting: https://docs.python.org/3/library/typing.html
1 change: 1 addition & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ to this list.
:class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.HiddenFieldInspector' <.inspectors.HiddenFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.RecursiveFieldInspector' <.inspectors.RecursiveFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.SerializerMethodFieldInspector' <.inspectors.SerializerMethodFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.SimpleFieldInspector' <.inspectors.SimpleFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.StringDefaultFieldInspector' <.inspectors.StringDefaultFieldInspector>`, |br| \
``]``
Expand Down
1 change: 1 addition & 0 deletions src/drf_yasg/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'drf_yasg.inspectors.DictFieldInspector',
'drf_yasg.inspectors.HiddenFieldInspector',
'drf_yasg.inspectors.RelatedFieldInspector',
'drf_yasg.inspectors.SerializerMethodFieldInspector',
'drf_yasg.inspectors.SimpleFieldInspector',
'drf_yasg.inspectors.StringDefaultFieldInspector',
],
Expand Down
4 changes: 2 additions & 2 deletions src/drf_yasg/inspectors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .field import (
CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, HiddenFieldInspector,
InlineSerializerInspector, RecursiveFieldInspector, ReferencingSerializerInspector, RelatedFieldInspector,
SimpleFieldInspector, StringDefaultFieldInspector
SerializerMethodFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector
)
from .query import CoreAPICompatInspector, DjangoRestResponsePagination
from .view import SwaggerAutoSchema
Expand All @@ -25,7 +25,7 @@
# field inspectors
'InlineSerializerInspector', 'RecursiveFieldInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector',
'SimpleFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector',
'StringDefaultFieldInspector', 'CamelCaseJSONFilter', 'HiddenFieldInspector',
'StringDefaultFieldInspector', 'CamelCaseJSONFilter', 'HiddenFieldInspector', 'SerializerMethodFieldInspector',

# view inspectors
'SwaggerAutoSchema',
Expand Down
115 changes: 115 additions & 0 deletions src/drf_yasg/inspectors/field.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import inspect
import logging
import operator
from collections import OrderedDict
from decimal import Decimal
import datetime
import uuid

from django.core import validators
from django.db import models
Expand All @@ -13,6 +16,13 @@
from ..utils import decimal_as_float, filter_none, get_serializer_ref_name
from .base import FieldInspector, NotHandled, SerializerInspector

try:
# Python>=3.5
import typing
HAS_TYPING = True
except ImportError:
HAS_TYPING = False

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -409,6 +419,111 @@ def get_basic_type_info(field):
return result


def decimal_return_type():
return openapi.TYPE_STRING if rest_framework_settings.COERCE_DECIMAL_TO_STRING else openapi.TYPE_NUMBER


raw_type_info = [
(bool, (openapi.TYPE_BOOLEAN, None)),
(int, (openapi.TYPE_INTEGER, None)),
(float, (openapi.TYPE_NUMBER, None)),
(Decimal, (decimal_return_type, openapi.FORMAT_DECIMAL)),
(uuid.UUID, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
(datetime.datetime, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
(datetime.date, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
# TODO - support typing.List etc
]


hinting_type_info = raw_type_info


def get_basic_type_info_from_hint(hint_class):
"""Given a class (eg from a SerializerMethodField's return type hint,
return its basic type information - ``type``, ``format``, ``pattern``,
and any applicable min/max limit values.
:param hint_class: the class
:return: the extracted attributes as a dictionary, or ``None`` if the field type is not known
:rtype: OrderedDict
"""

# based on get_basic_type_info, but without the model or field we have less to go on

for check_class, type_format in hinting_type_info:
if issubclass(hint_class, check_class):
swagger_type, format = type_format
if callable(swagger_type):
swagger_type = swagger_type()
# if callable(format):
# format = format(klass)
break
else: # pragma: no cover
return None

pattern = None

result = OrderedDict([
('type', swagger_type),
('format', format),
('pattern', pattern)
])

return result


class SerializerMethodFieldInspector(FieldInspector):

def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):

if isinstance(field, serializers.SerializerMethodField):
method = getattr(field.parent, field.method_name)

if hasattr(method, "_swagger_serializer"):
# attribute added by the swagger_serializer_method decorator

if inspect.isclass(method._swagger_serializer):
serializer_kwargs = {
# copy attributes from the SerializerMethodField
"help_text": field.help_text,
"label": field.label,
# SerializerMethodField is read_only by definition
"read_only": True,
}

serializer = method._swagger_serializer(**serializer_kwargs)
else:
serializer = method._swagger_serializer

# in order of preference for help_text, use:
# 1) field.help_text from SerializerMethodField(help_text)
# 2) serializer.help_text from swagger_serializer_method(serializer)
# 3) method's docstring
if field.help_text is not None:
serializer.help_text = field.help_text
elif serializer.help_text is None:
serializer.help_text = method.__doc__

if field.label is not None:
serializer.label = field.label

return self.probe_field_inspectors(serializer, swagger_object_type, use_references, read_only=True)

elif HAS_TYPING:
# look for Python 3.5+ style type hinting of the return value
hint_class = inspect.signature(method).return_annotation

if not issubclass(hint_class, inspect._empty):
type_info = get_basic_type_info_from_hint(hint_class)

if type_info is not None:
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references,
**kwargs)
return SwaggerType(**type_info)

return NotHandled


class SimpleFieldInspector(FieldInspector):
"""Provides conversions for fields which can be described using just ``type``, ``format``, ``pattern``
and min/max validators.
Expand Down
17 changes: 17 additions & 0 deletions src/drf_yasg/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ def decorator(view_method):
return decorator


def swagger_serializer_method(serializer):
"""
Decorates the method of a serializers.SerializerMethodField
to hint as to how Swagger should be generated for this field.
:param serializer: serializer class or instance
:return:
"""

def decorator(serializer_method):
# stash the serializer for SerializerMethodFieldInspector to find
serializer_method._swagger_serializer = serializer
return serializer_method

return decorator


def is_list_view(path, method, view):
"""Check if the given path/method appears to represent a list view (as opposed to a detail/instance view).
Expand Down
70 changes: 70 additions & 0 deletions testproj/users/method_serializers_with_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import datetime
import decimal
import typing
import uuid

from rest_framework import serializers


class Unknown(object):
pass


class MethodFieldExampleSerializer(serializers.Serializer):
"""
Implementation of SerializerMethodField using type hinting for Python >= 3.5
"""

hinted_bool = serializers.SerializerMethodField(
help_text="the type hint on the method should determine this to be a bool")

def get_hinted_bool(self, obj) -> bool:
return True

hinted_int = serializers.SerializerMethodField(
help_text="the type hint on the method should determine this to be an integer")

def get_hinted_int(self, obj) -> int:
return 1

hinted_float = serializers.SerializerMethodField(
help_text="the type hint on the method should determine this to be a number")

def get_hinted_float(self, obj) -> float:
return 1.0

hinted_decimal = serializers.SerializerMethodField(
help_text="the type hint on the method should determine this to be a decimal")

def get_hinted_decimal(self, obj) -> decimal.Decimal:
return decimal.Decimal(1)

hinted_datetime = serializers.SerializerMethodField(
help_text="the type hint on the method should determine this to be a datetime")

def get_hinted_datetime(self, obj) -> datetime.datetime:
return datetime.datetime.now()

hinted_date = serializers.SerializerMethodField(
help_text="the type hint on the method should determine this to be a date")

def get_hinted_date(self, obj) -> datetime.date:
return datetime.date.today()

hinted_uuid = serializers.SerializerMethodField(
help_text="the type hint on the method should determine this to be a uuid")

def get_hinted_uuid(self, obj) -> uuid.UUID:
return uuid.uuid4()

hinted_unknown = serializers.SerializerMethodField(
help_text="type hint is unknown, so is expected to fallback to string")

def get_hinted_unknown(self, obj) -> Unknown:
return Unknown()

non_hinted_number = serializers.SerializerMethodField(
help_text="No hint on the method, so this is expected to fallback to string")

def get_non_hinted_number(self, obj):
return 1.0
Loading

0 comments on commit fcc65aa

Please sign in to comment.