Skip to content

Commit 7c3477d

Browse files
ysavarycarltongibson
authored andcommitted
OpenAPI: Ported docstring operation description from CoreAPI inspector. (encode#6898)
1 parent becb962 commit 7c3477d

File tree

5 files changed

+100
-61
lines changed

5 files changed

+100
-61
lines changed

rest_framework/schemas/coreapi.py

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
1-
import re
21
import warnings
32
from collections import Counter, OrderedDict
43
from urllib import parse
54

65
from django.db import models
7-
from django.utils.encoding import force_str, smart_text
6+
from django.utils.encoding import force_str
87

98
from rest_framework import exceptions, serializers
109
from rest_framework.compat import coreapi, coreschema, uritemplate
1110
from rest_framework.settings import api_settings
12-
from rest_framework.utils import formatting
1311

1412
from .generators import BaseSchemaGenerator
1513
from .inspectors import ViewInspector
1614
from .utils import get_pk_description, is_list_view
1715

18-
# Used in _get_description_section()
19-
# TODO: ???: move up to base.
20-
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
21-
22-
# Generator #
23-
2416

2517
def common_path(paths):
2618
split_paths = [path.strip('/').split('/') for path in paths]
@@ -397,44 +389,6 @@ def get_link(self, path, method, base_url):
397389
description=description
398390
)
399391

400-
def get_description(self, path, method):
401-
"""
402-
Determine a link description.
403-
404-
This will be based on the method docstring if one exists,
405-
or else the class docstring.
406-
"""
407-
view = self.view
408-
409-
method_name = getattr(view, 'action', method.lower())
410-
method_docstring = getattr(view, method_name, None).__doc__
411-
if method_docstring:
412-
# An explicit docstring on the method or action.
413-
return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring)))
414-
else:
415-
return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description())
416-
417-
def _get_description_section(self, view, header, description):
418-
lines = [line for line in description.splitlines()]
419-
current_section = ''
420-
sections = {'': ''}
421-
422-
for line in lines:
423-
if header_regex.match(line):
424-
current_section, seperator, lead = line.partition(':')
425-
sections[current_section] = lead.strip()
426-
else:
427-
sections[current_section] += '\n' + line
428-
429-
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
430-
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
431-
if header in sections:
432-
return sections[header].strip()
433-
if header in coerce_method_names:
434-
if coerce_method_names[header] in sections:
435-
return sections[coerce_method_names[header]].strip()
436-
return sections[''].strip()
437-
438392
def get_path_fields(self, path, method):
439393
"""
440394
Return a list of `coreapi.Field` instances corresponding to any

rest_framework/schemas/inspectors.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
44
See schemas.__init__.py for package overview.
55
"""
6+
import re
67
from weakref import WeakKeyDictionary
78

9+
from django.utils.encoding import smart_text
10+
811
from rest_framework.settings import api_settings
12+
from rest_framework.utils import formatting
913

1014

