"""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)