Skip to content

Commit 7c3477d

Browse files
ysavarycarltongibson
authored andcommitted
OpenAPI: Ported docstring operation description from CoreAPI inspector. (#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy