Skip to content

Commit

Permalink
Merge pull request #72 from dougthor42/custom-docstring-ignore-tag-gh49
Browse files Browse the repository at this point in the history
Custom docstring ignore tag
  • Loading branch information
lafrech committed Sep 13, 2019
2 parents 5434725 + a965505 commit d5295d3
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 43 deletions.
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.'
)
}
)

0 comments on commit d5295d3

Please sign in to comment.