Context in case this is an XY problem: I’m working on a static site generator using jinja templates. I want to let the jinja templates use python code specific to the static site project. I also want to do incremental builds as quickly and correctly as possible, which means keeping track of which python files are imported so that a jinja template can be rebuilt when a python file it imports changes.
Actual question: I think the code below is working to both track all project-local python files that a jinja template imports (with self._fs.add_dependency(module_path)
) and to isolate (not for security) those files from the static site generator’s own code. But returning a module name of f"<ginjarator imported module: {fullname}>"
seems like a huge hack that could easily break in the future. Is there a better way to do this? Or is it actually ok for a MetaPathFinder to return a spec with an invalid module name?
(Please ignore the TODO comments, those are things that I can figure out easily myself after I figure out the right general approach to take here.)
# SPDX-FileCopyrightText: 2025 David Mandelberg <david@mandelberg.org>
#
# SPDX-License-Identifier: Apache-2.0
_enabled_finder: contextvars.ContextVar["_MetaPathFinder"] = (
contextvars.ContextVar("_enabled_finder")
)
class _MetaPathFinder(importlib.abc.MetaPathFinder):
"""Finder for local python modules in a Filesystem."""
def __init__(
self,
*,
fs: filesystem.Filesystem,
path: pathlib.Path,
) -> None:
"""Initializer.
Args:
fs: Filesystem access.
path: Where to look for project-local modules.
"""
self._fs = fs
self._path = path
@override
def find_spec(
self,
fullname: str,
path: Sequence[str] | None,
target: types.ModuleType | None = None,
) -> importlib.machinery.ModuleSpec | None:
del path, target # Unused.
if _enabled_finder.get(None) is not self:
return None
module_path = self._fs.resolve(
self._path / (fullname.replace(".", "/") + ".py")
)
if not module_path.exists():
# TODO: add a weak dependency, in depfile not dyndep?
# TODO: this is a race condition, use read_text instead
return None
self._fs.add_dependency(module_path)
# sys.modules caching could prevent this code from tracking all
# dependencies, and it could leak local modules from the project being
# built into ginjarator itself. This returns a spec with a different
# name than what was requested to disable caching, and uses an invalid
# name so it doesn't affect any normal imports.
fake_fullname = f"<ginjarator imported module: {fullname}>"
return importlib.util.spec_from_loader(
fake_fullname,
importlib.machinery.SourceFileLoader(
fake_fullname,
str(module_path),
),
)
@contextlib.contextmanager
def enable(self) -> Generator[None, None, None]:
"""Returns a context manager that temporarily enables this finder."""
with contextlib.ExitStack() as stack:
token = _enabled_finder.set(self)
stack.callback(_enabled_finder.reset, token)
sys.meta_path.append(self) # TODO: before normal path?
stack.callback(sys.meta_path.remove, self)
yield