11from __future__ import annotations
22
33import fnmatch
4+ import logging
45import threading
6+ import time
57from pathlib import Path
6- from typing import Any
78from typing import Callable
89from typing import Generator
10+ from typing import Iterable
11+ from typing import Tuple
12+ from typing import TypeVar
913
1014import watchfiles
1115from django .utils import autoreload
1216
17+ logger = logging .getLogger ("django_watchfiles" )
18+
19+
20+ # Duplicate `FileChange` type from `watchfiles`, which is not exported
21+ FileChange = Tuple [watchfiles .Change , str ]
22+
23+
24+ T = TypeVar ("T" )
25+
26+
27+ def watch_safely (f : Callable [[], Iterable [T ]], default : T ) -> Iterable [T ]:
28+ """
29+ Yield from `f()`, but when it fails, yield `default` once, log the exception and
30+ retry, unless there are 2 exceptions within 1 second, in which case the exception
31+ is raised.
32+ """
33+ ts : float | None = None
34+ while True :
35+ try :
36+ yield from f ()
37+ except Exception as e :
38+ current_ts = time .monotonic ()
39+ if ts is not None and current_ts - ts < 1.0 :
40+ # Exit after 2 exceptions within 1 second to avoid endlessly looping
41+ raise
42+ logger .warn (e , exc_info = True )
43+ ts = current_ts
44+ yield default
45+
1346
1447class MutableWatcher :
1548 """
@@ -34,17 +67,21 @@ def set_roots(self, roots: set[Path]) -> None:
3467 def stop (self ) -> None :
3568 self .stop_event .set ()
3669
37- def __iter__ (self ) -> Generator [Any , None , None ]: # TODO: better type
70+ def __iter__ (self ) -> Generator [set [FileChange ], None , None ]:
71+ no_changes : set [FileChange ] = set ()
3872 while True :
3973 self .change_event .clear ()
40- for changes in watchfiles .watch (
41- * self .roots ,
42- watch_filter = self .filter ,
43- stop_event = self .stop_event ,
44- debounce = False ,
45- rust_timeout = 100 ,
46- yield_on_timeout = True ,
47- ignore_permission_denied = True ,
74+ for changes in watch_safely (
75+ lambda : watchfiles .watch (
76+ * self .roots ,
77+ watch_filter = self .filter ,
78+ stop_event = self .stop_event ,
79+ debounce = False ,
80+ rust_timeout = 100 ,
81+ yield_on_timeout = True ,
82+ ignore_permission_denied = True ,
83+ ),
84+ default = no_changes ,
4885 ):
4986 if self .change_event .is_set ():
5087 break
0 commit comments