flask flask blueprint dynamic code loading flask application flask app

Dynamically registering your flask blueprint endpoints

Dynamically loading your Flask's blueprint is a great way to structure your endpoints and avoid duplicating code.

Photo by Sigmund on Unsplash

Photo by Sigmund on Unsplash

Flask’s blueprint is a great way to structure your endpoints.

In the past, to avoid duplicating code, I found myself having to reuse certain blueprints several times. What changed between uses was the url prefix and certain behaviors such as filters to be applied to the data.

What I did in the past

For this purpose, I used to have “register_blueprint” functions called with different parameters depending on usage. But this wasn’t great for cyclomatic complexity as endpoint definitions were nested inside a function.

def register_blueprint(app, url_prefix, version):
    blueprint = Blueprint(f"api_{version.replace('.', '_')}", __name__)

    @blueprint.route("/status")
    def status():
        return jsonify({"status": "OK", "version": version)

    if version > 2:
        @blueprint.route("/something")
        def something():
            ...
   
    ....
    app.register_blueprint(blueprint, url_prefix=url_prefix)

(This is an example not taken from a real code, but you can see the main point: you can register route according to the parameter values and also change how the endpoint responds)

What I do now

Now I dynamically load and register my blueprints with a custom decorator. The following example is not a final proposition, there is still room to improvement.

The custom decorator

import types
from typing import Optional, Callable, Union, cast

def register_route(rule, endpoint: Optional[str] = None, **rule_kwargs):
    def my_decorator(func):
        final_endpoint = endpoint if endpoint else func.__name__

        def wrapper(blueprint, **params):
            endpoint_enabled = cast(
                Union[Callable[[dict], bool], bool], rule_kwargs.pop("enabled", True)
            )
            if callable(endpoint_enabled):
                endpoint_enabled = endpoint_enabled(**params)
                print(endpoint_enabled)

            if endpoint_enabled:
                blueprint.route(rule, endpoint=final_endpoint, defaults=params, **rule_kwargs)(func)

        wrapper.is_url_rule = True
        return wrapper

    return my_decorator

def is_register_route(func) -> bool:
    if not isinstance(func, types.FunctionType):
        return False
    return isinstance(func, types.FunctionType) and getattr(func, "is_url_rule", False)

The loading mechanism

I start by defining a list of blueprints and their associated parameters.

Here the blueprint defined in the “status” (api.status in fact) module will be registered twice, with two different url_prefix and version param.

As for the “users” blueprint, only its prefix_url is given

blueprints = {
    "status": [
        {
            "name": "status_1",
            "kwargs": {"url_prefix": "/api/1.0/status"},
            "params": {"version": 1},
        },
        {
            "name": "status_2",
            "kwargs": {"url_prefix": "/api/2.0/status"},
            "params": {"version": 1.5},
        },
    ],
    "users": "/api/1.0/users",
}

I also define a register_blueprints function that will be called with the flask application as first positional parameter. This function will define my root api blueprint.

def register_blueprints(app, **kwargs):
    api_blueprint = Blueprint("api", __name__, **kwargs)

    for module, blueprints_data in blueprints.items():
        # we iterate on blueprints item
        module_name = f"api.{module}"
        module = __import__(module_name, fromlist=[""])
        
        # we get functions wrapped with our custom decorator
        functions = getmembers(module, is_register_route)

        if not functions:
            continue

        if not isinstance(blueprints_data, list):
            # we want to work with lists
            blueprints_data = [blueprints_data]

        for blueprint_data in blueprints_data:
            # we determine blueprint parameters
            if isinstance(blueprint_data, str):
                blueprint_data = {"kwargs": {"url_prefix": blueprint_data}}

            blueprint_name = blueprint_data.get(
                "name", module.__name__.split(".")[-1]
            )
            blueprint_kwargs = blueprint_data.get("kwargs", {})
            params = blueprint_data.get("params", {})
            
            # we create the blueprint
            module_blueprint = Blueprint(blueprint_name, module.__name__)

            for register_route in functions:
                # we register the endpoints
                register_route[1](module_blueprint, **params)

            # the "module" blueprint is registered to the root one
            api_blueprint.register_blueprint(module_blueprint, **blueprint_kwargs)

    app.register_blueprint(api_blueprint, **kwargs)

Registering the endpoints

(this is the api.status blueprint definition module)

from flask import jsonify

from api.blueprint_decorator import register_route

@register_route("/", enabled=lambda **params: params.get("version", 1) > 1)
def status(*, version: int = 1):
    return jsonify({"status": "OK", "version": version})

In this example you can see that the route is registered only if the parameter version value is greater than one (conditional registration).

You can also see that inside the endpoint we access the version parameter as a keyword one.

That’s it, now adding a endpoint is quite the same than using a “classic” blueprint.route approach but I can still adapt how endpoints work using parameters defined at the blueprint registration level.

Thanks for reading, feel free to comment !