From 7e3a3a4081ad3c77769a1a5f69f40411401a6ca3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Oct 2016 15:37:25 +0100 Subject: [PATCH] Added schema descriptions --- docs/api-guide/schemas.md | 8 ++++++ rest_framework/schemas.py | 51 +++++++++++++++++++++++++++++---------- tests/test_schemas.py | 12 ++++----- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 57b73addc..868207dac 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -242,6 +242,14 @@ You could then either: --- +# Schemas as documentation + +One common usage of API schemas is to use them to build documentation pages. + +The schema generation in REST framework uses docstrings to automatically + +--- + # Alternate schema formats In order to support an alternate schema format, you need to implement a custom renderer diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index d3c9d5025..dc4d8acb7 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -1,3 +1,4 @@ +import re from collections import OrderedDict from importlib import import_module @@ -15,6 +16,8 @@ from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView +header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:') + def as_query_fields(items): """ @@ -53,8 +56,7 @@ def insert_into(target, keys, value): def is_custom_action(action): return action not in set([ - 'read', 'retrieve', 'list', - 'create', 'update', 'partial_update', 'delete', 'destroy' + 'retrieve', 'list', 'create', 'update', 'partial_update', 'destroy' ]) @@ -171,17 +173,12 @@ class EndpointInspector(object): class SchemaGenerator(object): # Map methods onto 'actions' that are the names used in the link layout. default_mapping = { - 'get': 'read', + 'get': 'retrieve', 'post': 'create', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy', } - # Coerce the following viewset actions into different names. - coerce_actions = { - 'retrieve': 'read', - 'destroy': 'delete' - } endpoint_inspector_cls = EndpointInspector def __init__(self, title=None, url=None, patterns=None, urlconf=None): @@ -283,6 +280,8 @@ class SchemaGenerator(object): else: encoding = None + description = self.get_description(path, method, view) + if self.url and path.startswith('/'): path = path[1:] @@ -290,9 +289,38 @@ class SchemaGenerator(object): url=urlparse.urljoin(self.url, path), action=method.lower(), encoding=encoding, - fields=fields + fields=fields, + description=description ) + def get_description(self, path, method, view): + """ + Determine a link description. + + This with either be the class docstring, or a specific section for it. + + For views, use method names, eg `get:` to introduce a section. + For viewsets, use action names, eg `retrieve:` to introduce a section. + + The section names will correspond to the methods on the class. + """ + view_description = view.get_view_description() + lines = [line.strip() for line in view_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] += line + '\n' + + header = getattr(view, 'action', method.lower()) + if header in sections: + return sections[header].strip() + return sections[''].strip() + def get_encoding(self, path, method, view): """ Return the 'encoding' parameter to use for a given endpoint. @@ -401,10 +429,7 @@ class SchemaGenerator(object): """ if hasattr(view, 'action'): # Viewsets have explicitly named actions. - if view.action in self.coerce_actions: - action = self.coerce_actions[view.action] - else: - action = view.action + action = view.action else: # Views have no associated action, so we determine one from the method. if is_list_view(path, method, view): diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 5794f968a..1fc7c218e 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -94,12 +94,12 @@ class TestRouterGeneratedSchema(TestCase): action='get' ), 'custom_list_action_multiple_methods': { - 'read': coreapi.Link( + 'retrieve': coreapi.Link( url='/example/custom_list_action_multiple_methods/', action='get' ) }, - 'read': coreapi.Link( + 'retrieve': coreapi.Link( url='/example/{pk}/', action='get', fields=[ @@ -138,7 +138,7 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('b', required=False, location='form') ] ), - 'read': coreapi.Link( + 'retrieve': coreapi.Link( url='/example/{pk}/', action='get', fields=[ @@ -160,7 +160,7 @@ class TestRouterGeneratedSchema(TestCase): action='get' ), 'custom_list_action_multiple_methods': { - 'read': coreapi.Link( + 'retrieve': coreapi.Link( url='/example/custom_list_action_multiple_methods/', action='get' ), @@ -189,7 +189,7 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('b', required=False, location='form') ] ), - 'delete': coreapi.Link( + 'destroy': coreapi.Link( url='/example/{pk}/', action='delete', fields=[ @@ -249,7 +249,7 @@ class TestSchemaGenerator(TestCase): action='get', fields=[] ), - 'read': coreapi.Link( + 'retrieve': coreapi.Link( url='/example/{pk}/', action='get', fields=[