Skip to content

OpenAPI: Ported docstring operation description from CoreAPI inspector. #6898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -16,8 +16,6 @@
from .inspectors import ViewInspector
from .utils import get_pk_description, is_list_view

# Generator


class SchemaGenerator(BaseSchemaGenerator):

Expand Down Expand Up @@ -91,6 +89,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 @@ -71,7 +71,7 @@ def test_path_without_parameters(self):
method = 'GET'

view = create_view(
views.ExampleListView,
views.DocStringExampleListView,
method,
create_request(path)
)
Expand All @@ -80,7 +80,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 @@ -102,23 +103,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