Source code for drf_yasg.inspectors.field

import datetime
import inspect
import logging
import operator
import uuid
from collections import OrderedDict
from decimal import Decimal
from inspect import isclass

from django.core import validators
from django.db import models
from rest_framework import serializers
from rest_framework.settings import api_settings as rest_framework_settings

from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import decimal_as_float, filter_none, get_serializer_class, get_serializer_ref_name
from .base import FieldInspector, NotHandled, SerializerInspector

try:
    # Python>=3.5
    import typing
except ImportError:
    typing = None

logger = logging.getLogger(__name__)


class InlineSerializerInspector(SerializerInspector):
    """Provides serializer conversions using :meth:`.FieldInspector.field_to_swagger_object`."""

    #: whether to output :class:`.Schema` definitions inline or into the ``definitions`` section
    use_definitions = False

[docs] def get_schema(self, serializer): return self.probe_field_inspectors(serializer, openapi.Schema, self.use_definitions)
[docs] def add_manual_parameters(self, serializer, parameters): """Add/replace parameters from the given list of automatically generated request parameters. This method is called only when the serializer is converted into a list of parameters for use in a form data request. :param serializer: serializer instance :param list[openapi.Parameter] parameters: genereated parameters :return: modified parameters :rtype: list[openapi.Parameter] """ return parameters
[docs] def get_request_parameters(self, serializer, in_): fields = getattr(serializer, 'fields', {}) parameters = [ self.probe_field_inspectors( value, openapi.Parameter, self.use_definitions, name=self.get_parameter_name(key), in_=in_ ) for key, value in fields.items() ] return self.add_manual_parameters(serializer, parameters)
[docs] def get_property_name(self, field_name): return field_name
[docs] def get_parameter_name(self, field_name): return field_name
[docs] def get_serializer_ref_name(self, serializer): return get_serializer_ref_name(serializer)
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) if isinstance(field, (serializers.ListSerializer, serializers.ListField)): child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references) return SwaggerType( type=openapi.TYPE_ARRAY, items=child_schema, ) elif isinstance(field, serializers.Serializer): if swagger_object_type != openapi.Schema: raise SwaggerGenerationError("cannot instantiate nested serializer as " + swagger_object_type.__name__) ref_name = self.get_serializer_ref_name(field) def make_schema_definition(): properties = OrderedDict() required = [] for property_name, child in field.fields.items(): property_name = self.get_property_name(property_name) prop_kwargs = { 'read_only': bool(child.read_only) or None } prop_kwargs = filter_none(prop_kwargs) child_schema = self.probe_field_inspectors( child, ChildSwaggerType, use_references, **prop_kwargs ) properties[property_name] = child_schema if child.required and not getattr(child_schema, 'read_only', False): required.append(property_name) result = SwaggerType( type=openapi.TYPE_OBJECT, properties=properties, required=required or None, ) if not ref_name and 'title' in result: # on an inline model, the title is derived from the field name # but is visno coverually displayed like the model name, which is confusing # it is better to just remove title from inline models del result.title return result if not ref_name or not use_references: return make_schema_definition() definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS) actual_schema = definitions.setdefault(ref_name, make_schema_definition) actual_schema._remove_read_only() actual_serializer = get_serializer_class(getattr(actual_schema, '_serializer', None)) this_serializer = get_serializer_class(field) if actual_serializer and actual_serializer != this_serializer: # pragma: no cover logger.warning("Schema for %s will override distinct serializer %s because they " "share the same ref_name", actual_serializer, this_serializer) return openapi.SchemaRef(definitions, ref_name) return NotHandled
class ReferencingSerializerInspector(InlineSerializerInspector): use_definitions = True def get_queryset_field(queryset, field_name): """Try to get information about a model and model field from a queryset. :param queryset: the queryset :param field_name: 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) model_field = get_model_field(model, field_name) return model, model_field def get_model_field(model, field_name): """Try to get the given field from a django db model. :param model: the model :param field_name: target field name :return: model field or ``None`` """ try: if field_name == 'pk': return model._meta.pk else: return model._meta.get_field(field_name) except Exception: # pragma: no cover return None def get_queryset_from_view(view, serializer=None): """Try to get the queryset of the given view :param view: the view instance or class :param serializer: if given, will check that the view's get_serializer_class return matches this serialzier :return: queryset or ``None`` """ try: queryset = getattr(view, 'queryset', None) if queryset is not None and serializer is not None: # make sure the view is actually using *this* serializer assert type(serializer) == view.get_serializer_class() return queryset except Exception: # pragma: no cover return None def get_parent_serializer(field): """Get the nearest parent ``Serializer`` instance for the given field. :return: ``Serializer`` or ``None`` """ while field is not None: if isinstance(field, serializers.Serializer): return field field = field.parent return None # pragma: no cover def get_related_model(model, source): """Try to find the other side of a model relationship given the name of a related field. :param model: one side of the relationship :param str source: related field name :return: related model or ``None`` """ try: descriptor = getattr(model, source) try: return descriptor.rel.related_model except Exception: return descriptor.field.remote_field.model except Exception: # pragma: no cover return None class RelatedFieldInspector(FieldInspector): """Provides conversions for ``RelatedField``\\ s."""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) if isinstance(field, serializers.ManyRelatedField): child_schema = self.probe_field_inspectors(field.child_relation, ChildSwaggerType, use_references) return SwaggerType( type=openapi.TYPE_ARRAY, items=child_schema, unique_items=True, ) if not isinstance(field, serializers.RelatedField): return NotHandled field_queryset = getattr(field, 'queryset', None) if isinstance(field, (serializers.PrimaryKeyRelatedField, serializers.SlugRelatedField)): if getattr(field, 'pk_field', ''): # a PrimaryKeyRelatedField can have a `pk_field` attribute which is a # serializer field that will convert the PK value result = self.probe_field_inspectors(field.pk_field, swagger_object_type, use_references, **kwargs) # take the type, format, etc from `pk_field`, and the field-level information # like title, description, default from the PrimaryKeyRelatedField return SwaggerType(existing_object=result) target_field = getattr(field, 'slug_field', 'pk') if field_queryset is not None: # if the RelatedField has a queryset, try to get the related model field from there model, model_field = get_queryset_field(field_queryset, target_field) else: # if the RelatedField has no queryset (e.g. read only), try to find the target model # from the view queryset or ModelSerializer model, if present parent_serializer = get_parent_serializer(field) serializer_meta = getattr(parent_serializer, 'Meta', None) this_model = getattr(serializer_meta, 'model', None) if not this_model: view_queryset = get_queryset_from_view(self.view, parent_serializer) this_model = getattr(view_queryset, 'model', None) source = getattr(field, 'source', '') or field.field_name if not source and isinstance(field.parent, serializers.ManyRelatedField): source = field.parent.field_name model = get_related_model(this_model, source) model_field = get_model_field(model, target_field) attrs = get_basic_type_info(model_field) or {'type': openapi.TYPE_STRING} return SwaggerType(**attrs) elif isinstance(field, serializers.HyperlinkedRelatedField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI) return SwaggerType(type=openapi.TYPE_STRING)
def find_regex(regex_field): """Given a ``Field``, look for a ``RegexValidator`` and try to extract its pattern and return it as a string. :param serializers.Field regex_field: the field instance :return: the extracted pattern, or ``None`` :rtype: str """ regex_validator = None for validator in regex_field.validators: if isinstance(validator, validators.RegexValidator): if isinstance(validator, validators.URLValidator) or validator == validators.validate_ipv4_address: # skip the default url and IP regexes because they are complex and unhelpful # validate_ipv4_address is a RegexValidator instance in Django 1.11 continue if regex_validator is not None: # bail if multiple validators are found - no obvious way to choose return None # pragma: no cover regex_validator = validator # regex_validator.regex should be a compiled re object... try: pattern = getattr(getattr(regex_validator, 'regex', None), 'pattern', None) except Exception: # pragma: no cover logger.warning('failed to compile regex validator of ' + str(regex_field), exc_info=True) return None if pattern: # attempt some basic cleanup to remove regex constructs not supported by JavaScript # -- swagger uses javascript-style regexes - see https://github.com/swagger-api/swagger-editor/issues/1601 if pattern.endswith('\\Z') or pattern.endswith('\\z'): pattern = pattern[:-2] + '$' return pattern numeric_fields = (serializers.IntegerField, serializers.FloatField, serializers.DecimalField) limit_validators = [ # minimum and maximum apply to numbers (validators.MinValueValidator, numeric_fields, 'minimum', operator.__gt__), (validators.MaxValueValidator, numeric_fields, 'maximum', operator.__lt__), # minLength and maxLength apply to strings (validators.MinLengthValidator, serializers.CharField, 'min_length', operator.__gt__), (validators.MaxLengthValidator, serializers.CharField, 'max_length', operator.__lt__), # minItems and maxItems apply to lists (validators.MinLengthValidator, serializers.ListField, 'min_items', operator.__gt__), (validators.MaxLengthValidator, serializers.ListField, 'max_items', operator.__lt__), ] def find_limits(field): """Given a ``Field``, look for min/max value/length validators and return appropriate limit validation attributes. :param serializers.Field field: the field instance :return: the extracted limits :rtype: OrderedDict """ limits = {} applicable_limits = [ (validator, attr, improves) for validator, field_class, attr, improves in limit_validators 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(limit_value, limits[attr]): limits[attr] = limit_value if hasattr(field, "allow_blank") and not field.allow_blank: if limits.get('min_length', 0) < 1: limits['min_length'] = 1 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)), (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, (decimal_field_type, openapi.FORMAT_DECIMAL)), (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)), ] ip_format = {'ipv4': openapi.FORMAT_IPV4, 'ipv6': openapi.FORMAT_IPV6} serializer_field_to_basic_type = [ (serializers.EmailField, (openapi.TYPE_STRING, openapi.FORMAT_EMAIL)), (serializers.SlugField, (openapi.TYPE_STRING, openapi.FORMAT_SLUG)), (serializers.URLField, (openapi.TYPE_STRING, openapi.FORMAT_URI)), (serializers.IPAddressField, (openapi.TYPE_STRING, lambda field: ip_format.get(field.protocol, None))), (serializers.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)), (serializers.RegexField, (openapi.TYPE_STRING, None)), (serializers.CharField, (openapi.TYPE_STRING, None)), (serializers.BooleanField, (openapi.TYPE_BOOLEAN, None)), (serializers.NullBooleanField, (openapi.TYPE_BOOLEAN, None)), (serializers.IntegerField, (openapi.TYPE_INTEGER, 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)), (serializers.ModelField, (openapi.TYPE_STRING, None)), ] basic_type_info = serializer_field_to_basic_type + model_field_to_basic_type def get_basic_type_info(field): """Given a serializer or model ``Field``, return its basic type information - ``type``, ``format``, ``pattern``, and any applicable min/max limit values. :param field: the field instance :return: the extracted attributes as a dictionary, or ``None`` if the field type is not known :rtype: OrderedDict """ if field is None: return None 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 else: # pragma: no cover return None pattern = None if swagger_type == openapi.TYPE_STRING: pattern = find_regex(field) limits = find_limits(field) result = OrderedDict([ ('type', swagger_type), ('format', format), ('pattern', pattern) ]) result.update(limits) result = filter_none(result) 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 """ 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): """Provides conversion for SerializerMethodField, optionally using information from the swagger_serializer_method decorator. """
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): if not isinstance(field, serializers.SerializerMethodField): return NotHandled method = getattr(field.parent, field.method_name) if method is None: return NotHandled serializer = getattr(method, "_swagger_serializer", None) if serializer: # attribute added by the swagger_serializer_method decorator serializer = getattr(method, '_swagger_serializer', None) # in order of preference for description, use: # 1) field.help_text from SerializerMethodField(help_text) # 2) serializer.help_text from swagger_serializer_method(serializer) # 3) method's docstring description = field.help_text if description is None: description = getattr(serializer, 'help_text', None) if description is None: description = method.__doc__ label = field.label if label is None: label = getattr(serializer, 'label', None) if inspect.isclass(serializer): serializer_kwargs = { "help_text": description, "label": label, "read_only": True, } serializer = method._swagger_serializer(**serializer_kwargs) else: serializer.help_text = description serializer.label = label serializer.read_only = True return self.probe_field_inspectors(serializer, swagger_object_type, use_references, read_only=True) elif typing: # look for Python 3.5+ style type hinting of the return value hint_class = inspect.signature(method).return_annotation if not isclass(hint_class) and hasattr(hint_class, '__args__'): hint_class = hint_class.__args__[0] if isclass(hint_class) and 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. """
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): type_info = get_basic_type_info(field) if type_info is None: return NotHandled SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) return SwaggerType(**type_info)
class ChoiceFieldInspector(FieldInspector): """Provides conversions for ``ChoiceField`` and ``MultipleChoiceField``."""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) if isinstance(field, serializers.ChoiceField): enum_type = openapi.TYPE_STRING # for ModelSerializer, try to infer the type from the associated model field serializer = get_parent_serializer(field) if isinstance(serializer, serializers.ModelSerializer): model = getattr(getattr(serializer, 'Meta'), 'model') model_field = get_model_field(model, field.source) if model_field: model_type = get_basic_type_info(model_field) if model_type: enum_type = model_type.get('type', enum_type) if isinstance(field, serializers.MultipleChoiceField): return SwaggerType( type=openapi.TYPE_ARRAY, items=ChildSwaggerType( type=enum_type, enum=list(field.choices.keys()) ) ) return SwaggerType(type=enum_type, enum=list(field.choices.keys())) return NotHandled
class FileFieldInspector(FieldInspector): """Provides conversions for ``FileField``\\ s."""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) if isinstance(field, serializers.FileField): # swagger 2.0 does not support specifics about file fields, so ImageFile gets no special treatment # OpenAPI 3.0 does support it, so a future implementation could handle this better err = SwaggerGenerationError("FileField is supported only in a formData Parameter or response Schema") if swagger_object_type == openapi.Schema: # FileField.to_representation returns URL or file name result = SwaggerType(type=openapi.TYPE_STRING, read_only=True) if getattr(field, 'use_url', rest_framework_settings.UPLOADED_FILES_USE_URL): result.format = openapi.FORMAT_URI return result elif swagger_object_type == openapi.Parameter: param = SwaggerType(type=openapi.TYPE_FILE) if param['in'] != openapi.IN_FORM: raise err # pragma: no cover return param else: raise err # pragma: no cover return NotHandled
class DictFieldInspector(FieldInspector): """Provides conversion for ``DictField``."""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) if isinstance(field, serializers.DictField) and swagger_object_type == openapi.Schema: child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references) return SwaggerType( type=openapi.TYPE_OBJECT, additional_properties=child_schema ) return NotHandled
class HiddenFieldInspector(FieldInspector): """Hide ``HiddenField``."""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): if isinstance(field, serializers.HiddenField): return None return NotHandled
class StringDefaultFieldInspector(FieldInspector): """For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects."""
[docs] def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover # TODO unhandled fields: TimeField JSONField SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) return SwaggerType(type=openapi.TYPE_STRING)
try: from djangorestframework_camel_case.parser import CamelCaseJSONParser from djangorestframework_camel_case.render import CamelCaseJSONRenderer from djangorestframework_camel_case.render import camelize except ImportError: # pragma: no cover class CamelCaseJSONFilter(FieldInspector): """Converts property names to camelCase if ``djangorestframework_camel_case`` is used.""" pass else: def camelize_string(s): """Hack to force ``djangorestframework_camel_case`` to camelize a plain string.""" return next(iter(camelize({s: ''}))) def camelize_schema(schema_or_ref, components): """Recursively camelize property names for the given schema using ``djangorestframework_camel_case``.""" schema = openapi.resolve_ref(schema_or_ref, components) if getattr(schema, 'properties', {}): schema.properties = OrderedDict( (camelize_string(key), camelize_schema(val, components)) for key, val in schema.properties.items() ) if getattr(schema, 'required', []): schema.required = [camelize_string(p) for p in schema.required] return schema_or_ref class CamelCaseJSONFilter(FieldInspector): """Converts property names to camelCase if ``CamelCaseJSONParser`` or ``CamelCaseJSONRenderer`` are used."""
[docs] def is_camel_case(self): return ( any(issubclass(parser, CamelCaseJSONParser) for parser in self.view.parser_classes) or any(issubclass(renderer, CamelCaseJSONRenderer) for renderer in self.view.renderer_classes) )
[docs] def process_result(self, result, method_name, obj, **kwargs): if isinstance(result, openapi.Schema.OR_REF) and self.is_camel_case(): return camelize_schema(result, self.components) return result
try: from rest_framework_recursive.fields import RecursiveField except ImportError: # pragma: no cover class RecursiveFieldInspector(FieldInspector): """Provides conversion for RecursiveField (https://github.com/heywbj/django-rest-framework-recursive)""" pass else: class RecursiveFieldInspector(FieldInspector): """Provides conversion for RecursiveField (https://github.com/heywbj/django-rest-framework-recursive)""" def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): if isinstance(field, RecursiveField) and swagger_object_type == openapi.Schema: 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't create RecursiveField schema for inline " + str(type(field.proxied)) definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS) return openapi.SchemaRef(definitions, ref_name, ignore_unresolved=True) return NotHandled