Source code for flask_smorest.blueprint

"""API Blueprint

This is a subclass of Flask's Blueprint

It provides added features:

- Decorators to specify Marshmallow schema for view functions I/O

- API documentation registration

Documentation process works in several steps:

- At import time

  - When a MethodView or a view function is decorated, relevant information
    is automatically added to the object's ``_apidoc`` attribute.

  - The ``Blueprint.doc`` decorator stores additional information in there that
    flask-smorest can not - or does not yet - infer from the code.

  - The ``Blueprint.route`` decorator registers the endpoint in the Blueprint
    and gathers all documentation information about the endpoint in
    ``Blueprint._docs[endpoint]``.

- At initialization time

  - Schema instances are replaced by their reference in the `schemas` section
    of the spec components.

  - The ``Blueprint.register_blueprint`` method merges nested blueprint
    documentation into the parent blueprint documentation.

  - Documentation is finalized using the information stored in
    ``Blueprint._docs``, with adaptations to parameters only known at init
    time, such as OAS version.

  - Manual documentation is deep-merged with automatic documentation.

  - Endpoints documentation is registered in the APISpec object.
"""

from copy import deepcopy
from functools import wraps

from flask import Blueprint as FlaskBlueprint
from flask import current_app
from flask.views import MethodView

from .arguments import ArgumentsMixin
from .etag import EtagMixin
from .pagination import PaginationMixin
from .response import ResponseMixin
from .utils import deepupdate, load_info_from_docstring


