diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 0af7510cd..edb2503fd 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -1,3 +1,4 @@ +import re import warnings from urllib.parse import urljoin @@ -6,16 +7,22 @@ from django.core.validators import ( MinLengthValidator, MinValueValidator, RegexValidator, URLValidator ) from django.db import models -from django.utils.encoding import force_str +from django.utils.encoding import force_str, smart_text from rest_framework import exceptions, serializers from rest_framework.compat import uritemplate from rest_framework.fields import _UnvalidatedField, empty +from rest_framework.settings import api_settings +from rest_framework.utils import formatting from .generators import BaseSchemaGenerator from .inspectors import ViewInspector from .utils import get_pk_description, is_list_view +# Used in _get_description_section() +# TODO: ???: move up to base. +header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:') + # Generator @@ -87,10 +94,49 @@ class AutoSchema(ViewInspector): 'delete': 'Destroy', } + def get_description(self, path, method): + """ + Determine a link description. + + This will be based on the method docstring if one exists, + or else the class docstring. + """ + view = self.view + + method_name = getattr(view, 'action', method.lower()) + method_docstring = getattr(view, method_name, None).__doc__ + if method_docstring: + # An explicit docstring on the method or action. + return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring))) + else: + return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description()) + + def _get_description_section(self, view, header, description): + lines = [line for line in description.splitlines()] + current_section = '' + sections = {'': ''} + + for line in lines: + if header_regex.match(line): + current_section, seperator, lead = line.partition(':') + sections[current_section] = lead.strip() + else: + sections[current_section] += '\n' + line + + # TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys` + coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES + if header in sections: + return sections[header].strip() + if header in coerce_method_names: + if coerce_method_names[header] in sections: + return sections[coerce_method_names[header]].strip() + return sections[''].strip() + def get_operation(self, path, method): operation = {} operation['operationId'] = self._get_operation_id(path, method) + operation['description'] = self.get_description(path, method) parameters = [] parameters += self._get_path_parameters(path, method) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 78a5609da..0681833f8 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -71,7 +71,7 @@ class TestOperationIntrospection(TestCase): method = 'GET' view = create_view( - views.ExampleListView, + views.DocStringExampleListView, method, create_request(path) ) @@ -80,7 +80,8 @@ class TestOperationIntrospection(TestCase): operation = inspector.get_operation(path, method) assert operation == { - 'operationId': 'ListExamples', + 'operationId': 'ListDocStringExamples', + 'description': 'A description of my GET operation.', 'parameters': [], 'responses': { '200': { @@ -102,23 +103,38 @@ class TestOperationIntrospection(TestCase): method = 'GET' view = create_view( - views.ExampleDetailView, + views.DocStringExampleDetailView, method, create_request(path) ) inspector = AutoSchema() inspector.view = view - parameters = inspector._get_path_parameters(path, method) - assert parameters == [{ - 'description': '', - 'in': 'path', - 'name': 'id', - 'required': True, - 'schema': { - 'type': 'string', + operation = inspector.get_operation(path, method) + assert operation == { + 'operationId': 'RetrieveDocStringExampleDetail', + 'description': 'A description of my GET operation.', + 'parameters': [{ + 'description': '', + 'in': 'path', + 'name': 'id', + 'required': True, + 'schema': { + 'type': 'string', + }, + }], + 'responses': { + '200': { + 'description': '', + 'content': { + 'application/json': { + 'schema': { + }, + }, + }, + }, }, - }] + } def test_request_body(self): path = '/' diff --git a/tests/schemas/views.py b/tests/schemas/views.py index 273f1d30a..836a38016 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -29,6 +29,30 @@ class ExampleDetailView(APIView): pass +class DocStringExampleListView(APIView): + """ + get: A description of my GET operation. + post: A description of my POST operation. + """ + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get(self, *args, **kwargs): + pass + + def post(self, request, *args, **kwargs): + pass + + +class DocStringExampleDetailView(APIView): + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get(self, *args, **kwargs): + """ + A description of my GET operation. + """ + pass + + # Generics. class ExampleSerializer(serializers.Serializer): date = serializers.DateField()