This commit is contained in:
Joakim Hellsén 2026-04-27 20:43:26 +02:00
commit a7a5b5c8ea
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
43 changed files with 5531 additions and 9 deletions

112
config/dev_autoreload.py Normal file
View file

@ -0,0 +1,112 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import django_watchfiles
from django.utils import autoreload
if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Sequence
from watchfiles import Change
IGNORED_PROJECT_ROOT_NAMES: frozenset[str] = frozenset({
".git",
".venv",
"__pycache__",
"firecracker",
})
def _resolve_path(path: Path) -> Path:
try:
return path.resolve()
except OSError:
return path.absolute()
def _is_relative_to(path: Path, parent: Path) -> bool:
try:
path.relative_to(parent)
except ValueError:
return False
return True
def build_project_watch_roots(
project_root: Path,
*,
ignored_root_names: Sequence[str] = tuple(IGNORED_PROJECT_ROOT_NAMES),
) -> tuple[Path, ...]:
"""Return top-level project directories worth watching during development."""
ignored_names = set(ignored_root_names)
resolved_project_root = _resolve_path(project_root)
return tuple(
sorted(
_resolve_path(child)
for child in resolved_project_root.iterdir()
if child.is_dir() and not child.name.startswith(".") and child.name not in ignored_names
),
)
class TussilagoWatchfilesReloader(django_watchfiles.WatchfilesReloader):
"""Use child directories instead of repo root for dev autoreload watches."""
def __init__(
self,
*,
project_root: Path,
ignored_paths: Sequence[Path] | None = None,
) -> None:
"""Store project-specific watch settings before watcher startup."""
self.project_root = _resolve_path(project_root)
self.ignored_paths = tuple(
_resolve_path(path) for path in (ignored_paths or (self.project_root / "firecracker",))
)
self.project_watch_roots = build_project_watch_roots(self.project_root)
super().__init__()
def file_filter(self, change: Change, filename: str) -> bool:
"""Drop file events from ignored paths such as the firecracker tree.
Returns:
True when the file event should still be watched.
"""
resolved_path = _resolve_path(Path(filename))
if any(_is_relative_to(resolved_path, ignored_path) for ignored_path in self.ignored_paths):
return False
return super().file_filter(change, filename)
def watched_roots(self, watched_files: Iterable[Path]) -> frozenset[Path]:
"""Replace repo-root watches with top-level child directories.
Returns:
Watch roots with the project root expanded into child directories.
"""
roots = {_resolve_path(root) for root in super().watched_roots(watched_files)}
filtered_roots = {
root
for root in roots
if not any(_is_relative_to(root, ignored_path) for ignored_path in self.ignored_paths)
}
if self.project_root in filtered_roots:
filtered_roots.remove(self.project_root)
filtered_roots.update(self.project_watch_roots)
return frozenset(filtered_roots)
def install_watchfiles_reloader_patch(*, project_root: Path) -> None:
"""Install project-specific watchfiles reloader for dev and test runtimes."""
resolved_project_root = _resolve_path(project_root)
class ProjectWatchfilesReloader(TussilagoWatchfilesReloader):
def __init__(self) -> None:
super().__init__(project_root=resolved_project_root)
def replaced_get_reloader() -> autoreload.BaseReloader:
return ProjectWatchfilesReloader()
autoreload.get_reloader = replaced_get_reloader