diff --git a/apispec_webframeworks/flask.py b/apispec_webframeworks/flask.py index 6b6a435..ebe5c41 100644 --- a/apispec_webframeworks/flask.py +++ b/apispec_webframeworks/flask.py @@ -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/') + 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/', 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'<(?:[^:<>]+:)?([^<>]+)>') @@ -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): + """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): + """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] diff --git a/apispec_webframeworks/tests/test_ext_flask.py b/apispec_webframeworks/tests/test_ext_flask.py index e52590b..7d1a8b9 100644 --- a/apispec_webframeworks/tests/test_ext_flask.py +++ b/apispec_webframeworks/tests/test_ext_flask.py @@ -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 @@ -16,7 +16,7 @@ def spec(request): title='Swagger Petstore', version='1.0.0', openapi_version=request.param, - plugins=(FlaskPlugin(), ), + plugins=(FlaskPlugin(),), ) @@ -52,6 +52,7 @@ class HelloApi(MethodView): --- x-extension: global metadata """ + def get(self): """A greeting endpoint. --- @@ -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' @@ -101,6 +101,7 @@ class HelloApi(MethodView): --- x-extension: global metadata """ + def get(self): """A greeting endpoint. --- @@ -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. @@ -167,10 +167,193 @@ def hello(): assert extension == 'value' def test_path_is_translated_to_swagger_template(self, app, spec): - @app.route('/pet/') 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.'