Skip to content
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

Custom docstring ignore tag #72

Merged
merged 4 commits into from
Sep 13, 2019
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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Contributors (chronological)
============================

- Ryan Yin `@ryan4yin <https://github.com/ryan4yin>`_
- Douglas Thor `@dougthor42 <https://github.com/dougthor42>`_
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Features:
reasonable default content type for ``form`` and ``files`` (:pr:`83`).
- Add ``description`` parameter to ``Blueprint.arguments`` to pass description
for ``requestBody`` (:pr:`93`).
- Allow customization of docstring delimiter string (:issue:`49`).

Bug fixes:

Expand Down
5 changes: 5 additions & 0 deletions docs/openapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ The example above produces the following documentation attributes:
}
}

The delimiter line is the line starting with the delimiter string defined in
``Blueprint.DOCSTRING_INFO_DELIMITER``. This string defaults to ``"---"`` and
can be customized in a subclass. ``None`` means "no delimiter": the whole
docstring is included in the docs.

Document Operations Parameters and Responses
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
16 changes: 11 additions & 5 deletions flask_rest_api/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class Blueprint(
"files": "multipart/form-data",
}

DOCSTRING_INFO_DELIMITER = "---"

def __init__(self, *args, **kwargs):

self.description = kwargs.pop('description', '')
Expand Down Expand Up @@ -135,12 +137,16 @@ def _store_endpoint_docs(self, endpoint, obj, parameters, **options):

def store_method_docs(method, function):
"""Add auto and manual doc to table for later registration"""
# Get summary/description from docstring
# and auto documentation from decorators
# Get auto documentation from decorators
# and summary/description from docstring
# Get manual documentation from @doc decorator
docstring = function.__doc__
auto_doc = load_info_from_docstring(docstring) if docstring else {}
auto_doc.update(getattr(function, '_apidoc', {}))
auto_doc = getattr(function, '_apidoc', {})
auto_doc.update(
load_info_from_docstring(
function.__doc__,
delimiter=self.DOCSTRING_INFO_DELIMITER
)
)
manual_doc = getattr(function, '_api_manual_doc', {})
# Store function auto and manual docs for later registration
method_l = method.lower()
Expand Down
35 changes: 20 additions & 15 deletions flask_rest_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,33 @@ def get_appcontext():
return ctx.flask_rest_api


def load_info_from_docstring(docstring):
"""Load summary and description from docstring"""
split_lines = trim_docstring(docstring).split('\n')
def load_info_from_docstring(docstring, *, delimiter="---"):
"""Load summary and description from docstring

# Info is separated from rest of docstring by a '---' line
for index, line in enumerate(split_lines):
if line.lstrip().startswith('---'):
cut_at = index
break
else:
cut_at = index + 1
:param str delimiter: Summary and description information delimiter.
If a line starts with this string, this line and the lines after are
ignored. Defaults to "---".
"""
split_lines = trim_docstring(docstring).split('\n')

split_info_lines = split_lines[:cut_at]
if delimiter is not None:
# Info is separated from rest of docstring by a `delimiter` line
for index, line in enumerate(split_lines):
if line.lstrip().startswith(delimiter):
cut_at = index
break
else:
cut_at = index + 1
split_lines = split_lines[:cut_at]

# Description is separated from summary by an empty line
for index, line in enumerate(split_info_lines):
for index, line in enumerate(split_lines):
if line.strip() == '':
summary_lines = split_info_lines[:index]
description_lines = split_info_lines[index + 1:]
summary_lines = split_lines[:index]
description_lines = split_lines[index + 1:]
break
else:
summary_lines = split_info_lines
summary_lines = split_lines
description_lines = []

info = {}
Expand Down
45 changes: 26 additions & 19 deletions tests/test_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,43 +676,50 @@ def get(self):
assert resp['description'] == 'Description'
assert 'schema' in resp['content']['application/json']

def test_blueprint_doc_info_from_docstring(self, app):
@pytest.mark.parametrize('delimiter', (False, None, "---"))
def test_blueprint_doc_info_from_docstring(self, app, delimiter):
api = Api(app)
blp = Blueprint('test', __name__, url_prefix='/test')

class MyBlueprint(Blueprint):
# Check delimiter default value
if delimiter is not False:
DOCSTRING_INFO_DELIMITER = delimiter

blp = MyBlueprint('test', __name__, url_prefix='/test')

@blp.route('/')
class Resource(MethodView):

def get(self):
"""Docstring get summary"""
"""Get summary"""

def put(self):
"""Docstring put summary
"""Put summary

Docstring put description
Put description
---
Private docstring
"""

@blp.doc(
summary='Decorator patch summary',
description='Decorator patch description'
)
def patch(self):
"""Docstring patch summary

Docstring patch description
"""
pass

api.register_blueprint(blp)
spec = api.spec.to_dict()
path = spec['paths']['/test/']

assert path['get']['summary'] == 'Docstring get summary'
assert path['get']['summary'] == 'Get summary'
assert 'description' not in path['get']
assert path['put']['summary'] == 'Docstring put summary'
assert path['put']['description'] == 'Docstring put description'
# @doc decorator overrides docstring
assert path['patch']['summary'] == 'Decorator patch summary'
assert path['patch']['description'] == 'Decorator patch description'
assert path['put']['summary'] == 'Put summary'
if delimiter is None:
assert (
path['put']['description'] ==
'Put description\n---\nPrivate docstring'
)
else:
assert path['put']['description'] == 'Put description'
assert 'summary' not in path['patch']
assert 'description' not in path['patch']

@pytest.mark.parametrize('http_methods', (
['OPTIONS', 'HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
Expand Down
25 changes: 21 additions & 4 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def test_deepupdate(self):
}

def test_load_info_from_docstring(self):
assert (load_info_from_docstring(None)) == {}
assert (load_info_from_docstring(None, delimiter="---")) == {}
assert (load_info_from_docstring('')) == {}

docstring = """
Expand All @@ -51,7 +53,22 @@ def test_load_info_from_docstring(self):
---
Ignore this.
"""
assert load_info_from_docstring(docstring) == {
'summary': 'Summary\nTwo-line summary is possible.',
'description': 'Long description\nReally long description'
}
assert (
load_info_from_docstring(docstring) ==
load_info_from_docstring(docstring, delimiter="---") ==
{
'summary': 'Summary\nTwo-line summary is possible.',
'description': 'Long description\nReally long description',
}
)
assert (
load_info_from_docstring(docstring, delimiter=None) ==
load_info_from_docstring(docstring, delimiter="~~~") ==
{
'summary': 'Summary\nTwo-line summary is possible.',
'description': (
'Long description\nReally long description\n---\n'
'Ignore this.'
)
}
)