Skip to content
78 changes: 76 additions & 2 deletions apispec_webframeworks/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,58 @@ 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
from collections import defaultdict
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 +153,38 @@ 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):
"""
Initialize blueprint. Must be provided an APISpec object.
:param APISpec spec: APISpec object which will be attached to the blueprint.
"""
super(DocumentedBlueprint, self).__init__(name, import_name)
self.documented_view_functions = defaultdict(list)
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.
"""

return super(DocumentedBlueprint, self).route(rule, documented=documented, **options)

def add_url_rule(self, rule, endpoint=None, view_func=None, documented=True, **options):
"""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.
"""
super(DocumentedBlueprint, self).add_url_rule(rule, endpoint=endpoint, view_func=view_func, **options)
if documented:
self.documented_view_functions[rule].append(view_func)

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 rule, view_functions in self.documented_view_functions.items():
[self.spec.path(view=f) for f in view_functions]
193 changes: 188 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,193 @@ 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'

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

@documented_blueprint.route('/')
def index():
"""
Gist detail view.
---
x-extension: metadata
get:
description: Get gist detail
responses:
200:
schema:
$ref: '#/definitions/Gist'
"""
return 'index'

documented_blueprint.add_url_rule('/', view_func=index, methods=['POST'])

app.register_blueprint(documented_blueprint)

paths = get_paths(spec)
assert '/' in paths
get_op = paths['/']['get']
assert get_op['description'] == 'Get gist detail'

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

class Crud(MethodView):
"""
Crud methodview.
---
x-extension: global metadata
"""

def get(self):
"""
Crud get view.
---
description: Crud get view.
responses:
200:
schema:
$ref: '#/definitions/Crud'
"""
return 'crud_get'

def post(self):
"""
Crud post view.
---
description: Crud post view.
responses:
200:
schema:
$ref: '#/definitions/Crud'
"""
return 'crud_get'

documented_blueprint.add_url_rule('/crud', view_func=Crud.as_view('crud_view'))

app.register_blueprint(documented_blueprint)

paths = get_paths(spec)
assert '/crud' in paths
get_op = paths['/crud']['get']
post_op = paths['/crud']['post']
assert get_op['description'] == 'Crud get view.'
assert post_op['description'] == 'Crud post view.'