Skip to content

Flask DocumentedBlueprint: Add to spec all views defined in the blueprint. #27

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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.'