diff --git a/flask_apispec/apidoc.py b/flask_apispec/apidoc.py index 239c673..77596a0 100644 --- a/flask_apispec/apidoc.py +++ b/flask_apispec/apidoc.py @@ -11,7 +11,9 @@ 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_resource, resolve_annotations, merge_recursive +from flask_apispec.utils import resolve_instance, resolve_annotations, merge_recursive +import inspect + class Converter(object): @@ -59,9 +61,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 @@ -100,6 +108,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): @@ -111,3 +122,30 @@ def get_operations(self, rule, resource): def get_parent(self, resource, **kwargs): return resolve_resource(resource, **kwargs) + + +class ClassfulConverter(Converter): + + def convert(self, classful_meta): + paths = list() + 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, classful_meta, classful_meta=classful_meta)) + + return paths + + def get_description(self, view): + return inspect.getdoc(view) + + def get_operations(self, rule, endpoint): + # remove OPTIONS (its for CORS) + methods = set(rule.methods) - {'OPTIONS'} + return { + method: endpoint['view_func'] + for method in methods + } + + 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 2c6bbb5..1972479 100644 --- a/flask_apispec/extension.py +++ b/flask_apispec/extension.py @@ -6,7 +6,7 @@ from apispec.ext.marshmallow import MarshmallowPlugin from flask_apispec import ResourceMeta -from flask_apispec.apidoc import ViewConverter, ResourceConverter +from flask_apispec.apidoc import ViewConverter, ResourceConverter, ClassfulConverter class FlaskApiSpec(object): @@ -49,6 +49,9 @@ def __init__(self, app=None): 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')) @@ -93,12 +96,23 @@ 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) + if hasattr(rule, '_classful_meta'): + self._register( + rule, + blueprint=blueprint_name, + ) + else: + self.register(rule, endpoint=endpoint_name, blueprint=blueprint_name) except TypeError: pass @@ -130,7 +144,10 @@ 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 isinstance(target, ResourceMeta): paths = self.resource_converter.convert( 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'): diff --git a/flask_apispec/wrapper.py b/flask_apispec/wrapper.py index 067261f..de0a726 100644 --- a/flask_apispec/wrapper.py +++ b/flask_apispec/wrapper.py @@ -58,6 +58,7 @@ def marshal_result(self, unpacked, status_code): dumped = schema.dump(unpacked[0]) output = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped else: + format_response = identity output = unpacked[0] return format_output((format_response(output), ) + unpacked[1:]) diff --git a/tests/test_extension.py b/tests/test_extension.py index 2806188..faa4204 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_existing_resources() + assert '/bands/{band_id}/' in docs.spec._paths + def test_register_resource_with_constructor_args(self, app, docs): @doc(tags=['band']) class BandResource(MethodResource):