From c20d02a5aab41b01b270d08beaf396a27c11a761 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Tue, 7 Nov 2017 17:42:52 -0800 Subject: [PATCH 01/16] flask-classful view support --- flask_apispec/apidoc.py | 40 ++++++++++++++++++++++++++++++++++++++ flask_apispec/extension.py | 6 +++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/flask_apispec/apidoc.py b/flask_apispec/apidoc.py index 88f1ede..7f52356 100644 --- a/flask_apispec/apidoc.py +++ b/flask_apispec/apidoc.py @@ -8,6 +8,7 @@ import apispec from apispec.core import VALID_METHODS from apispec.ext.marshmallow import swagger +from flask_classful import FlaskView, get_interesting_members from marshmallow import Schema from marshmallow.utils import is_instance_or_subclass @@ -99,3 +100,42 @@ def get_operations(self, rule, resource): def get_parent(self, resource, **kwargs): return resolve_instance(resource, **kwargs) + + +class ClassfulConverter(Converter): + + def convert(self, target, methods): + # endpoint = endpoint or target.__name__.lower() + # if blueprint: + # endpoint = '{}.{}'.format(blueprint, endpoint) + # endpoint_prefix = target.__name__ + + paths = list() + + for method in methods: + endpoint = method['endpoint'] + target = method['target'] + rules = self.app.url_map._rules_by_endpoint[endpoint] + for rule in rules: + print(f"METHOD: {method} rule: {rule}") + paths.append(self.get_path(rule, target)) + + return paths + # return [self.get_path(method['endpoint'], method['target']) for method in methods] + + + # for member in get_interesting_members(FlaskView, target): + # print(f"target: {target} member: {member} method: {method}") + # rules = self.app.url_map._rules_by_endpoint[endpoint] + # return [self.get_path(rule, target, **kwargs) for rule in rules] + + def get_operations(self, rule, resource): + return { + method: getattr(resource, method.lower()) + for method in rule.methods + if hasattr(resource, method.lower()) + } + + def get_parent(self, resource, **kwargs): + print(f"get_parent resource: {resource}") + return resolve_instance(resource, **kwargs) diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index 3d90f0a..a504fe9 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -3,9 +3,10 @@ import functools import types from apispec import APISpec +from flask_classful import FlaskView from flask_apispec import ResourceMeta -from flask_apispec.apidoc import ViewConverter, ResourceConverter +from flask_apispec.apidoc import ViewConverter, ResourceConverter, ClassfulConverter class FlaskApiSpec(object): @@ -50,6 +51,7 @@ def init_app(self, app): self.app = app self.view_converter = ViewConverter(self.app) self.resource_converter = ResourceConverter(self.app) + self.classful_converter = ClassfulConverter(self.app) self.spec = self.app.config.get('APISPEC_SPEC') or \ make_apispec(self.app.config.get('APISPEC_TITLE', 'flask-apispec'), self.app.config.get('APISPEC_VERSION', 'v1')) @@ -132,6 +134,8 @@ def _register(self, target, endpoint=None, blueprint=None, """ if isinstance(target, types.FunctionType): paths = self.view_converter.convert(target, endpoint, blueprint) + elif issubclass(target, FlaskView): + paths = self.classful_converter.convert(target, endpoint) elif isinstance(target, ResourceMeta): paths = self.resource_converter.convert( target, From fcaaef12c2f9a9d8a615d182ccbe602032180e14 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Wed, 8 Nov 2017 19:02:12 -0800 Subject: [PATCH 02/16] build apispec for classful views --- flask_apispec/apidoc.py | 46 +++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/flask_apispec/apidoc.py b/flask_apispec/apidoc.py index 7f52356..3e26b84 100644 --- a/flask_apispec/apidoc.py +++ b/flask_apispec/apidoc.py @@ -8,13 +8,13 @@ import apispec from apispec.core import VALID_METHODS from apispec.ext.marshmallow import swagger -from flask_classful import FlaskView, get_interesting_members from marshmallow import Schema from marshmallow.utils import is_instance_or_subclass from flask_apispec.paths import rule_to_path, rule_to_params from flask_apispec.utils import resolve_instance, resolve_annotations, merge_recursive +import inspect class Converter(object): @@ -51,9 +51,15 @@ def get_operation(self, rule, view, parent=None): 'responses': self.get_responses(view, parent), 'parameters': self.get_parameters(rule, view, docs, parent), } + description = self.get_description(view) + if description: + operation['description'] = description docs.pop('params', None) return merge_recursive([operation, docs]) + def get_description(self, view): + return None + def get_parent(self, view): return None @@ -104,38 +110,28 @@ def get_parent(self, resource, **kwargs): class ClassfulConverter(Converter): - def convert(self, target, methods): - # endpoint = endpoint or target.__name__.lower() - # if blueprint: - # endpoint = '{}.{}'.format(blueprint, endpoint) - # endpoint_prefix = target.__name__ - + def convert(self, resource, endpoints): paths = list() - - for method in methods: - endpoint = method['endpoint'] - target = method['target'] - rules = self.app.url_map._rules_by_endpoint[endpoint] + for endpoint in endpoints: + route = endpoint['route'] + rule = endpoint['rule'] + rules = self.app.url_map._rules_by_endpoint[route] for rule in rules: - print(f"METHOD: {method} rule: {rule}") - paths.append(self.get_path(rule, target)) + paths.append(self.get_path(rule, endpoint)) return paths - # return [self.get_path(method['endpoint'], method['target']) for method in methods] - - # for member in get_interesting_members(FlaskView, target): - # print(f"target: {target} member: {member} method: {method}") - # rules = self.app.url_map._rules_by_endpoint[endpoint] - # return [self.get_path(rule, target, **kwargs) for rule in rules] + def get_description(self, view): + return inspect.getdoc(view) - def get_operations(self, rule, resource): + def get_operations(self, rule, endpoint): + # remove OPTIONS (its for CORS) + methods = set(rule.methods) - {'OPTIONS'} return { - method: getattr(resource, method.lower()) - for method in rule.methods - if hasattr(resource, method.lower()) + method: endpoint['view_func'] + for method in methods } def get_parent(self, resource, **kwargs): - print(f"get_parent resource: {resource}") + # print(f"get_parent resource: {resource}") return resolve_instance(resource, **kwargs) From 171bd9f23e5c0852ca919c65efebe1d5c6ee369a Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Wed, 8 Nov 2017 19:05:46 -0800 Subject: [PATCH 03/16] auto-register flask-classful view defs on app init --- flask_apispec/extension.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index a504fe9..4662084 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -58,6 +58,11 @@ def init_app(self, app): self.add_swagger_routes() + # register flask-classful views + if hasattr(app, '_classful_resource_views'): + for view_class, endpoints in app._classful_resource_views.items(): + self._register(view_class, endpoints) + for deferred in self._deferred: deferred() From e62378e292404baf947a8df503cd9ec233200c3a Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Wed, 8 Nov 2017 23:03:13 -0800 Subject: [PATCH 04/16] fix parent for classful to make responses refs work --- flask_apispec/apidoc.py | 7 +++---- flask_apispec/utils.py | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/flask_apispec/apidoc.py b/flask_apispec/apidoc.py index 3e26b84..b1dd04a 100644 --- a/flask_apispec/apidoc.py +++ b/flask_apispec/apidoc.py @@ -117,7 +117,7 @@ def convert(self, resource, endpoints): rule = endpoint['rule'] rules = self.app.url_map._rules_by_endpoint[route] for rule in rules: - paths.append(self.get_path(rule, endpoint)) + paths.append(self.get_path(rule, endpoint, classful_endpoint=endpoint)) return paths @@ -132,6 +132,5 @@ def get_operations(self, rule, endpoint): for method in methods } - def get_parent(self, resource, **kwargs): - # print(f"get_parent resource: {resource}") - return resolve_instance(resource, **kwargs) + def get_parent(self, resource, classful_endpoint, **kwargs): + return classful_endpoint['target'] diff --git a/flask_apispec/utils.py b/flask_apispec/utils.py index 5cbac4e..8d06516 100644 --- a/flask_apispec/utils.py +++ b/flask_apispec/utils.py @@ -17,6 +17,9 @@ class Ref(object): def __init__(self, key): self.key = key + def __repr__(self): + return "".format(self.key) + def resolve(self, obj): return getattr(obj, self.key, None) From c68355ab9c8d08126e97e9812c970d57abd3103e Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 00:27:39 -0800 Subject: [PATCH 05/16] simplifying classful auto-registration. now runs in register_existing_resources, allowing additional paths to be registered at runtime. --- flask_apispec/apidoc.py | 13 ++++++------- flask_apispec/extension.py | 11 +++++------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/flask_apispec/apidoc.py b/flask_apispec/apidoc.py index b1dd04a..165a751 100644 --- a/flask_apispec/apidoc.py +++ b/flask_apispec/apidoc.py @@ -110,14 +110,13 @@ def get_parent(self, resource, **kwargs): class ClassfulConverter(Converter): - def convert(self, resource, endpoints): + def convert(self, resource, endpoint): paths = list() - for endpoint in endpoints: - route = endpoint['route'] - rule = endpoint['rule'] - rules = self.app.url_map._rules_by_endpoint[route] - for rule in rules: - paths.append(self.get_path(rule, endpoint, classful_endpoint=endpoint)) + route = endpoint['route'] + rule = endpoint['rule'] + rules = self.app.url_map._rules_by_endpoint[route] + for rule in rules: + paths.append(self.get_path(rule, endpoint, classful_endpoint=endpoint)) return paths diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index 4662084..2b5bed4 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -58,11 +58,6 @@ def init_app(self, app): self.add_swagger_routes() - # register flask-classful views - if hasattr(app, '_classful_resource_views'): - for view_class, endpoints in app._classful_resource_views.items(): - self._register(view_class, endpoints) - for deferred in self._deferred: deferred() @@ -105,7 +100,11 @@ def register_existing_resources(self): blueprint_name = None try: - self.register(rule, blueprint=blueprint_name) + if hasattr(rule, '_classful_meta'): + _classful_meta = getattr(rule, '_classful_meta') + self._register(_classful_meta['target'], _classful_meta, blueprint=blueprint_name) + else: + self.register(rule, blueprint=blueprint_name) except TypeError: pass From c65b198ed1033cbb04e6fe7f0fa1c3e777f9719b Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 01:06:11 -0800 Subject: [PATCH 06/16] lint --- flask_apispec/extension.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index 2b5bed4..baa650d 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -102,7 +102,11 @@ def register_existing_resources(self): try: if hasattr(rule, '_classful_meta'): _classful_meta = getattr(rule, '_classful_meta') - self._register(_classful_meta['target'], _classful_meta, blueprint=blueprint_name) + self._register( + _classful_meta['target'], + _classful_meta, + blueprint=blueprint_name, + ) else: self.register(rule, blueprint=blueprint_name) except TypeError: From 6fb804d87a7ecf37d3eb68607a40ddf594caa557 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 16:33:43 -0800 Subject: [PATCH 07/16] well this shouldn't be here --- flask_apispec/extension.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index baa650d..9a8d8ae 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -3,7 +3,6 @@ import functools import types from apispec import APISpec -from flask_classful import FlaskView from flask_apispec import ResourceMeta from flask_apispec.apidoc import ViewConverter, ResourceConverter, ClassfulConverter From fb46fdf023898d03f1aa2c8f8d28349fe6279520 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 16:48:23 -0800 Subject: [PATCH 08/16] removing flask_classy dep --- flask_apispec/apidoc.py | 12 ++++++------ flask_apispec/extension.py | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/flask_apispec/apidoc.py b/flask_apispec/apidoc.py index 165a751..6a7d416 100644 --- a/flask_apispec/apidoc.py +++ b/flask_apispec/apidoc.py @@ -110,13 +110,13 @@ def get_parent(self, resource, **kwargs): class ClassfulConverter(Converter): - def convert(self, resource, endpoint): + def convert(self, classful_meta): paths = list() - route = endpoint['route'] - rule = endpoint['rule'] + route = classful_meta['route'] + rule = classful_meta['rule'] rules = self.app.url_map._rules_by_endpoint[route] for rule in rules: - paths.append(self.get_path(rule, endpoint, classful_endpoint=endpoint)) + paths.append(self.get_path(rule, classful_meta, classful_meta=classful_meta)) return paths @@ -131,5 +131,5 @@ def get_operations(self, rule, endpoint): for method in methods } - def get_parent(self, resource, classful_endpoint, **kwargs): - return classful_endpoint['target'] + def get_parent(self, resource, classful_meta, **kwargs): + return classful_meta['target'] diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index 9a8d8ae..d426bee 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -100,10 +100,8 @@ def register_existing_resources(self): try: if hasattr(rule, '_classful_meta'): - _classful_meta = getattr(rule, '_classful_meta') self._register( - _classful_meta['target'], - _classful_meta, + rule, blueprint=blueprint_name, ) else: @@ -139,10 +137,11 @@ def _register(self, target, endpoint=None, blueprint=None, :param dict resource_class_kwargs: (optional) kwargs to be forwarded to the view class constructor. """ - if isinstance(target, types.FunctionType): + if hasattr(target, '_classful_meta'): + classful_meta = getattr(target, '_classful_meta') + paths = self.classful_converter.convert(classful_meta) + elif isinstance(target, types.FunctionType): paths = self.view_converter.convert(target, endpoint, blueprint) - elif issubclass(target, FlaskView): - paths = self.classful_converter.convert(target, endpoint) elif isinstance(target, ResourceMeta): paths = self.resource_converter.convert( target, From 5dc7f3fb52b5162023d11540dfa2cd5ed31d05b9 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 17:32:41 -0800 Subject: [PATCH 09/16] test for classful --- tests/test_extension.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_extension.py b/tests/test_extension.py index 2806188..51576cc 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -47,6 +47,26 @@ def get(self, **kwargs): docs.register(BandResource, endpoint='band') assert '/bands/{band_id}/' in docs.spec._paths + def test_register_classful(self, app, docs): + @doc(tags=['band']) + class BandResource(MethodResource): + def get(self, **kwargs): + return 'slowdive' + route = '/bands//' + view_func = BandResource.as_view('band') + app.add_url_rule(route, view_func=view_func) + rule = docs.app.view_functions['band'] + meta = dict( + view_func=view_func, + rule=rule, + route='band', + target=BandResource, + methods=['GET'], + ) + setattr(view_func, "_classful_meta", meta) + docs.register(view_func) + assert '/bands/{band_id}/' in docs.spec._paths + def test_register_resource_with_constructor_args(self, app, docs): @doc(tags=['band']) class BandResource(MethodResource): From f83b1cb3afffc3782de1b39824064396b5df3f6f Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 17:56:14 -0800 Subject: [PATCH 10/16] test cov --- flask_apispec/utils.py | 3 --- tests/test_extension.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/flask_apispec/utils.py b/flask_apispec/utils.py index 8d06516..5cbac4e 100644 --- a/flask_apispec/utils.py +++ b/flask_apispec/utils.py @@ -17,9 +17,6 @@ class Ref(object): def __init__(self, key): self.key = key - def __repr__(self): - return "".format(self.key) - def resolve(self, obj): return getattr(obj, self.key, None) diff --git a/tests/test_extension.py b/tests/test_extension.py index 51576cc..faa4204 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -64,7 +64,7 @@ def get(self, **kwargs): methods=['GET'], ) setattr(view_func, "_classful_meta", meta) - docs.register(view_func) + docs.register_existing_resources() assert '/bands/{band_id}/' in docs.spec._paths def test_register_resource_with_constructor_args(self, app, docs): From 19e0c52a42264dda28d6ff4427027b6135cb4489 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 21:21:07 -0800 Subject: [PATCH 11/16] fix register_existing_resources endpoint name when detecting blueprint --- flask_apispec/extension.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index d426bee..6a2ef28 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -94,10 +94,15 @@ def swagger_ui(self): def register_existing_resources(self): for name, rule in self.app.view_functions.items(): try: - blueprint_name, _ = name.split('.') + blueprint_name, endpoint_name = name.split('.') except ValueError: + endpoint_name = name blueprint_name = None - + # don't auto-register ResourceMeta endpoints + if hasattr(rule, 'view_class'): + view_class = rule.view_class + if isinstance(view_class, ResourceMeta): + continue try: if hasattr(rule, '_classful_meta'): self._register( @@ -105,7 +110,7 @@ def register_existing_resources(self): blueprint=blueprint_name, ) else: - self.register(rule, blueprint=blueprint_name) + self.register(rule, endpoint=endpoint_name, blueprint=blueprint_name) except TypeError: pass From 8a89b29810539928896994602eac242d7def7d06 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 21:21:37 -0800 Subject: [PATCH 12/16] skip arguments that incorrect matched on rule but aren't in a specific path method --- flask_apispec/paths.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask_apispec/paths.py b/flask_apispec/paths.py index 39e657f..baaff8b 100644 --- a/flask_apispec/paths.py +++ b/flask_apispec/paths.py @@ -23,6 +23,7 @@ def rule_to_params(rule, overrides=None): result = [ argument_to_param(argument, rule, overrides.get(argument, {})) for argument in rule.arguments + if argument in rule._converters ] for key in overrides.keys(): if overrides[key].get('in') in ('header', 'query'): From b9904770728d4062df84ca6b732e0b74ee32048b Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 21:21:07 -0800 Subject: [PATCH 13/16] Merge fix --- flask_apispec/extension.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flask_apispec/extension.py b/flask_apispec/extension.py index 3d90f0a..0744091 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -93,12 +93,17 @@ def swagger_ui(self): def register_existing_resources(self): for name, rule in self.app.view_functions.items(): try: - blueprint_name, _ = name.split('.') + blueprint_name, endpoint_name = name.split('.') except ValueError: + endpoint_name = name blueprint_name = None - + # don't auto-register ResourceMeta endpoints + if hasattr(rule, 'view_class'): + view_class = rule.view_class + if isinstance(view_class, ResourceMeta): + continue try: - self.register(rule, blueprint=blueprint_name) + self.register(rule, endpoint=endpoint_name, blueprint=blueprint_name) except TypeError: pass From 370a67e3656ac0f18eafe848b904633ef42fad21 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Thu, 9 Nov 2017 21:21:37 -0800 Subject: [PATCH 14/16] skip arguments that incorrect matched on rule but aren't in a specific path method --- flask_apispec/paths.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask_apispec/paths.py b/flask_apispec/paths.py index 39e657f..baaff8b 100644 --- a/flask_apispec/paths.py +++ b/flask_apispec/paths.py @@ -23,6 +23,7 @@ def rule_to_params(rule, overrides=None): result = [ argument_to_param(argument, rule, overrides.get(argument, {})) for argument in rule.arguments + if argument in rule._converters ] for key in overrides.keys(): if overrides[key].get('in') in ('header', 'query'): From 2c42d5165d50de077874a5ef017012e745d3d298 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Fri, 10 Nov 2017 10:46:54 -0800 Subject: [PATCH 15/16] allow tracking of parents for routed class methods --- flask_apispec/apidoc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flask_apispec/apidoc.py b/flask_apispec/apidoc.py index 6a7d416..04ac7e7 100644 --- a/flask_apispec/apidoc.py +++ b/flask_apispec/apidoc.py @@ -95,6 +95,9 @@ class ViewConverter(Converter): def get_operations(self, rule, view): return {method: view for method in rule.methods} + def get_parent(self, resource, **kwargs): + return resource.method_view if hasattr(resource, 'method_view') else None + class ResourceConverter(Converter): def get_operations(self, rule, resource): From 7865d357327ece657c76635fcd4b993a1f0220b2 Mon Sep 17 00:00:00 2001 From: Mischa Spiegelmock Date: Mon, 13 Nov 2017 19:41:11 -0800 Subject: [PATCH 16/16] don't format response (jsonify) if apply=False in marshal_with --- flask_apispec/wrapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask_apispec/wrapper.py b/flask_apispec/wrapper.py index 8d1e691..612f9b8 100644 --- a/flask_apispec/wrapper.py +++ b/flask_apispec/wrapper.py @@ -51,6 +51,7 @@ def marshal_result(self, unpacked, status_code): schema = utils.resolve_instance(schema['schema']) output = schema.dump(unpacked[0]).data else: + format_response = identity output = unpacked[0] return format_output((format_response(output), ) + unpacked[1:])