v0.166.0
 1import os
 2import os.path
 3import re
 4import sys
 5import threading
 6from collections.abc import Callable
 7
 8import watchfiles
 9
10COMPILED_EXT_RE = re.compile(r"py[co]$")
11
12
13class Reloader(threading.Thread):
14    """File change reloader using watchfiles for cross-platform native file watching."""
15
16    def __init__(self, callback: Callable[[str], None], watch_html: bool) -> None:
17        super().__init__()
18        self.daemon = True
19        self._callback = callback
20        self._watch_html = watch_html
21
22    def get_watch_paths(self) -> set[str]:
23        """Get all directories to watch for changes."""
24        paths = set()
25
26        # Get directories from loaded Python modules
27        for module in tuple(sys.modules.values()):
28            if not hasattr(module, "__file__") or not module.__file__:
29                continue
30            # Convert .pyc/.pyo to .py and get directory
31            file_path = COMPILED_EXT_RE.sub("py", module.__file__)
32            dir_path = os.path.dirname(os.path.abspath(file_path))
33            if os.path.isdir(dir_path):
34                paths.add(dir_path)
35
36        # Add current working directory for .env files
37        cwd = os.getcwd()
38        if os.path.isdir(cwd):
39            paths.add(cwd)
40
41        return paths
42
43    def run(self) -> None:
44        """Watch for file changes and trigger callback."""
45        watch_paths = self.get_watch_paths()
46
47        for changes in watchfiles.watch(*watch_paths, rust_timeout=1000):
48            for change_type, file_path in changes:
49                should_reload = False
50                filename = os.path.basename(file_path)
51
52                # Python files: reload on modify/add
53                if change_type in (
54                    watchfiles.Change.modified,
55                    watchfiles.Change.added,
56                ) and file_path.endswith(".py"):
57                    should_reload = True
58
59                # .env files: reload on modify/add/delete
60                if change_type in (
61                    watchfiles.Change.modified,
62                    watchfiles.Change.added,
63                    watchfiles.Change.deleted,
64                ) and filename.startswith(".env"):
65                    should_reload = True
66
67                # HTML files: only reload on add/delete (Jinja auto-reloads modifications)
68                if (
69                    self._watch_html
70                    and change_type
71                    in (
72                        watchfiles.Change.added,
73                        watchfiles.Change.deleted,
74                    )
75                    and file_path.endswith(".html")
76                ):
77                    should_reload = True
78
79                if should_reload:
80                    self._callback(file_path)