Skip to main content

The Blueprint Setup Mechanism

The Blueprint mechanism in this codebase follows a "record-and-apply" pattern. Because a Blueprint is often defined before the Flask application object exists, it cannot register routes or error handlers immediately. Instead, it records these operations as deferred functions and applies them later during the registration process using a specialized state object.

The Record-and-Apply Pattern

The Blueprint class (found in src/flask/sansio/blueprints.py) acts as a recorder. When you use decorators like @bp.route or @bp.errorhandler, the blueprint does not modify the application's routing table. Instead, it wraps the operation in a lambda or a local function and stores it in self.deferred_functions.

The Recording Phase

The record method is the primary way operations are queued. For example, the add_url_rule implementation in Blueprint does not register the rule directly; it records a call to the setup state's version of the method:

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

This ensures that the final URL rule and endpoint name are only determined when the blueprint is actually attached to an application, allowing for dynamic prefixes and names.

The Registration Process

Registration occurs when app.register_blueprint(bp) is called, which triggers Blueprint.register(app, options). This process involves several distinct steps to transition the blueprint's state into the application.

  1. Name Resolution: The blueprint determines its unique name within the app, considering any name_prefix from parent blueprints.
  2. State Creation: It calls make_setup_state to create an instance of BlueprintSetupState.
  3. Function Merging: It calls _merge_blueprint_funcs to copy error handlers, view functions, and request hooks (like before_request) directly into the application's internal dictionaries.
  4. Deferred Execution: It iterates through self.deferred_functions, calling each one with the BlueprintSetupState instance.
  5. Nested Blueprints: It recursively calls register on any blueprints nested within it.

Consistency Checks

To prevent inconsistent application state, the blueprint tracks if it has been registered using _got_registered_once. Any attempt to call a @setupmethod (like route or record) after registration will raise an AssertionError.

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

The Blueprint Setup State

The BlueprintSetupState class is a temporary object that lives only during the registration process. It serves as the bridge between the generic instructions recorded on the Blueprint and the specific requirements of the App.

Its primary responsibility is resolving the final configuration for routes. When BlueprintSetupState.add_url_rule is called, it performs the following:

  • URL Joining: It prepends the registration's url_prefix to the rule.
  • Endpoint Prefixing: It constructs the final endpoint name by combining the name_prefix, the blueprint's name, and the local endpoint.
  • Subdomain Application: It applies the subdomain specified during registration or defined on the blueprint.
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

# ...

self.app.add_url_rule(
rule,
f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
view_func,
defaults=defaults,
**options,
)

Advanced Setup Mechanisms

The record_once Mechanism

Some operations, such as registering template filters or globals, should only happen once even if a blueprint is registered multiple times (e.g., with different URL prefixes). The record_once method wraps a deferred function to check the state.first_registration flag:

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

Nested Blueprints and Accumulation

When blueprints are nested via Blueprint.register_blueprint, the parent blueprint stores the child in self._blueprints. During the parent's registration, it calculates the cumulative url_prefix and subdomain for the child:

if state.url_prefix is not None and bp_url_prefix is not None:
bp_options["url_prefix"] = (
state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
)
elif bp_url_prefix is not None:
bp_options["url_prefix"] = bp_url_prefix
elif state.url_prefix is not None:
bp_options["url_prefix"] = state.url_prefix

This allows for deeply nested structures where each level contributes to the final routing path and endpoint namespacing.