694 lines
29 KiB
Python
694 lines
29 KiB
Python
import inspect
|
|
import sys
|
|
from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar, Union
|
|
|
|
from django.utils.functional import Promise
|
|
|
|
# direct import due to https://github.com/microsoft/pyright/issues/3025
|
|
if sys.version_info >= (3, 8):
|
|
from typing import Final, Literal
|
|
else:
|
|
from typing_extensions import Final, Literal
|
|
|
|
from rest_framework.fields import Field, empty
|
|
from rest_framework.serializers import ListSerializer, Serializer
|
|
from rest_framework.settings import api_settings
|
|
|
|
from drf_spectacular.drainage import (
|
|
error, get_view_method_names, isolate_view_method, set_override, warn,
|
|
)
|
|
from drf_spectacular.types import OpenApiTypes, _KnownPythonTypes
|
|
|
|
_ListSerializerType = Union[ListSerializer, Type[ListSerializer]]
|
|
_SerializerType = Union[Serializer, Type[Serializer]]
|
|
_FieldType = Union[Field, Type[Field]]
|
|
_ParameterLocationType = Literal['query', 'path', 'header', 'cookie']
|
|
_StrOrPromise = Union[str, Promise]
|
|
_SchemaType = Dict[str, Any]
|
|
Direction = Literal['request', 'response']
|
|
|
|
|
|
class PolymorphicProxySerializer(Serializer):
|
|
"""
|
|
This class is to be used with :func:`@extend_schema <.extend_schema>` to
|
|
signal a request/response might be polymorphic (accepts/returns data
|
|
possibly from different serializers). Usage usually looks like this:
|
|
|
|
.. code-block::
|
|
|
|
@extend_schema(
|
|
request=PolymorphicProxySerializer(
|
|
component_name='MetaPerson',
|
|
serializers=[
|
|
LegalPersonSerializer, NaturalPersonSerializer,
|
|
],
|
|
resource_type_field_name='person_type',
|
|
)
|
|
)
|
|
def create(self, request, *args, **kwargs):
|
|
return Response(...)
|
|
|
|
**Beware** that this is not a real serializer and it will raise an AssertionError
|
|
if used in that way. It **cannot** be used in views as ``serializer_class``
|
|
or as field in an actual serializer. It is solely meant for annotation purposes.
|
|
|
|
Also make sure that each sub-serializer has a field named after the value of
|
|
``resource_type_field_name`` (discriminator field). Generated clients will likely
|
|
depend on the existence of this field.
|
|
|
|
Setting ``resource_type_field_name`` to ``None`` will remove the discriminator
|
|
altogether. This may be useful in certain situations, but will most likely break
|
|
client generation. Another use-case is explicit control over sub-serializer's ``many``
|
|
attribute. To explicitly control this aspect, you need disable the discriminator with
|
|
``resource_type_field_name=None`` as well as disable automatic list handling with
|
|
``many=False``.
|
|
|
|
It is **strongly** recommended to pass the ``Serializers`` as **list**,
|
|
and by that let *drf-spectacular* retrieve the field and handle the mapping
|
|
automatically. In special circumstances, the field may not available when
|
|
*drf-spectacular* processes the serializer. In those cases you can explicitly state
|
|
the mapping with ``{'legal': LegalPersonSerializer, ...}``, but it is then your
|
|
responsibility to have a valid mapping.
|
|
|
|
It is also permissible to provide a callable with no parameters for ``serializers``,
|
|
such as a lambda that will return an appropriate list or dict when evaluated.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
component_name: str,
|
|
serializers: Union[
|
|
Sequence[_SerializerType],
|
|
Dict[str, _SerializerType],
|
|
Callable[[], Sequence[_SerializerType]],
|
|
Callable[[], Dict[str, _SerializerType]]
|
|
],
|
|
resource_type_field_name: Optional[str],
|
|
many: Optional[bool] = None,
|
|
**kwargs
|
|
):
|
|
self.component_name = component_name
|
|
self.serializers = serializers
|
|
self.resource_type_field_name = resource_type_field_name
|
|
if self._many is False: # type: ignore[attr-defined]
|
|
set_override(self, 'many', False)
|
|
# retain kwargs in context for potential anonymous re-init with many=True
|
|
kwargs.setdefault('context', {}).update({
|
|
'component_name': component_name,
|
|
'serializers': serializers,
|
|
'resource_type_field_name': resource_type_field_name
|
|
})
|
|
super().__init__(**kwargs)
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
many = kwargs.pop('many', None)
|
|
if many is True:
|
|
context = kwargs.get('context', {})
|
|
for arg in ['component_name', 'serializers', 'resource_type_field_name']:
|
|
if arg in context:
|
|
kwargs[arg] = context.pop(arg) # re-apply retained args
|
|
instance = cls.many_init(*args, **kwargs)
|
|
else:
|
|
instance = super().__new__(cls, *args, **kwargs)
|
|
instance._many = many
|
|
return instance
|
|
|
|
@property
|
|
def serializers(self):
|
|
if callable(self._serializers):
|
|
self._serializers = self._serializers()
|
|
return self._serializers
|
|
|
|
@serializers.setter
|
|
def serializers(self, value):
|
|
self._serializers = value
|
|
|
|
@property
|
|
def data(self):
|
|
self._trap()
|
|
|
|
def to_internal_value(self, data):
|
|
self._trap()
|
|
|
|
def to_representation(self, instance):
|
|
self._trap()
|
|
|
|
def _trap(self):
|
|
raise AssertionError(
|
|
"PolymorphicProxySerializer is an annotation helper and not supposed to "
|
|
"be used for real requests. See documentation for correct usage."
|
|
)
|
|
|
|
|
|
class OpenApiSchemaBase:
|
|
pass
|
|
|
|
|
|
class OpenApiExample(OpenApiSchemaBase):
|
|
"""
|
|
Helper class to document a API parameter / request body / response body
|
|
with a concrete example value.
|
|
|
|
It is recommended to provide a singular example value, since pagination
|
|
and list responses are handled by drf-spectacular.
|
|
|
|
The example will be attached to the operation object where appropriate,
|
|
i.e. where the given ``media_type``, ``status_code`` and modifiers match.
|
|
Example that do not match any scenario are ignored.
|
|
|
|
- media_type will default to 'application/json' unless implicitly specified
|
|
through :class:`.OpenApiResponse`
|
|
- status_codes will default to [200, 201] unless implicitly specified
|
|
through :class:`.OpenApiResponse`
|
|
"""
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
value: Any = empty,
|
|
external_value: str = '',
|
|
summary: _StrOrPromise = '',
|
|
description: _StrOrPromise = '',
|
|
request_only: bool = False,
|
|
response_only: bool = False,
|
|
parameter_only: Optional[Tuple[str, _ParameterLocationType]] = None,
|
|
media_type: Optional[str] = None,
|
|
status_codes: Optional[Sequence[Union[str, int]]] = None,
|
|
):
|
|
self.name = name
|
|
self.summary = summary
|
|
self.description = description
|
|
self.value = value
|
|
self.external_value = external_value
|
|
self.request_only = request_only
|
|
self.response_only = response_only
|
|
self.parameter_only = parameter_only
|
|
self.media_type = media_type
|
|
self.status_codes = status_codes
|
|
|
|
|
|
class OpenApiParameter(OpenApiSchemaBase):
|
|
"""
|
|
Helper class to document request query/path/header/cookie parameters.
|
|
Can also be used to document response headers.
|
|
|
|
Please note that not all arguments apply to all ``location``/``type``/direction
|
|
variations, e.g. path parameters are ``required=True`` by definition.
|
|
|
|
For valid ``style`` choices please consult the
|
|
`OpenAPI specification <https://swagger.io/specification/#style-values>`_.
|
|
"""
|
|
QUERY: Final = 'query'
|
|
PATH: Final = 'path'
|
|
HEADER: Final = 'header'
|
|
COOKIE: Final = 'cookie'
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
type: Union[_SerializerType, _KnownPythonTypes, OpenApiTypes, _SchemaType] = str,
|
|
location: _ParameterLocationType = QUERY,
|
|
required: bool = False,
|
|
description: _StrOrPromise = '',
|
|
enum: Optional[Sequence[Any]] = None,
|
|
pattern: Optional[str] = None,
|
|
deprecated: bool = False,
|
|
style: Optional[str] = None,
|
|
explode: Optional[bool] = None,
|
|
default: Any = None,
|
|
allow_blank: bool = True,
|
|
many: Optional[bool] = None,
|
|
examples: Optional[Sequence[OpenApiExample]] = None,
|
|
extensions: Optional[Dict[str, Any]] = None,
|
|
exclude: bool = False,
|
|
response: Union[bool, Sequence[Union[int, str]]] = False,
|
|
):
|
|
self.name = name
|
|
self.type = type
|
|
self.location = location
|
|
self.required = required
|
|
self.description = description
|
|
self.enum = enum
|
|
self.pattern = pattern
|
|
self.deprecated = deprecated
|
|
self.style = style
|
|
self.explode = explode
|
|
self.default = default
|
|
self.allow_blank = allow_blank
|
|
self.many = many
|
|
self.examples = examples or []
|
|
self.extensions = extensions
|
|
self.exclude = exclude
|
|
self.response = response
|
|
|
|
|
|
class OpenApiResponse(OpenApiSchemaBase):
|
|
"""
|
|
Helper class to bundle a response object (``Serializer``, ``OpenApiType``,
|
|
raw schema, etc) together with a response object description and/or examples.
|
|
Examples can alternatively be provided via :func:`@extend_schema <.extend_schema>`.
|
|
|
|
This class is especially helpful for explicitly describing status codes on a
|
|
"Response Object" level.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
response: Any = None,
|
|
description: _StrOrPromise = '',
|
|
examples: Optional[Sequence[OpenApiExample]] = None
|
|
):
|
|
self.response = response
|
|
self.description = description
|
|
self.examples = examples or []
|
|
|
|
|
|
class OpenApiRequest(OpenApiSchemaBase):
|
|
"""
|
|
Helper class to combine a request object (``Serializer``, ``OpenApiType``,
|
|
raw schema, etc.) together with an encoding object and/or examples.
|
|
Examples can alternatively be provided via :func:`@extend_schema <.extend_schema>`.
|
|
|
|
This class is especially helpful for customizing value encoding for
|
|
``application/x-www-form-urlencoded`` and ``multipart/*``. The encoding parameter
|
|
takes a dictionary with field names as keys and encoding objects as values.
|
|
Refer to the `specification <https://swagger.io/specification/#encoding-object>`_
|
|
on how to build a valid encoding object.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
request: Any = None,
|
|
encoding: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
examples: Optional[Sequence[OpenApiExample]] = None,
|
|
):
|
|
self.request = request
|
|
self.encoding = encoding
|
|
self.examples = examples or []
|
|
|
|
|
|
F = TypeVar('F', bound=Callable[..., Any])
|
|
|
|
|
|
class OpenApiCallback(OpenApiSchemaBase):
|
|
"""
|
|
Helper class to bundle a callback definition. This specifies a view on the callee's
|
|
side, effectively stating the expectations on the receiving end. Please note that this
|
|
particular :func:`@extend_schema <.extend_schema>` instance operates from the perspective
|
|
of the callback origin, which means that ``request`` specifies the outgoing request.
|
|
|
|
For convenience sake, we assume the callback sends ``application/json`` and return a ``200``.
|
|
If that is not sufficient, you can use ``request`` and ``responses`` overloads just as you
|
|
normally would.
|
|
|
|
:param name: Name under which the this callback is listed in the schema.
|
|
:param path: Path on which the callback operation is performed. To reference request
|
|
body contents, please refer to OpenAPI specification's
|
|
`key expressions <https://swagger.io/specification/#key-expression>`_ for valid choices.
|
|
:param decorator: :func:`@extend_schema <.extend_schema>` decorator that specifies the receiving
|
|
endpoint. In this special context the allowed parameters are ``requests``, ``responses``,
|
|
``summary``, ``description``, ``deprecated``.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
name: _StrOrPromise,
|
|
path: str,
|
|
decorator: Union[Callable[[F], F], Dict[str, Callable[[F], F]], Dict[str, Any]],
|
|
):
|
|
self.name = name
|
|
self.path = path
|
|
self.decorator = decorator
|
|
|
|
|
|
class OpenApiWebhook(OpenApiSchemaBase):
|
|
"""
|
|
Helper class to document webhook definitions. A webhook specifies a possible out-of-band
|
|
request initiated by the API provider and the expected responses from the consumer.
|
|
|
|
Please note that this particular :func:`@extend_schema <.extend_schema>` instance operates
|
|
from the perspective of the webhook origin, which means that ``request`` specifies the
|
|
outgoing request.
|
|
|
|
For convenience sake, we assume the API provider sends a POST request with a body of type
|
|
``application/json`` and the receiver responds with ``200`` if the event was successfully
|
|
received.
|
|
|
|
:param name: Name under which this webhook is listed in the schema.
|
|
:param decorator: :func:`@extend_schema <.extend_schema>` decorator that specifies the receiving
|
|
endpoint. In this special context the allowed parameters are ``requests``, ``responses``,
|
|
``summary``, ``description``, ``deprecated``.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
name: _StrOrPromise,
|
|
decorator: Union[Callable[[F], F], Dict[str, Callable[[F], F]], Dict[str, Any]],
|
|
):
|
|
self.name = name
|
|
self.decorator = decorator
|
|
|
|
|
|
def extend_schema(
|
|
operation_id: Optional[str] = None,
|
|
parameters: Optional[Sequence[Union[OpenApiParameter, _SerializerType]]] = None,
|
|
request: Any = empty,
|
|
responses: Any = empty,
|
|
auth: Optional[Sequence[str]] = None,
|
|
description: Optional[_StrOrPromise] = None,
|
|
summary: Optional[_StrOrPromise] = None,
|
|
deprecated: Optional[bool] = None,
|
|
tags: Optional[Sequence[str]] = None,
|
|
filters: Optional[bool] = None,
|
|
exclude: Optional[bool] = None,
|
|
operation: Optional[_SchemaType] = None,
|
|
methods: Optional[Sequence[str]] = None,
|
|
versions: Optional[Sequence[str]] = None,
|
|
examples: Optional[Sequence[OpenApiExample]] = None,
|
|
extensions: Optional[Dict[str, Any]] = None,
|
|
callbacks: Optional[Sequence[OpenApiCallback]] = None,
|
|
external_docs: Optional[Union[Dict[str, str], str]] = None,
|
|
) -> Callable[[F], F]:
|
|
"""
|
|
Decorator mainly for the "view" method kind. Partially or completely overrides
|
|
what would be otherwise generated by drf-spectacular.
|
|
|
|
:param operation_id: replaces the auto-generated operation_id. make sure there
|
|
are no naming collisions.
|
|
:param parameters: list of additional or replacement parameters added to the
|
|
auto-discovered fields.
|
|
:param responses: replaces the discovered Serializer. Takes a variety of
|
|
inputs that can be used individually or combined
|
|
|
|
- ``Serializer`` class
|
|
- ``Serializer`` instance (e.g. ``Serializer(many=True)`` for listings)
|
|
- basic types or instances of ``OpenApiTypes``
|
|
- :class:`.OpenApiResponse` for bundling any of the other choices together with
|
|
either a dedicated response description and/or examples.
|
|
- :class:`.PolymorphicProxySerializer` for signaling that
|
|
the operation may yield data from different serializers depending
|
|
on the circumstances.
|
|
- ``dict`` with status codes as keys and one of the above as values.
|
|
Additionally in this case, it is also possible to provide a raw schema dict
|
|
as value.
|
|
- ``dict`` with tuples (status_code, media_type) as keys and one of the above
|
|
as values. Additionally in this case, it is also possible to provide a raw
|
|
schema dict as value.
|
|
:param request: replaces the discovered ``Serializer``. Takes a variety of inputs
|
|
|
|
- ``Serializer`` class/instance
|
|
- basic types or instances of ``OpenApiTypes``
|
|
- :class:`.PolymorphicProxySerializer` for signaling that the operation
|
|
accepts a set of different types of objects.
|
|
- ``dict`` with media_type as keys and one of the above as values. Additionally, in
|
|
this case, it is also possible to provide a raw schema dict as value.
|
|
:param auth: replace discovered auth with explicit list of auth methods
|
|
:param description: replaces discovered doc strings
|
|
:param summary: an optional short summary of the description
|
|
:param deprecated: mark operation as deprecated
|
|
:param tags: override default list of tags
|
|
:param filters: ignore list detection and forcefully enable/disable filter discovery
|
|
:param exclude: set True to exclude operation from schema
|
|
:param operation: manually override what auto-discovery would generate. you must
|
|
provide a OpenAPI3-compliant dictionary that gets directly translated to YAML.
|
|
:param methods: scope extend_schema to specific methods. matches all by default.
|
|
:param versions: scope extend_schema to specific API version. matches all by default.
|
|
:param examples: attach request/response examples to the operation
|
|
:param extensions: specification extensions, e.g. ``x-badges``, ``x-code-samples``, etc.
|
|
:param callbacks: associate callbacks with this endpoint
|
|
:param external_docs: Link external documentation. Provide a dict with an "url" key and
|
|
optionally a "description" key. For convenience, if only a string is given it is
|
|
treated as the URL.
|
|
:return:
|
|
"""
|
|
if methods is not None:
|
|
methods = [method.upper() for method in methods]
|
|
|
|
def decorator(f):
|
|
BaseSchema = (
|
|
# explicit manually set schema or previous view annotation
|
|
getattr(f, 'schema', None)
|
|
# previously set schema with @extend_schema on views methods
|
|
or getattr(f, 'kwargs', {}).get('schema', None)
|
|
# previously set schema with @extend_schema on @api_view
|
|
or getattr(getattr(f, 'cls', None), 'kwargs', {}).get('schema', None)
|
|
# the default
|
|
or api_settings.DEFAULT_SCHEMA_CLASS
|
|
)
|
|
|
|
if not inspect.isclass(BaseSchema):
|
|
BaseSchema = BaseSchema.__class__
|
|
|
|
def is_in_scope(ext_schema):
|
|
version, _ = ext_schema.view.determine_version(
|
|
ext_schema.view.request,
|
|
**ext_schema.view.kwargs
|
|
)
|
|
version_scope = versions is None or version in versions
|
|
method_scope = methods is None or ext_schema.method in methods
|
|
return method_scope and version_scope
|
|
|
|
class ExtendedSchema(BaseSchema):
|
|
def get_operation(self, path, path_regex, path_prefix, method, registry):
|
|
self.method = method.upper()
|
|
|
|
if operation is not None and is_in_scope(self):
|
|
return operation
|
|
return super().get_operation(path, path_regex, path_prefix, method, registry)
|
|
|
|
def is_excluded(self):
|
|
if exclude is not None and is_in_scope(self):
|
|
return exclude
|
|
return super().is_excluded()
|
|
|
|
def get_operation_id(self):
|
|
if operation_id and is_in_scope(self):
|
|
return operation_id
|
|
return super().get_operation_id()
|
|
|
|
def get_override_parameters(self):
|
|
if parameters and is_in_scope(self):
|
|
return super().get_override_parameters() + parameters
|
|
return super().get_override_parameters()
|
|
|
|
def get_auth(self):
|
|
if auth is not None and is_in_scope(self):
|
|
return auth
|
|
return super().get_auth()
|
|
|
|
def get_examples(self):
|
|
if examples and is_in_scope(self):
|
|
return super().get_examples() + examples
|
|
return super().get_examples()
|
|
|
|
def get_request_serializer(self):
|
|
if request is not empty and is_in_scope(self):
|
|
return request
|
|
return super().get_request_serializer()
|
|
|
|
def get_response_serializers(self):
|
|
if responses is not empty and is_in_scope(self):
|
|
return responses
|
|
return super().get_response_serializers()
|
|
|
|
def get_description(self):
|
|
if description and is_in_scope(self):
|
|
return description
|
|
return super().get_description()
|
|
|
|
def get_summary(self):
|
|
if summary and is_in_scope(self):
|
|
return str(summary)
|
|
return super().get_summary()
|
|
|
|
def is_deprecated(self):
|
|
if deprecated and is_in_scope(self):
|
|
return deprecated
|
|
return super().is_deprecated()
|
|
|
|
def get_tags(self):
|
|
if tags is not None and is_in_scope(self):
|
|
return tags
|
|
return super().get_tags()
|
|
|
|
def get_extensions(self):
|
|
if extensions and is_in_scope(self):
|
|
return extensions
|
|
return super().get_extensions()
|
|
|
|
def get_filter_backends(self):
|
|
if filters is not None and is_in_scope(self):
|
|
return getattr(self.view, 'filter_backends', []) if filters else []
|
|
return super().get_filter_backends()
|
|
|
|
def get_callbacks(self):
|
|
if callbacks is not None and is_in_scope(self):
|
|
return callbacks
|
|
return super().get_callbacks()
|
|
|
|
def get_external_docs(self):
|
|
if external_docs is not None and is_in_scope(self):
|
|
return external_docs
|
|
return super().get_external_docs()
|
|
|
|
if inspect.isclass(f):
|
|
# either direct decoration of views, or unpacked @api_view from OpenApiViewExtension
|
|
if operation_id is not None or operation is not None:
|
|
error(
|
|
f'using @extend_schema on viewset class {f.__name__} with parameters '
|
|
f'operation_id or operation will most likely result in a broken schema.',
|
|
delayed=f,
|
|
)
|
|
# reorder schema class MRO so that view method annotation takes precedence
|
|
# over view class annotation. only relevant if there is a method annotation
|
|
for view_method_name in get_view_method_names(view=f, schema=BaseSchema):
|
|
if 'schema' not in getattr(getattr(f, view_method_name), 'kwargs', {}):
|
|
continue
|
|
view_method = isolate_view_method(f, view_method_name)
|
|
view_method.kwargs['schema'] = type(
|
|
'ExtendedMetaSchema', (view_method.kwargs['schema'], ExtendedSchema), {}
|
|
)
|
|
# persist schema on class to provide annotation to derived view methods.
|
|
# the second purpose is to serve as base for view multi-annotation
|
|
f.schema = ExtendedSchema()
|
|
return f
|
|
elif callable(f) and hasattr(f, 'cls'):
|
|
# 'cls' attr signals that as_view() was called, which only applies to @api_view.
|
|
# keep a "unused" schema reference at root level for multi annotation convenience.
|
|
setattr(f.cls, 'kwargs', {'schema': ExtendedSchema})
|
|
# set schema on method kwargs context to emulate regular view behaviour.
|
|
for method in f.cls.http_method_names:
|
|
setattr(getattr(f.cls, method), 'kwargs', {'schema': ExtendedSchema})
|
|
return f
|
|
elif callable(f):
|
|
# custom actions have kwargs in their context, others don't. create it so our create_view
|
|
# implementation can overwrite the default schema
|
|
if not hasattr(f, 'kwargs'):
|
|
f.kwargs = {}
|
|
# this simulates what @action is actually doing. somewhere along the line in this process
|
|
# the schema is picked up from kwargs and used. it's involved my dear friends.
|
|
# use class instead of instance due to descriptor weakref reverse collisions
|
|
f.kwargs['schema'] = ExtendedSchema
|
|
return f
|
|
else:
|
|
return f
|
|
|
|
return decorator
|
|
|
|
|
|
def extend_schema_field(
|
|
field: Union[_SerializerType, _FieldType, OpenApiTypes, _SchemaType, _KnownPythonTypes],
|
|
component_name: Optional[str] = None
|
|
) -> Callable[[F], F]:
|
|
"""
|
|
Decorator for the "field" kind. Can be used with ``SerializerMethodField`` (annotate the actual
|
|
method) or with custom ``serializers.Field`` implementations.
|
|
|
|
If your custom serializer field base class is already the desired type, decoration is not necessary.
|
|
To override the discovered base class type, you can decorate your custom field class.
|
|
|
|
Always takes precedence over other mechanisms (e.g. type hints, auto-discovery).
|
|
|
|
:param field: accepts a ``Serializer``, :class:`~.types.OpenApiTypes` or raw ``dict``
|
|
:param component_name: signals that the field should be broken out as separate component
|
|
"""
|
|
|
|
def decorator(f):
|
|
set_override(f, 'field', field)
|
|
set_override(f, 'field_component_name', component_name)
|
|
return f
|
|
|
|
return decorator
|
|
|
|
|
|
def extend_schema_serializer(
|
|
many: Optional[bool] = None,
|
|
exclude_fields: Optional[Sequence[str]] = None,
|
|
deprecate_fields: Optional[Sequence[str]] = None,
|
|
examples: Optional[Sequence[OpenApiExample]] = None,
|
|
extensions: Optional[Dict[str, Any]] = None,
|
|
component_name: Optional[str] = None,
|
|
) -> Callable[[F], F]:
|
|
"""
|
|
Decorator for the "serializer" kind. Intended for overriding default serializer behaviour that
|
|
cannot be influenced through :func:`@extend_schema <.extend_schema>`.
|
|
|
|
:param many: override how serializer is initialized. Mainly used to coerce the list view detection
|
|
heuristic to acknowledge a non-list serializer.
|
|
:param exclude_fields: fields to ignore while processing the serializer. only affects the
|
|
schema. fields will still be exposed through the API.
|
|
:param deprecate_fields: fields to mark as deprecated while processing the serializer.
|
|
:param examples: define example data to serializer.
|
|
:param extensions: specification extensions, e.g. ``x-is-dynamic``, etc.
|
|
:param component_name: override default class name extraction.
|
|
"""
|
|
def decorator(klass):
|
|
if many is not None:
|
|
set_override(klass, 'many', many)
|
|
if exclude_fields:
|
|
set_override(klass, 'exclude_fields', exclude_fields)
|
|
if deprecate_fields:
|
|
set_override(klass, 'deprecate_fields', deprecate_fields)
|
|
if examples:
|
|
set_override(klass, 'examples', examples)
|
|
if extensions:
|
|
set_override(klass, 'extensions', extensions)
|
|
if component_name:
|
|
set_override(klass, 'component_name', component_name)
|
|
return klass
|
|
|
|
return decorator
|
|
|
|
|
|
def extend_schema_view(**kwargs) -> Callable[[F], F]:
|
|
"""
|
|
Convenience decorator for the "view" kind. Intended for annotating derived view methods that
|
|
are are not directly present in the view (usually methods like ``list`` or ``retrieve``).
|
|
Spares you from overriding methods like ``list``, only to perform a super call in the body
|
|
so that you have have something to attach :func:`@extend_schema <.extend_schema>` to.
|
|
|
|
This decorator also takes care of safely attaching annotations to derived view methods,
|
|
preventing leakage into unrelated views.
|
|
|
|
This decorator also supports custom DRF ``@action`` with the method name as the key.
|
|
|
|
:param kwargs: method names as argument names and :func:`@extend_schema <.extend_schema>`
|
|
calls as values
|
|
"""
|
|
def decorator(view):
|
|
# special case for @api_view. redirect decoration to enclosed WrappedAPIView
|
|
if callable(view) and hasattr(view, 'cls'):
|
|
extend_schema_view(**kwargs)(view.cls)
|
|
return view
|
|
|
|
available_view_methods = get_view_method_names(view)
|
|
|
|
for method_name, method_decorator in kwargs.items():
|
|
if method_name not in available_view_methods:
|
|
warn(
|
|
f'@extend_schema_view argument "{method_name}" was not found on view '
|
|
f'{view.__name__}. method override for "{method_name}" will be ignored.',
|
|
delayed=view
|
|
)
|
|
continue
|
|
|
|
# the context of derived methods must not be altered, as it belongs to the
|
|
# other view. create a new context so the schema can be safely stored in the
|
|
# wrapped_method. view methods that are not derived can be safely altered.
|
|
if hasattr(method_decorator, '__iter__'):
|
|
for sub_method_decorator in method_decorator:
|
|
sub_method_decorator(isolate_view_method(view, method_name))
|
|
else:
|
|
method_decorator(isolate_view_method(view, method_name))
|
|
return view
|
|
|
|
return decorator
|
|
|
|
|
|
def inline_serializer(name: str, fields: Dict[str, Field], **kwargs) -> Serializer:
|
|
"""
|
|
A helper function to create an inline serializer. Primary use is with
|
|
:func:`@extend_schema <.extend_schema>`, where one needs an implicit one-off
|
|
serializer that is not reflected in an actual class.
|
|
|
|
:param name: name of the
|
|
:param fields: dict with field names as keys and serializer fields as values
|
|
:param kwargs: optional kwargs for serializer initialization
|
|
"""
|
|
serializer_class = type(name, (Serializer,), fields)
|
|
return serializer_class(**kwargs)
|