Skip to content

Commit

Permalink
OpenAPI: Ported docstring operation description from CoreAPI inspecto…
Browse files Browse the repository at this point in the history
…r. (#6898)
  • Loading branch information
ysavary authored and carltongibson committed Nov 6, 2019
1 parent becb962 commit 7c3477d
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 61 deletions.
48 changes: 1 addition & 47 deletions rest_framework/schemas/coreapi.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
import re
import warnings
from collections import Counter, OrderedDict
from urllib import parse

from django.db import models
from django.utils.encoding import force_str, smart_text
from django.utils.encoding import force_str

from rest_framework import exceptions, serializers
from rest_framework.compat import coreapi, coreschema, uritemplate
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 #


def common_path(paths):
split_paths = [path.strip('/').split('/') for path in paths]
Expand Down Expand Up @@ -397,44 +389,6 @@ def get_link(self, path, method, base_url):
description=description
)

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_path_fields(self, path, method):
"""
Return a list of `coreapi.Field` instances corresponding to any
Expand Down
46 changes: 46 additions & 0 deletions rest_framework/schemas/inspectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
See schemas.__init__.py for package overview.
"""
import re
from weakref import WeakKeyDictionary

from django.utils.encoding import smart_text

from rest_framework.settings import api_settings
from rest_framework.utils import formatting


class ViewInspector:
Expand All @@ -15,6 +19,9 @@ class ViewInspector:
Provide subclass for per-view schema generation
"""

# Used in _get_description_section()
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')

def __init__(self):
self.instance_schemas = WeakKeyDictionary()

Expand Down Expand Up @@ -62,6 +69,45 @@ def view(self, value):
def view(self):
self._view = None

def get_description(self, path, method):
"""
Determine a path 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 self.header_regex.match(line):
current_section, separator, 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()


class DefaultSchema(ViewInspector):
"""Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting"""
Expand Down
3 changes: 1 addition & 2 deletions rest_framework/schemas/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
from .inspectors import ViewInspector
from .utils import get_pk_description, is_list_view

# Generator


class SchemaGenerator(BaseSchemaGenerator):

Expand Down Expand Up @@ -94,6 +92,7 @@ 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)
Expand Down
40 changes: 28 additions & 12 deletions tests/schemas/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def test_path_without_parameters(self):
method = 'GET'

view = create_view(
views.ExampleListView,
views.DocStringExampleListView,
method,
create_request(path)
)
Expand All @@ -86,7 +86,8 @@ def test_path_without_parameters(self):

operation = inspector.get_operation(path, method)
assert operation == {
'operationId': 'listExamples',
'operationId': 'listDocStringExamples',
'description': 'A description of my GET operation.',
'parameters': [],
'responses': {
'200': {
Expand All @@ -108,23 +109,38 @@ def test_path_with_id_parameter(self):
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 = '/'
Expand Down
24 changes: 24 additions & 0 deletions tests/schemas/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@ def get(self, *args, **kwargs):
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()
Expand Down

0 comments on commit 7c3477d

Please sign in to comment.