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.