[docs] class Blueprint( FlaskBlueprint, ArgumentsMixin, ResponseMixin, PaginationMixin, EtagMixin ): """Blueprint that registers info in API documentation""" # Order in which the methods are presented in the spec HTTP_METHODS = ["OPTIONS", "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"] DEFAULT_LOCATION_CONTENT_TYPE_MAPPING = { "json": "application/json", "form": "application/x-www-form-urlencoded", "files": "multipart/form-data", } DOCSTRING_INFO_DELIMITER = "---" def __init__(self, *args, **kwargs): self.description = kwargs.pop("description", "") super().__init__(*args, **kwargs) # _docs stores information used at init time to produce documentation. # For each endpoint, for each method, each feature stores info in there # to be is used by a dedicated _prepare_*_doc callback. An extra # "parameters" entry is added to store common route parameters doc. # { # endpoint: { # 'parameters: [list of common route parameters], # 'get': { # 'response': { info used by response decorator to produce doc}, # 'argument': { info used by arguments decorator to produce doc}, # ... # 'post': ..., # ... # }, # ... # } self._docs = {} self._endpoints = [] self._prepare_doc_cbks = [ self._prepare_arguments_doc, self._prepare_response_doc, self._prepare_pagination_doc, self._prepare_etag_doc, ]
[docs] def add_url_rule( self, rule, endpoint=None, view_func=None, provide_automatic_options=None, *, parameters=None, tags=None, **options, ): """Register url rule in application Also stores doc info for later registration Use this to register a :class:`MethodView <flask.views.MethodView>` or a resource function. :param str rule: URL rule as string. :param str endpoint: Endpoint for the registered URL rule (defaults to function name). :param callable|MethodView view_func: View function or MethodView class :param list parameters: List of parameter descriptions relevant to all operations in this path. Only used to document the resource. :param list tags: List of tags for the resource. If None, ``Blueprint`` name is used. :param options: Options to be forwarded to the underlying :class:`werkzeug.routing.Rule <Rule>` object. """ if view_func is None: raise TypeError("view_func must be provided") if endpoint is None: endpoint = view_func.__name__ # Ensure endpoint name is unique # - to avoid a name clash when registering a MethodView # - to use it as a key internally in endpoint -> doc mapping if endpoint in self._endpoints: endpoint = f"{endpoint}_{len(self._endpoints)}" self._endpoints.append(endpoint) if isinstance(view_func, type(MethodView)): func = view_func.as_view(endpoint) else: func = view_func # Add URL rule in Flask and store endpoint documentation super().add_url_rule(rule, endpoint, func, **options) self._store_endpoint_docs(endpoint, view_func, parameters, tags, **options)
[docs] def route(self, rule, *, parameters=None, tags=None, **options): """Decorator to register view function in application and documentation Calls :meth:`add_url_rule <Blueprint.add_url_rule>`. """ def decorator(func): endpoint = options.pop("endpoint", None) self.add_url_rule( rule, endpoint, func, parameters=parameters, tags=tags, **options ) return func return decorator
[docs] def register_blueprint(self, blueprint, **options): """Register a nested blueprint in application Also stores doc info from the nested bluepint for later registration. Use this to register a nested :class:`Blueprint <Blueprint>`. :param Blueprint blueprint: Blueprint to register under this blueprint. :param options: Options to be forwarded to the underlying :meth:`flask.Blueprint.register_blueprint` method. See :ref:`register-nested-blueprints`. """ blp_name = options.get("name", blueprint.name) # Inherit all endpoints self._docs.update( { ".".join((blp_name, endpoint_name)): doc for endpoint_name, doc in blueprint._docs.items() } ) return super().register_blueprint(blueprint, **options)
def _store_endpoint_docs(self, endpoint, obj, parameters, tags, **options): """Store view or function doc info""" endpoint_doc_info = self._docs.setdefault(endpoint, {}) def store_method_docs(method, function): """Add auto and manual doc to table for later registration""" # Get documentation from decorators # Deepcopy doc info as it may be used for several methods and it # may be mutated in apispec doc = deepcopy(getattr(function, "_apidoc", {})) # Get summary/description from docstring doc["docstring"] = load_info_from_docstring( function.__doc__, delimiter=self.DOCSTRING_INFO_DELIMITER ) # Tags for this resource doc["tags"] = tags # Store function doc infos for later processing/registration endpoint_doc_info[method.lower()] = doc # MethodView (class) if isinstance(obj, type(MethodView)): for method in self.HTTP_METHODS: if method in obj.methods: if "methods" not in options or method in options["methods"]: func = getattr(obj, method.lower()) store_method_docs(method, func) # Function else: for method in self.HTTP_METHODS: if method in options.get("methods", ("GET",)): store_method_docs(method, obj) # Store parameters doc info from route decorator endpoint_doc_info["parameters"] = parameters
[docs] def register_views_in_doc(self, api, app, spec, *, name, parameters): """Register views information in documentation If a schema in a parameter or a response appears in the spec `schemas` section, it is replaced by a reference in the parameter or response documentation: "schema":{"$ref": "#/components/schemas/MySchema"} """ url_prefix_parameters = parameters or [] # This method uses the documentation information associated with each # endpoint in self._docs to provide documentation for corresponding # route to the spec object. # Deepcopy to avoid mutating the source. Allows registering blueprint # multiple times (e.g. when creating multiple apps during tests). for endpoint, endpoint_doc_info in deepcopy(self._docs).items(): endpoint_route_parameters = endpoint_doc_info.pop("parameters") or [] endpoint_parameters = url_prefix_parameters + endpoint_route_parameters doc = {} # Use doc info stored by decorators to generate doc for method_l, operation_doc_info in endpoint_doc_info.items(): tags = operation_doc_info.pop("tags") operation_doc = {} for func in self._prepare_doc_cbks: operation_doc = func( operation_doc, operation_doc_info, api=api, app=app, spec=spec, method=method_l, ) operation_doc.update(operation_doc_info["docstring"]) # Tag all operations with Blueprint name unless tags specified operation_doc["tags"] = ( tags if tags is not None else [ name, ] ) # Complete doc with manual doc info manual_doc = operation_doc_info.get("manual_doc", {}) doc[method_l] = deepupdate(operation_doc, manual_doc) # Thanks to self.route, there can only be one rule per endpoint full_endpoint = ".".join((name, endpoint)) rule = next(app.url_map.iter_rules(full_endpoint)) spec.path(rule=rule, operations=doc, parameters=endpoint_parameters)
[docs] @staticmethod def doc(**kwargs): """Decorator adding description attributes to a view function Values passed as kwargs are copied verbatim in the docs Example: :: @blp.doc(description="Return pets based on ID", summary="Find pets by ID" ) def get(...): ... """ def decorator(func): @wraps(func) def wrapper(*f_args, **f_kwargs): return current_app.ensure_sync(func)(*f_args, **f_kwargs) # The deepcopy avoids modifying the wrapped function doc wrapper._apidoc = deepcopy(getattr(wrapper, "_apidoc", {})) wrapper._apidoc["manual_doc"] = deepupdate( deepcopy(wrapper._apidoc.get("manual_doc", {})), kwargs ) return wrapper return decorator
def _decorate_view_func_or_method_view(self, decorator, obj): """Apply decorator to view func or MethodView HTTP methods""" # Decorating a MethodView decorates all HTTP methods if isinstance(obj, type(MethodView)): for method in self.HTTP_METHODS: if method in obj.methods: method_l = method.lower() func = getattr(obj, method_l) setattr(obj, method_l, decorator(func)) return obj return decorator(obj)