From 2e6774d32624a9fcc9206b2777b6f32a1212c4b5 Mon Sep 17 00:00:00 2001 From: JavierLuna Date: Sat, 12 Jan 2019 10:23:38 +0100 Subject: [PATCH 1/8] Update gitignore to include .idea and .vscode rules --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 894a44c..0510c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -100,5 +100,9 @@ venv.bak/ # mkdocs documentation /site +# Editors +.idea/ +.vscode/ + # mypy .mypy_cache/ From bf161bf5b9853df6c206d8f4a3187521cbada3cc Mon Sep 17 00:00:00 2001 From: JavierLuna Date: Sat, 12 Jan 2019 11:40:10 +0100 Subject: [PATCH 2/8] Add DocumentedBlueprint to flask. Add tests. --- apispec_webframeworks/flask.py | 31 ++++- apispec_webframeworks/tests/test_ext_flask.py | 121 +++++++++++++++++- 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/apispec_webframeworks/flask.py b/apispec_webframeworks/flask.py index 6b6a435..e30a784 100644 --- a/apispec_webframeworks/flask.py +++ b/apispec_webframeworks/flask.py @@ -66,14 +66,13 @@ def post(self): 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'<(?:[^:<>]+:)?([^<>]+)>') @@ -114,3 +113,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, document=True, **options): + """If document is set to True, the route will be added to the spec. + :param bool document: Whether you want this route to be added to the spec or not. + """ + + def decorator(f): + if document 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): + """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) diff --git a/apispec_webframeworks/tests/test_ext_flask.py b/apispec_webframeworks/tests/test_ext_flask.py index e52590b..3f8e759 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,121 @@ 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', document=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', document=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' From 4bf1ac8d8d4f2d050895499e0ff02d0e68b136c2 Mon Sep 17 00:00:00 2001 From: JavierLuna Date: Sat, 12 Jan 2019 11:50:27 +0100 Subject: [PATCH 3/8] Add documentation to plugin docstring --- apispec_webframeworks/flask.py | 47 +++++++++++++++++-- apispec_webframeworks/tests/test_ext_flask.py | 4 +- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/apispec_webframeworks/flask.py b/apispec_webframeworks/flask.py index e30a784..ec9833d 100644 --- a/apispec_webframeworks/flask.py +++ b/apispec_webframeworks/flask.py @@ -61,6 +61,45 @@ 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 @@ -123,13 +162,13 @@ def __init__(self, name, import_name, spec): self.documented_view_functions = [] self.spec = spec - def route(self, rule, document=True, **options): - """If document is set to True, the route will be added to the spec. - :param bool document: Whether you want this route to be added to the spec or not. + 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. """ def decorator(f): - if document and f not in self.documented_view_functions: + if documented and f not in self.documented_view_functions: self.documented_view_functions.append(f) return super(DocumentedBlueprint, self).route(rule, **options)(f) diff --git a/apispec_webframeworks/tests/test_ext_flask.py b/apispec_webframeworks/tests/test_ext_flask.py index 3f8e759..73cc965 100644 --- a/apispec_webframeworks/tests/test_ext_flask.py +++ b/apispec_webframeworks/tests/test_ext_flask.py @@ -180,7 +180,7 @@ class TestDocumentedBlueprint: def test_document_document_true(self, app, spec): documented_blueprint = DocumentedBlueprint('test', __name__, spec) - @documented_blueprint.route('/test', document=True) + @documented_blueprint.route('/test', documented=True) def test(): return 'Hello' @@ -191,7 +191,7 @@ def test(): def test_document_document_false(self, app, spec): documented_blueprint = DocumentedBlueprint('test', __name__, spec) - @documented_blueprint.route('/test', document=False) + @documented_blueprint.route('/test', documented=False) def test(): return 'Hello' From d6794b27c1e1ddf71f83ba55bb5fff963e703e35 Mon Sep 17 00:00:00 2001 From: JavierLuna Date: Sun, 27 Jan 2019 13:13:40 +0100 Subject: [PATCH 4/8] Revert "Update gitignore to include .idea and .vscode rules" This reverts commit 2e6774d32624a9fcc9206b2777b6f32a1212c4b5. --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 0510c0b..894a44c 100644 --- a/.gitignore +++ b/.gitignore @@ -100,9 +100,5 @@ venv.bak/ # mkdocs documentation /site -# Editors -.idea/ -.vscode/ - # mypy .mypy_cache/ From 72513c0fa475c10a5ab662cfa7ddc9514c83860a Mon Sep 17 00:00:00 2001 From: JavierLuna Date: Wed, 6 Feb 2019 22:20:10 +0100 Subject: [PATCH 5/8] Add add_url_rule support. Add corresponding test --- apispec_webframeworks/flask.py | 20 ++++++++------ apispec_webframeworks/tests/test_ext_flask.py | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/apispec_webframeworks/flask.py b/apispec_webframeworks/flask.py index ec9833d..d3e2d66 100644 --- a/apispec_webframeworks/flask.py +++ b/apispec_webframeworks/flask.py @@ -103,6 +103,7 @@ def repo_detail(repo_id): """ from __future__ import absolute_import +from collections import defaultdict import re from flask import current_app, Blueprint @@ -159,7 +160,7 @@ class DocumentedBlueprint(Blueprint): def __init__(self, name, import_name, spec): super(DocumentedBlueprint, self).__init__(name, import_name) - self.documented_view_functions = [] + self.documented_view_functions = defaultdict(list) self.spec = spec def route(self, rule, documented=True, **options): @@ -167,16 +168,19 @@ def route(self, rule, documented=True, **options): :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 super(DocumentedBlueprint, self).route(rule, documented=documented, **options) - return decorator + def add_url_rule(self, rule, endpoint=None, view_func=None, documented=True, **options): + """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for + the :func:`url_for` function is prefixed with the name of the blueprint. + """ + 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 f in self.documented_view_functions: - self.spec.path(view=f) + 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 73cc965..87f09e0 100644 --- a/apispec_webframeworks/tests/test_ext_flask.py +++ b/apispec_webframeworks/tests/test_ext_flask.py @@ -285,3 +285,30 @@ def 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' From 620bc3627ce6f1f6703a9bbbe7fa5875b20bad5f Mon Sep 17 00:00:00 2001 From: JavierLuna Date: Wed, 6 Feb 2019 22:44:27 +0100 Subject: [PATCH 6/8] Add test for add_url_rule with MethodViews. --- apispec_webframeworks/tests/test_ext_flask.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/apispec_webframeworks/tests/test_ext_flask.py b/apispec_webframeworks/tests/test_ext_flask.py index 87f09e0..7d1a8b9 100644 --- a/apispec_webframeworks/tests/test_ext_flask.py +++ b/apispec_webframeworks/tests/test_ext_flask.py @@ -312,3 +312,48 @@ def index(): 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.' From fe8d44d8004ee7a8fcf8b72859f46206accc9047 Mon Sep 17 00:00:00 2001 From: JavierLuna Date: Wed, 6 Feb 2019 22:46:42 +0100 Subject: [PATCH 7/8] Add documentation to add_url_rule function --- apispec_webframeworks/flask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apispec_webframeworks/flask.py b/apispec_webframeworks/flask.py index d3e2d66..7a1ed7c 100644 --- a/apispec_webframeworks/flask.py +++ b/apispec_webframeworks/flask.py @@ -171,8 +171,8 @@ def route(self, rule, documented=True, **options): return super(DocumentedBlueprint, self).route(rule, documented=documented, **options) def add_url_rule(self, rule, endpoint=None, view_func=None, documented=True, **options): - """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for - the :func:`url_for` function is prefixed with the name of the blueprint. + """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: From d983f6c85ec6d153fb5f9ee7dd00da8e598593b6 Mon Sep 17 00:00:00 2001 From: JavierLuna Date: Wed, 6 Feb 2019 22:53:59 +0100 Subject: [PATCH 8/8] Add documentation to DocumentedBlueprint's __init__ method --- apispec_webframeworks/flask.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apispec_webframeworks/flask.py b/apispec_webframeworks/flask.py index 7a1ed7c..ebe5c41 100644 --- a/apispec_webframeworks/flask.py +++ b/apispec_webframeworks/flask.py @@ -159,6 +159,10 @@ 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