112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
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
|