base.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. import inspect
  2. import logging
  3. from rest_framework import serializers
  4. from .. import openapi
  5. from ..utils import force_real_str, get_field_default, get_object_classes, is_list_view
  6. #: Sentinel value that inspectors must return to signal that they do not know how to handle an object
  7. NotHandled = object()
  8. logger = logging.getLogger(__name__)
  9. def is_callable_method(cls_or_instance, method_name):
  10. method = getattr(cls_or_instance, method_name)
  11. if inspect.ismethod(method) and getattr(method, '__self__', None):
  12. # bound classmethod or instance method
  13. return method, True
  14. from inspect import getattr_static
  15. return method, isinstance(getattr_static(cls_or_instance, method_name, None), staticmethod)
  16. def call_view_method(view, method_name, fallback_attr=None, default=None):
  17. """Call a view method which might throw an exception. If an exception is thrown, log an informative error message
  18. and return the value of fallback_attr, or default if not present. The method must be callable without any arguments
  19. except cls or self.
  20. :param view: view class or instance; if a class is passed, instance methods won't be called
  21. :type view: rest_framework.views.APIView or type[rest_framework.views.APIView]
  22. :param str method_name: name of a method on the view
  23. :param str fallback_attr: name of an attribute on the view to fall back on, if calling the method fails
  24. :param default: default value if all else fails
  25. :return: view method's return value, or value of view's fallback_attr, or default
  26. :rtype: any or None
  27. """
  28. if hasattr(view, method_name):
  29. try:
  30. view_method, is_callabale = is_callable_method(view, method_name)
  31. if is_callabale:
  32. return view_method()
  33. except Exception: # pragma: no cover
  34. logger.warning("view's %s raised exception during schema generation; use "
  35. "`getattr(self, 'swagger_fake_view', False)` to detect and short-circuit this",
  36. type(view).__name__, exc_info=True)
  37. if fallback_attr and hasattr(view, fallback_attr):
  38. return getattr(view, fallback_attr)
  39. return default
  40. class BaseInspector(object):
  41. def __init__(self, view, path, method, components, request):
  42. """
  43. :param rest_framework.views.APIView view: the view associated with this endpoint
  44. :param str path: the path component of the operation URL
  45. :param str method: the http method of the operation
  46. :param openapi.ReferenceResolver components: referenceable components
  47. :param rest_framework.request.Request request: the request made against the schema view; can be None
  48. """
  49. self.view = view
  50. self.path = path
  51. self.method = method
  52. self.components = components
  53. self.request = request
  54. def process_result(self, result, method_name, obj, **kwargs):
  55. """After an inspector handles an object (i.e. returns a value other than :data:`.NotHandled`), all inspectors
  56. that were probed get the chance to alter the result, in reverse order. The inspector that handled the object
  57. is the first to receive a ``process_result`` call with the object it just returned.
  58. This behavior is similar to the Django request/response middleware processing.
  59. If this inspector has no post-processing to do, it should just ``return result`` (the default implementation).
  60. :param result: the return value of the winning inspector, or ``None`` if no inspector handled the object
  61. :param str method_name: name of the method that was called on the inspector
  62. :param obj: first argument passed to inspector method
  63. :param kwargs: additional arguments passed to inspector method
  64. :return:
  65. """
  66. return result
  67. def probe_inspectors(self, inspectors, method_name, obj, initkwargs=None, **kwargs):
  68. """Probe a list of inspectors with a given object. The first inspector in the list to return a value that
  69. is not :data:`.NotHandled` wins.
  70. :param list[type[BaseInspector]] inspectors: list of inspectors to probe
  71. :param str method_name: name of the target method on the inspector
  72. :param obj: first argument to inspector method
  73. :param dict initkwargs: extra kwargs for instantiating inspector class
  74. :param kwargs: additional arguments to inspector method
  75. :return: the return value of the winning inspector, or ``None`` if no inspector handled the object
  76. """
  77. initkwargs = initkwargs or {}
  78. tried_inspectors = []
  79. for inspector in inspectors:
  80. assert inspect.isclass(inspector), "inspector must be a class, not an object"
  81. assert issubclass(inspector, BaseInspector), "inspectors must subclass BaseInspector"
  82. inspector = inspector(self.view, self.path, self.method, self.components, self.request, **initkwargs)
  83. tried_inspectors.append(inspector)
  84. method = getattr(inspector, method_name, None)
  85. if method is None:
  86. continue
  87. result = method(obj, **kwargs)
  88. if result is not NotHandled:
  89. break
  90. else: # pragma: no cover
  91. logger.warning("%s ignored because no inspector in %s handled it (operation: %s)",
  92. obj, inspectors, method_name)
  93. result = None
  94. for inspector in reversed(tried_inspectors):
  95. result = inspector.process_result(result, method_name, obj, **kwargs)
  96. return result
  97. def get_renderer_classes(self):
  98. """Get the renderer classes of this view by calling `get_renderers`.
  99. :return: renderer classes
  100. :rtype: list[type[rest_framework.renderers.BaseRenderer]]
  101. """
  102. return get_object_classes(call_view_method(self.view, 'get_renderers', 'renderer_classes', []))
  103. def get_parser_classes(self):
  104. """Get the parser classes of this view by calling `get_parsers`.
  105. :return: parser classes
  106. :rtype: list[type[rest_framework.parsers.BaseParser]]
  107. """
  108. return get_object_classes(call_view_method(self.view, 'get_parsers', 'parser_classes', []))
  109. class PaginatorInspector(BaseInspector):
  110. """Base inspector for paginators.
  111. Responsible for determining extra query parameters and response structure added by given paginators.
  112. """
  113. def get_paginator_parameters(self, paginator):
  114. """Get the pagination parameters for a single paginator **instance**.
  115. Should return :data:`.NotHandled` if this inspector does not know how to handle the given `paginator`.
  116. :param BasePagination paginator: the paginator
  117. :rtype: list[openapi.Parameter]
  118. """
  119. return NotHandled
  120. def get_paginated_response(self, paginator, response_schema):
  121. """Add appropriate paging fields to a response :class:`.Schema`.
  122. Should return :data:`.NotHandled` if this inspector does not know how to handle the given `paginator`.
  123. :param BasePagination paginator: the paginator
  124. :param openapi.Schema response_schema: the response schema that must be paged.
  125. :rtype: openapi.Schema
  126. """
  127. return NotHandled
  128. class FilterInspector(BaseInspector):
  129. """Base inspector for filter backends.
  130. Responsible for determining extra query parameters added by given filter backends.
  131. """
  132. def get_filter_parameters(self, filter_backend):
  133. """Get the filter parameters for a single filter backend **instance**.
  134. Should return :data:`.NotHandled` if this inspector does not know how to handle the given `filter_backend`.
  135. :param BaseFilterBackend filter_backend: the filter backend
  136. :rtype: list[openapi.Parameter]
  137. """
  138. return NotHandled
  139. class FieldInspector(BaseInspector):
  140. """Base inspector for serializers and serializer fields. """
  141. def __init__(self, view, path, method, components, request, field_inspectors):
  142. super(FieldInspector, self).__init__(view, path, method, components, request)
  143. self.field_inspectors = field_inspectors
  144. def add_manual_fields(self, serializer_or_field, schema):
  145. """Set fields from the ``swagger_schema_fields`` attribute on the Meta class. This method is called
  146. only for serializers or fields that are converted into ``openapi.Schema`` objects.
  147. :param serializer_or_field: serializer or field instance
  148. :param openapi.Schema schema: the schema object to be modified in-place
  149. """
  150. meta = getattr(serializer_or_field, 'Meta', None)
  151. swagger_schema_fields = getattr(meta, 'swagger_schema_fields', {})
  152. if swagger_schema_fields:
  153. for attr, val in swagger_schema_fields.items():
  154. setattr(schema, attr, val)
  155. def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
  156. """Convert a drf Serializer or Field instance into a Swagger object.
  157. Should return :data:`.NotHandled` if this inspector does not know how to handle the given `field`.
  158. :param rest_framework.serializers.Field field: the source field
  159. :param type[openapi.SwaggerDict] swagger_object_type: should be one of Schema, Parameter, Items
  160. :param bool use_references: if False, forces all objects to be declared inline
  161. instead of by referencing other components
  162. :param kwargs: extra attributes for constructing the object;
  163. if swagger_object_type is Parameter, ``name`` and ``in_`` should be provided
  164. :return: the swagger object
  165. :rtype: openapi.Parameter or openapi.Items or openapi.Schema or openapi.SchemaRef
  166. """
  167. return NotHandled
  168. def probe_field_inspectors(self, field, swagger_object_type, use_references, **kwargs):
  169. """Helper method for recursively probing `field_inspectors` to handle a given field.
  170. All arguments are the same as :meth:`.field_to_swagger_object`.
  171. :rtype: openapi.Parameter or openapi.Items or openapi.Schema or openapi.SchemaRef
  172. """
  173. return self.probe_inspectors(
  174. self.field_inspectors, 'field_to_swagger_object', field, {'field_inspectors': self.field_inspectors},
  175. swagger_object_type=swagger_object_type, use_references=use_references, **kwargs
  176. )
  177. def _get_partial_types(self, field, swagger_object_type, use_references, **kwargs):
  178. """Helper method to extract generic information from a field and return a partial constructor for the
  179. appropriate openapi object.
  180. All arguments are the same as :meth:`.field_to_swagger_object`.
  181. The return value is a tuple consisting of:
  182. * a function for constructing objects of `swagger_object_type`; its prototype is: ::
  183. def SwaggerType(existing_object=None, **instance_kwargs):
  184. This function creates an instance of `swagger_object_type`, passing the following attributes to its init,
  185. in order of precedence:
  186. - arguments specified by the ``kwargs`` parameter of :meth:`._get_partial_types`
  187. - ``instance_kwargs`` passed to the constructor function
  188. - ``title``, ``description``, ``required``, ``x-nullable`` and ``default`` inferred from the field,
  189. where appropriate
  190. If ``existing_object`` is not ``None``, it is updated instead of creating a new object.
  191. * a type that should be used for child objects if `field` is of an array type. This can currently have two
  192. values:
  193. - :class:`.Schema` if `swagger_object_type` is :class:`.Schema`
  194. - :class:`.Items` if `swagger_object_type` is :class:`.Parameter` or :class:`.Items`
  195. :rtype: (function,type[openapi.Schema] or type[openapi.Items])
  196. """
  197. assert swagger_object_type in (openapi.Schema, openapi.Parameter, openapi.Items)
  198. assert not isinstance(field, openapi.SwaggerDict), "passed field is already a SwaggerDict object"
  199. title = force_real_str(field.label) if field.label else None
  200. title = title if swagger_object_type == openapi.Schema else None # only Schema has title
  201. help_text = getattr(field, 'help_text', None)
  202. description = force_real_str(help_text) if help_text else None
  203. description = description if swagger_object_type != openapi.Items else None # Items has no description either
  204. def SwaggerType(existing_object=None, use_field_title=True, **instance_kwargs):
  205. if 'required' not in instance_kwargs and swagger_object_type == openapi.Parameter:
  206. instance_kwargs['required'] = field.required
  207. if 'default' not in instance_kwargs and swagger_object_type != openapi.Items:
  208. default = get_field_default(field)
  209. if default not in (None, serializers.empty):
  210. instance_kwargs['default'] = default
  211. if use_field_title and instance_kwargs.get('type', None) != openapi.TYPE_ARRAY:
  212. instance_kwargs.setdefault('title', title)
  213. if description is not None:
  214. instance_kwargs.setdefault('description', description)
  215. if getattr(field, 'allow_null', None):
  216. instance_kwargs['x_nullable'] = True
  217. instance_kwargs.update(kwargs)
  218. if existing_object is not None:
  219. assert isinstance(existing_object, swagger_object_type)
  220. for key, val in sorted(instance_kwargs.items()):
  221. setattr(existing_object, key, val)
  222. result = existing_object
  223. else:
  224. result = swagger_object_type(**instance_kwargs)
  225. # Provide an option to add manual parameters to a schema
  226. # for example, to add examples
  227. if swagger_object_type == openapi.Schema:
  228. self.add_manual_fields(field, result)
  229. return result
  230. # arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
  231. child_swagger_type = openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items
  232. return SwaggerType, child_swagger_type
  233. class SerializerInspector(FieldInspector):
  234. def get_schema(self, serializer):
  235. """Convert a DRF Serializer instance to an :class:`.openapi.Schema`.
  236. Should return :data:`.NotHandled` if this inspector does not know how to handle the given `serializer`.
  237. :param serializers.BaseSerializer serializer: the ``Serializer`` instance
  238. :rtype: openapi.Schema
  239. """
  240. return NotHandled
  241. def get_request_parameters(self, serializer, in_):
  242. """Convert a DRF serializer into a list of :class:`.Parameter`\\ s.
  243. Should return :data:`.NotHandled` if this inspector does not know how to handle the given `serializer`.
  244. :param serializers.BaseSerializer serializer: the ``Serializer`` instance
  245. :param str in_: the location of the parameters, one of the `openapi.IN_*` constants
  246. :rtype: list[openapi.Parameter]
  247. """
  248. return NotHandled
  249. class ViewInspector(BaseInspector):
  250. body_methods = ('PUT', 'PATCH', 'POST', 'DELETE') #: methods that are allowed to have a request body
  251. #: methods that are assumed to require a request body determined by the view's ``serializer_class``
  252. implicit_body_methods = ('PUT', 'PATCH', 'POST')
  253. #: methods which are assumed to return a list of objects when present on non-detail endpoints
  254. implicit_list_response_methods = ('GET',)
  255. # real values set in __init__ to prevent import errors
  256. field_inspectors = [] #:
  257. filter_inspectors = [] #:
  258. paginator_inspectors = [] #:
  259. def __init__(self, view, path, method, components, request, overrides):
  260. """
  261. Inspector class responsible for providing :class:`.Operation` definitions given a view, path and method.
  262. :param dict overrides: manual overrides as passed to :func:`@swagger_auto_schema <.swagger_auto_schema>`
  263. """
  264. super(ViewInspector, self).__init__(view, path, method, components, request)
  265. self.overrides = overrides
  266. self._prepend_inspector_overrides('field_inspectors')
  267. self._prepend_inspector_overrides('filter_inspectors')
  268. self._prepend_inspector_overrides('paginator_inspectors')
  269. def _prepend_inspector_overrides(self, inspectors):
  270. extra_inspectors = self.overrides.get(inspectors, None)
  271. if extra_inspectors:
  272. default_inspectors = [insp for insp in getattr(self, inspectors) if insp not in extra_inspectors]
  273. setattr(self, inspectors, extra_inspectors + default_inspectors)
  274. def get_operation(self, operation_keys):
  275. """Get an :class:`.Operation` for the given API endpoint (path, method).
  276. This includes query, body parameters and response schemas.
  277. :param tuple[str] operation_keys: an array of keys describing the hierarchical layout of this view in the API;
  278. e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
  279. :rtype: openapi.Operation
  280. """
  281. raise NotImplementedError("ViewInspector must implement get_operation()!")
  282. def is_list_view(self):
  283. """Determine whether this view is a list or a detail view. The difference between the two is that
  284. detail views depend on a pk/id path parameter. Note that a non-detail view does not necessarily imply a list
  285. response (:meth:`.has_list_response`), nor are list responses limited to non-detail views.
  286. For example, one might have a `/topic/<pk>/posts` endpoint which is a detail view that has a list response.
  287. :rtype: bool"""
  288. return is_list_view(self.path, self.method, self.view)
  289. def has_list_response(self):
  290. """Determine whether this view returns multiple objects. By default this is any non-detail view
  291. (see :meth:`.is_list_view`) whose request method is one of :attr:`.implicit_list_response_methods`.
  292. :rtype: bool
  293. """
  294. return self.is_list_view() and (self.method.upper() in self.implicit_list_response_methods)
  295. def should_filter(self):
  296. """Determine whether filter backend parameters should be included for this request.
  297. :rtype: bool
  298. """
  299. return getattr(self.view, 'filter_backends', None) and self.has_list_response()
  300. def get_filter_parameters(self):
  301. """Return the parameters added to the view by its filter backends.
  302. :rtype: list[openapi.Parameter]
  303. """
  304. if not self.should_filter():
  305. return []
  306. fields = []
  307. for filter_backend in getattr(self.view, 'filter_backends'):
  308. fields += self.probe_inspectors(self.filter_inspectors, 'get_filter_parameters', filter_backend()) or []
  309. return fields
  310. def should_page(self):
  311. """Determine whether paging parameters and structure should be added to this operation's request and response.
  312. :rtype: bool
  313. """
  314. return getattr(self.view, 'paginator', None) and self.has_list_response()
  315. def get_pagination_parameters(self):
  316. """Return the parameters added to the view by its paginator.
  317. :rtype: list[openapi.Parameter]
  318. """
  319. if not self.should_page():
  320. return []
  321. return self.probe_inspectors(self.paginator_inspectors, 'get_paginator_parameters',
  322. getattr(self.view, 'paginator')) or []
  323. def serializer_to_schema(self, serializer):
  324. """Convert a serializer to an OpenAPI :class:`.Schema`.
  325. :param serializers.BaseSerializer serializer: the ``Serializer`` instance
  326. :returns: the converted :class:`.Schema`, or ``None`` in case of an unknown serializer
  327. :rtype: openapi.Schema or openapi.SchemaRef
  328. """
  329. return self.probe_inspectors(
  330. self.field_inspectors, 'get_schema', serializer, {'field_inspectors': self.field_inspectors}
  331. )
  332. def serializer_to_parameters(self, serializer, in_):
  333. """Convert a serializer to a possibly empty list of :class:`.Parameter`\\ s.
  334. :param serializers.BaseSerializer serializer: the ``Serializer`` instance
  335. :param str in_: the location of the parameters, one of the `openapi.IN_*` constants
  336. :rtype: list[openapi.Parameter]
  337. """
  338. return self.probe_inspectors(
  339. self.field_inspectors, 'get_request_parameters', serializer, {'field_inspectors': self.field_inspectors},
  340. in_=in_
  341. ) or []
  342. def get_paginated_response(self, response_schema):
  343. """Add appropriate paging fields to a response :class:`.Schema`.
  344. :param openapi.Schema response_schema: the response schema that must be paged.
  345. :returns: the paginated response class:`.Schema`, or ``None`` in case of an unknown pagination scheme
  346. :rtype: openapi.Schema
  347. """
  348. return self.probe_inspectors(self.paginator_inspectors, 'get_paginated_response',
  349. getattr(self.view, 'paginator'), response_schema=response_schema)