Skip to main content

Template Loading Strategy

The template loading strategy in this project is centered around the DispatchingJinjaLoader, a specialized Jinja2 BaseLoader that coordinates template discovery across the main application and all registered blueprints. Instead of searching a single directory, it implements a hierarchical search pattern that allows blueprints to provide their own templates while giving the application the power to override them.

The Dispatching Mechanism

The DispatchingJinjaLoader does not load template source code directly from the filesystem. Instead, it acts as a dispatcher that delegates the actual loading to other loaders. It maintains a reference to the App instance and iterates through the available "scaffolds" (the application itself and its blueprints) to find a loader that can provide the requested template.

The core of this logic resides in the _iter_loaders method:

def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]:
loader = self.app.jinja_loader
if loader is not None:
yield self.app, loader

for blueprint in self.app.iter_blueprints():
loader = blueprint.jinja_loader
if loader is not None:
yield blueprint, loader

This method establishes a strict search order:

  1. The Application: The loader first checks the application's own jinja_loader.
  2. Blueprints: If the template is not found in the application, it iterates through all registered blueprints in the order they were registered via app.register_blueprint().

Individual Loaders

Each Scaffold (the base class for both App and Blueprint) provides its own jinja_loader property. By default, if a template_folder was specified during initialization, this property returns a Jinja FileSystemLoader pointing to that directory relative to the scaffold's root_path.

@cached_property
def jinja_loader(self) -> BaseLoader | None:
if self.template_folder is not None:
return FileSystemLoader(os.path.join(self.root_path, self.template_folder))
else:
return None

Search Resolution and Overriding

Because the application's loader is always checked first, any template placed in the application's template folder will "shadow" or override a template with the same name in any blueprint. This is a deliberate design choice that allows application developers to customize the UI of third-party blueprints without modifying the blueprint's source code.

Similarly, if two blueprints provide a template with the same name, the blueprint that was registered first with the application takes precedence.

Loading Performance and Debugging

The loader implements two distinct paths for template resolution: a "fast" path for production and an "explained" path for development.

Fast Path

In normal operation, _get_source_fast is used. it simply iterates through the loaders and returns the first successful match. If a loader raises a TemplateNotFound exception, the dispatcher silently moves to the next one.

Explained Path

When the configuration EXPLAIN_TEMPLATE_LOADING is set to True, the loader switches to _get_source_explained. This mode tracks every attempt made by the dispatcher, including which loaders were checked and whether they succeeded or failed.

def _get_source_explained(
self, environment: BaseEnvironment, template: str
) -> tuple[str, str | None, t.Callable[[], bool] | None]:
attempts = []
# ... iteration logic ...
from .debughelpers import explain_template_loading_attempts
explain_template_loading_attempts(self.app, template, attempts)
# ... return result ...

This information is then passed to explain_template_loading_attempts, which logs a detailed breakdown of the search process. This is invaluable for troubleshooting why a specific template is being loaded from an unexpected location or why a template override isn't working as intended.

Customization Points

While the DispatchingJinjaLoader is the default, the system provides hooks for customization:

  1. Overriding the Global Loader: An application can override create_global_jinja_loader to return a different loader implementation entirely.
  2. Blueprint Template Folders: Blueprints only participate in template loading if they are initialized with a template_folder argument. If this is omitted, the blueprint's jinja_loader remains None, and it is skipped during the dispatching process.
  3. Manual Environment Configuration: The Environment class in src.flask.templating automatically sets up the DispatchingJinjaLoader if no loader is provided to its constructor, ensuring that the dispatching logic is integrated into the Jinja environment lifecycle.