1115
class ViewInspector:
@@ -15,6 +19,9 @@ class ViewInspector:
1519
Provide subclass for per-view schema generation
1620
"""
1721

22+
# Used in _get_description_section()
23+
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
24+
1825
def __init__(self):
1926
self.instance_schemas = WeakKeyDictionary()
2027

@@ -62,6 +69,45 @@ def view(self, value):
6269
def view(self):
6370
self._view = None
6471

72+
def get_description(self, path, method):
73+
"""
74+
Determine a path description.
75+
76+
This will be based on the method docstring if one exists,
77+
or else the class docstring.
78+
"""
79+
view = self.view
80+
81+
method_name = getattr(view, 'action', method.lower())
82+
method_docstring = getattr(view, method_name, None).__doc__
83+
if method_docstring:
84+
# An explicit docstring on the method or action.
85+
return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring)))
86+
else:
87+
return self._get_description_section(view, getattr(view, 'action', method.lower()),
88+
view.get_view_description())
89+
90+
def _get_description_section(self, view, header, description):
91+
lines = [line for line in description.splitlines()]
92+
current_section = ''
93+
sections = {'': ''}
94+
95+
for line in lines:
96+
if self.header_regex.match(line):
97+
current_section, separator, lead = line.partition(':')
98+
sections[current_section] = lead.strip()
99+
else:
100+
sections[current_section] += '\n' + line
101+
102+
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
103+
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
104+
if header in sections:
105+
return sections[header].strip()
106+
if header in coerce_method_names:
107+
if coerce_method_names[header] in sections:
108+
return sections[coerce_method_names[header]].strip()
109+
return sections[''].strip()
110+
65111

66112
class DefaultSchema(ViewInspector):
67113
"""Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting"""

rest_framework/schemas/openapi.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
from .inspectors import ViewInspector
1818
from .utils import get_pk_description, is_list_view
1919

20-
# Generator
21-
2220

2321
class SchemaGenerator(BaseSchemaGenerator):
2422

@@ -94,6 +92,7 @@ def get_operation(self, path, method):
9492
operation = {}
9593

9694
operation['operationId'] = self._get_operation_id(path, method)
95+
operation['description'] = self.get_description(path, method)
9796

9897
parameters = []
9998
parameters += self._get_path_parameters(path, method)

tests/schemas/test_openapi.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_path_without_parameters(self):
7777
method = 'GET'
7878

7979
view = create_view(
80-
views.ExampleListView,
80+
views.DocStringExampleListView,
8181
method,
8282
create_request(path)
8383
)
@@ -86,7 +86,8 @@ def test_path_without_parameters(self):
8686

8787
operation = inspector.get_operation(path, method)
8888
assert operation == {
89-
'operationId': 'listExamples',
89+
'operationId': 'listDocStringExamples',
90+
'description': 'A description of my GET operation.',
9091
'parameters': [],
9192
'responses': {
9293
'200': {
@@ -108,23 +109,38 @@ def test_path_with_id_parameter(self):
108109
method = 'GET'
109110

110111
view = create_view(
111-
views.ExampleDetailView,
112+
views.DocStringExampleDetailView,
112113
method,
113114
create_request(path)
114115
)
115116
inspector = AutoSchema()
116117
inspector.view = view
117118

118-
parameters = inspector._get_path_parameters(path, method)
119-
assert parameters == [{
120-
'description': '',
121-
'in': 'path',
122-
'name': 'id',
123-
'required': True,
124-
'schema': {
125-
'type': 'string',
119+
operation = inspector.get_operation(path, method)
120+
assert operation == {
121+
'operationId': 'RetrieveDocStringExampleDetail',
122+
'description': 'A description of my GET operation.',
123+
'parameters': [{
124+
'description': '',
125+
'in': 'path',
126+
'name': 'id',
127+
'required': True,
128+
'schema': {
129+
'type': 'string',
130+
},
131+
}],
132+
'responses': {
133+
'200': {
134+
'description': '',
135+
'content': {
136+
'application/json': {
137+
'schema': {
138+
},
139+
},
140+
},
141+
},
126142
},
127-
}]
143+
}
128144

129145
def test_request_body(self):
130146
path = '/'

tests/schemas/views.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ def get(self, *args, **kwargs):
2929
pass
3030

3131

32+
class DocStringExampleListView(APIView):
33+
"""
34+
get: A description of my GET operation.
35+
post: A description of my POST operation.
36+
"""
37+
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
38+
39+
def get(self, *args, **kwargs):
40+
pass
41+
42+
def post(self, request, *args, **kwargs):
43+
pass
44+
45+
46+
class DocStringExampleDetailView(APIView):
47+
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
48+
49+
def get(self, *args, **kwargs):
50+
"""
51+
A description of my GET operation.
52+
"""
53+
pass
54+
55+
3256
# Generics.
3357
class ExampleSerializer(serializers.Serializer):
3458
date = serializers.DateField()

0 commit comments

Comments
 (0)