diff --git a/drf_spectacular/openapi.py b/drf_spectacular/openapi.py index 9caf79d9..83da9093 100644 --- a/drf_spectacular/openapi.py +++ b/drf_spectacular/openapi.py @@ -1,4 +1,3 @@ -import inspect import re import typing from operator import attrgetter @@ -23,8 +22,8 @@ from drf_spectacular.plumbing import ( ComponentRegistry, ResolvedComponent, anyisinstance, append_meta, build_array_type, build_basic_type, build_choice_field, build_object_type, build_parameter_type, error, - follow_field_source, force_instance, get_override, has_override, is_basic_type, is_field, - is_serializer, resolve_regex_path_parameter, safe_ref, warn, + follow_field_source, force_instance, get_doc, get_override, has_override, is_basic_type, + is_field, is_serializer, resolve_regex_path_parameter, safe_ref, warn, ) from drf_spectacular.settings import spectacular_settings from drf_spectacular.types import OpenApiTypes @@ -171,8 +170,8 @@ def dict_helper(parameters): def get_description(self): """ override this for custom behaviour """ action_or_method = getattr(self.view, getattr(self.view, 'action', self.method.lower()), None) - view_doc = inspect.getdoc(self.view) or '' - action_doc = inspect.getdoc(action_or_method) or '' + view_doc = get_doc(self.view.__class__) + action_doc = get_doc(action_or_method) return action_doc or view_doc def get_summary(self): @@ -645,7 +644,7 @@ def _map_basic_serializer(self, serializer, direction): return build_object_type( properties=properties, required=required, - description=inspect.getdoc(serializer), + description=get_doc(serializer.__class__), ) def _map_field_validators(self, field, schema): diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index e2496adc..b337c7fa 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -14,7 +14,9 @@ from django import __version__ as DJANGO_VERSION from django.urls.resolvers import _PATH_PARAMETER_COMPONENT_RE, get_resolver # type: ignore from django.utils.module_loading import import_string -from rest_framework import exceptions, fields, serializers, versioning +from rest_framework import ( + exceptions, fields, generics, mixins, serializers, versioning, views, viewsets, +) from uritemplate import URITemplate from drf_spectacular.settings import spectacular_settings @@ -131,6 +133,40 @@ def get_override(obj, prop): return obj._spectacular_annotation[prop] +def get_doc(obj): + """ get doc string with fallback on obj's base classes (ignoring DRF documentation). """ + if not inspect.isclass(obj): + return inspect.getdoc(obj) or '' + + def safe_index(lst, item): + try: + return lst.index(item) + except ValueError: + return float("inf") + + lib_doc_excludes = [ + serializers.Serializer, + serializers.ModelSerializer, + serializers.HyperlinkedModelSerializer, + viewsets.ModelViewSet, + viewsets.GenericViewSet, + viewsets.ViewSet, + viewsets.ReadOnlyModelViewSet, + generics.GenericAPIView, + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + views.APIView, + ] + lib_barrier = min(safe_index(obj.__mro__, c) for c in lib_doc_excludes) + for cls in obj.__mro__[:lib_barrier]: + if cls.__doc__: + return inspect.cleandoc(cls.__doc__) + return '' + + def build_basic_type(obj): """ resolve either enum or actual type and yield schema template for modification diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 6eac1697..3d974905 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -35,6 +35,7 @@ class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): class XView(APIView): """ underspecified library view """ def get(self): + """ docstring for GET """ return Response(1.234) # pragma: no cover @@ -53,6 +54,7 @@ def get(self, request): schema = generate_schema('x', view=XView) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number' + assert operation['description'].strip() == 'docstring for GET' @api_view() @@ -72,3 +74,4 @@ def view_replacement(self): schema = generate_schema('x', view_function=x_view_function) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number' + assert operation['description'].strip() == 'underspecified library view'