Skip to main content

The Registration Lifecycle

In this project, blueprints are designed to be portable collections of routes and handlers that can be attached to an application at a later time. Because a blueprint may be defined before an application instance even exists, it cannot immediately register its routes. Instead, it uses a deferred registration lifecycle managed by the BlueprintSetupState class.

The Deferred Registration Pattern

When you call a method like route or add_url_rule on a Blueprint, the blueprint does not modify any routing tables. Instead, it "records" the action for later execution. This is implemented in src/flask/sansio/blueprints.py using the record method, which appends a callback to the self.deferred_functions list:

# src/flask/sansio/blueprints.py

@setupmethod
def record(self, func: DeferredSetupFunction) -> None:
"""Registers a function that is called when the blueprint is
registered on the application.
"""
self.deferred_functions.append(func)

@setupmethod
def add_url_rule(
self,
rule: str,
endpoint: str | None = None,
view_func: ft.RouteCallable | None = None,
**options: t.Any,
) -> None:
# ... validation ...
self.record(
lambda s: s.add_url_rule(
rule,
endpoint,
view_func,
**options,
)
)

Every action that needs to interact with the App instance is wrapped in a lambda or a function that accepts a BlueprintSetupState object (often named s or state).

Capturing Context with BlueprintSetupState

The BlueprintSetupState class is a temporary container created during the registration process. Its primary purpose is to capture the specific context of a single registration call. Since the same blueprint can be registered multiple times (e.g., with different URL prefixes), the state object ensures that the recorded functions are applied with the correct parameters for that specific instance.

The state object tracks several key attributes:

  • app: The target application instance.
  • blueprint: The blueprint being registered.
  • options: The keyword arguments passed to app.register_blueprint.
  • url_prefix and subdomain: The final calculated values, merging blueprint defaults with registration-time overrides.

The add_url_rule method on BlueprintSetupState is where the actual integration with the application happens. It handles the logic of prefixing the endpoint with the blueprint's name and joining the URL rule with the registration's url_prefix:

# src/flask/sansio/blueprints.py

def add_url_rule(
self,
rule: str,
endpoint: str | None = None,
view_func: ft.RouteCallable | None = None,
**options: t.Any,
) -> None:
if self.url_prefix is not None:
if rule:
rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/")))
else:
rule = self.url_prefix

# ... endpoint prefixing ...
self.app.add_url_rule(
rule,
f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
view_func,
# ...
)

The Registration Trigger

The lifecycle transitions from "recording" to "execution" when app.register_blueprint(bp, **options) is called. This method invokes blueprint.register(self, options), which performs the following steps:

  1. Name Resolution: It determines the unique name for this registration (using the name or name_prefix options).
  2. State Creation: It calls make_setup_state to instantiate the BlueprintSetupState.
  3. Static Folder Setup: If the blueprint has a static folder, it automatically records a route for it.
  4. Function Execution: It iterates through self.deferred_functions and calls each one, passing the state object.
  5. Recursion: It triggers the registration of any nested blueprints.
# src/flask/sansio/blueprints.py

def register(self, app: App, options: dict[str, t.Any]) -> None:
# ... name resolution and validation ...

state = self.make_setup_state(app, options, first_bp_registration)

# Execute all recorded functions
for deferred in self.deferred_functions:
deferred(state)

# Handle nested blueprints
for blueprint, bp_options in self._blueprints:
# ... calculate nested prefixes and subdomains ...
blueprint.register(app, bp_options)

Multi-Registration and Global Handlers

A significant challenge in the registration lifecycle is handling "global" application components, such as template filters or global error handlers. If a blueprint is registered twice, you typically only want these global components registered once.

The BlueprintSetupState includes a first_registration boolean to solve this. The record_once method wraps a callback to ensure it only executes if state.first_registration is true:

# src/flask/sansio/blueprints.py

@setupmethod
def record_once(self, func: DeferredSetupFunction) -> None:
def wrapper(state: BlueprintSetupState) -> None:
if state.first_registration:
func(state)

self.record(update_wrapper(wrapper, func))

This is used for methods like add_app_template_filter, ensuring that the filter is added to the app only during the very first time the blueprint is attached to that specific application instance.

Immutability and Consistency

To ensure the integrity of the registration lifecycle, blueprints enforce a strict "no changes after registration" policy. Once register has been called, the blueprint sets self._got_registered_once = True.

Any subsequent calls to methods decorated with @setupmethod (like route, record, or errorhandler) will trigger _check_setup_finished, raising an AssertionError. This prevents developers from adding routes to a blueprint after it has already been processed by an application, which would lead to inconsistent state where some application instances have the route and others do not.

# src/flask/sansio/blueprints.py

def _check_setup_finished(self, f_name: str) -> None:
if self._got_registered_once:
raise AssertionError(
f"The setup method '{f_name}' can no longer be called on the blueprint"
f" '{self.name}'. It has already been registered at least once..."
)