Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,9 @@ venv.bak/
# mkdocs documentation
/site

# Editors
.idea/
.vscode/

# mypy
.mypy_cache/
70 changes: 68 additions & 2 deletions apispec_webframeworks/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,57 @@ def post(self):
# 'post': {},
# 'x-extension': 'metadata'}}

Using DocumentedBlueprint:

from flask import Flask
from flask.views import MethodView

app = Flask(__name__)
documented_blueprint = DocumentedBlueprint('gistapi', __name__)

@documented_blueprint.route('/gists/<gist_id>')
def gist_detail(gist_id):
'''Gist detail view.
---
x-extension: metadata
get:
responses:
200:
schema:
$ref: '#/definitions/Gist'
'''
return 'detail for gist {}'.format(gist_id)

@documented_blueprint.route('/repos/<repo_id>', documented=False)
def repo_detail(repo_id):
'''This endpoint won't be documented
---
x-extension: metadata
get:
responses:
200:
schema:
$ref: '#/definitions/Repo'
'''
return 'detail for repo {}'.format(repo_id)

app.register_blueprint(documented_blueprint)

print(spec.to_dict()['paths'])
# {'/gists/{gist_id}': {'get': {'responses': {200: {'schema': {'$ref': '#/definitions/Gist'}}}},
# 'x-extension': 'metadata'}}

"""
from __future__ import absolute_import
import re

from flask import current_app
from flask import current_app, Blueprint
from flask.views import MethodView

from apispec.compat import iteritems
from apispec import BasePlugin, yaml_utils
from apispec.exceptions import APISpecError


# from flask-restplus
RE_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')

Expand Down Expand Up @@ -114,3 +152,31 @@ def path_helper(self, operations, view, **kwargs):
method = getattr(view.view_class, method_name)
operations[method_name] = yaml_utils.load_yaml_from_docstring(method.__doc__)
return self.flaskpath2openapi(rule.rule)


class DocumentedBlueprint(Blueprint):
"""Flask Blueprint which documents every view function defined in it."""

def __init__(self, name, import_name, spec):
super(DocumentedBlueprint, self).__init__(name, import_name)
self.documented_view_functions = []
self.spec = spec

def route(self, rule, documented=True, **options):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I overrode this function to include the documented=True kwarg, as I like declaring them explicitly.

Do you like it this way or shall I remove this?

"""If documented is set to True, the route will be added to the spec.
:param bool documented: Whether you want this route to be added to the spec or not.
"""

def decorator(f):
if documented and f not in self.documented_view_functions:
self.documented_view_functions.append(f)
return super(DocumentedBlueprint, self).route(rule, **options)(f)

return decorator

def register(self, app, options, first_registration=False):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my usercase I prefer to pass the spec in the register (or add_url_rule method) instead of the constructor.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be a solution, yeah!

app.register_blueprint(doc_blueprint, spec=spec)

I'd still add the option of passing it through the __init__ as a kwarg and add an attach(spec) method, to add variety of uses.

What do you think @tinproject ?

"""Register current blueprint in the app. Add all the view_functions to the spec."""
super(DocumentedBlueprint, self).register(app, options, first_registration=first_registration)
with app.app_context():
for f in self.documented_view_functions:
self.spec.path(view=f)
121 changes: 116 additions & 5 deletions apispec_webframeworks/tests/test_ext_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from flask.views import MethodView

from apispec import APISpec
from apispec_webframeworks.flask import FlaskPlugin
from apispec_webframeworks.flask import FlaskPlugin, DocumentedBlueprint

from .utils import get_paths

Expand All @@ -16,7 +16,7 @@ def spec(request):
title='Swagger Petstore',
version='1.0.0',
openapi_version=request.param,
plugins=(FlaskPlugin(), ),
plugins=(FlaskPlugin(),),
)


Expand Down Expand Up @@ -52,6 +52,7 @@ class HelloApi(MethodView):
---
x-extension: global metadata
"""

def get(self):
"""A greeting endpoint.
---
Expand All @@ -78,7 +79,6 @@ def post(self):
assert paths['/hi']['x-extension'] == 'global metadata'

def test_path_with_multiple_methods(self, app, spec):

@app.route('/hello', methods=['GET', 'POST'])
def hello():
return 'hi'
Expand All @@ -101,6 +101,7 @@ class HelloApi(MethodView):
---
x-extension: global metadata
"""

def get(self):
"""A greeting endpoint.
---
Expand All @@ -126,7 +127,6 @@ def delete(self):
assert 'delete' not in paths['/hi']

def test_integration_with_docstring_introspection(self, app, spec):

@app.route('/hello')
def hello():
"""A greeting endpoint.
Expand Down Expand Up @@ -167,10 +167,121 @@ def hello():
assert extension == 'value'

def test_path_is_translated_to_swagger_template(self, app, spec):

@app.route('/pet/<pet_id>')
def get_pet(pet_id):
return 'representation of pet {pet_id}'.format(pet_id=pet_id)

spec.path(view=get_pet)
assert '/pet/{pet_id}' in get_paths(spec)


class TestDocumentedBlueprint:

def test_document_document_true(self, app, spec):
documented_blueprint = DocumentedBlueprint('test', __name__, spec)

@documented_blueprint.route('/test', documented=True)
def test():
return 'Hello'

app.register_blueprint(documented_blueprint)

assert '/test' in get_paths(spec)

def test_document_document_false(self, app, spec):
documented_blueprint = DocumentedBlueprint('test', __name__, spec)

@documented_blueprint.route('/test', documented=False)
def test():
return 'Hello'

app.register_blueprint(documented_blueprint)

assert '/test' not in get_paths(spec)

def test_docstring_introspection(self, app, spec):
documented_blueprint = DocumentedBlueprint('test', __name__, spec)

@documented_blueprint.route('/test')
def test():
"""A test endpoint.
---
get:
description: Test description
responses:
200:
description: Test OK answer
"""
return 'Test'

app.register_blueprint(documented_blueprint)

paths = get_paths(spec)
assert '/test' in paths
get_op = paths['/test']['get']
assert get_op['description'] == 'Test description'

def test_docstring_introspection_multiple_routes(self, app, spec):
documented_blueprint = DocumentedBlueprint('test', __name__, spec)

@documented_blueprint.route('/test')
def test_get():
"""A test endpoint.
---
get:
description: Get test description
responses:
200:
description: Test OK answer
"""
return 'Test'

@documented_blueprint.route('/test', methods=['POST'])
def test_post():
"""A test endpoint.
---
post:
description: Post test description
responses:
200:
description: Test OK answer
"""
return 'Test'

app.register_blueprint(documented_blueprint)

paths = get_paths(spec)
assert '/test' in paths
get_op = paths['/test']['get']
post_op = paths['/test']['post']
assert get_op['description'] == 'Get test description'
assert post_op['description'] == 'Post test description'

def test_docstring_introspection_multiple_http_methods(self, app, spec):
documented_blueprint = DocumentedBlueprint('test', __name__, spec)

@documented_blueprint.route('/test', methods=['GET', 'POST'])
def test_get():
"""A test endpoint.
---
get:
description: Get test description
responses:
200:
description: Test OK answer
post:
description: Post test description
responses:
200:
description: Test OK answer
"""
return 'Test'

app.register_blueprint(documented_blueprint)

paths = get_paths(spec)
assert '/test' in paths
get_op = paths['/test']['get']
post_op = paths['/test']['post']
assert get_op['description'] == 'Get test description'
assert post_op['description'] == 'Post test description'