Source code for next.testing.loaders

"""Eager page-module loader used in tests.

`eager_load_pages` walks a pages directory and imports every `page.py`
file so that `@context` and `@forms.action` side effects register
before a test dispatches HTTP requests. Results are memoised per
absolute directory so repeated calls during a pytest session are
cheap.
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

from next.components import components_manager


_loaded_dirs: set[Path] = set()


[docs] def eager_load_pages(base_dir: Path | str) -> list[Path]: """Import every `page.py` under `base_dir` and return loaded paths. The call is idempotent for the same absolute directory. Non-existent paths raise `FileNotFoundError`. Importer errors bubble up so that broken page modules fail loudly in test setup rather than producing confusing 404 responses later. """ directory = Path(base_dir).resolve() if not directory.is_dir(): msg = f"Pages directory not found: {directory}" raise FileNotFoundError(msg) if directory in _loaded_dirs: return [] loaded: list[Path] = [] for page_file in sorted(directory.rglob("page.py")): _load_module_from_path(page_file) loaded.append(page_file) _loaded_dirs.add(directory) return loaded
def _load_module_from_path(path: Path) -> None: module_name = _derive_module_name(path) spec = importlib.util.spec_from_file_location(module_name, path) if spec is None or spec.loader is None: msg = f"Cannot build import spec for {path}" raise ImportError(msg) module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) def _derive_module_name(path: Path) -> str: parts = list(path.with_suffix("").parts) cleaned = [p.replace("[", "_").replace("]", "_").replace(":", "_") for p in parts] return "next_testing_pages." + "_".join(cleaned[-6:])
[docs] def clear_loaded_dirs() -> None: """Drop the memoisation cache so the next call reloads page modules. Intended for self-tests of the loader. Production test suites do not need to call this because each pytest session gets a fresh interpreter. """ _loaded_dirs.clear()
[docs] def eager_load_components() -> None: """Import every registered `component.py` so decorators register before tests.""" components_manager._ensure_backends() for backend in components_manager._backends: ensure = getattr(backend, "_ensure_loaded", None) if callable(ensure): ensure() import_all = getattr(backend, "import_all_component_modules", None) if callable(import_all): import_all()
__all__ = ["clear_loaded_dirs", "eager_load_components", "eager_load_pages"]