1
1
from __future__ import annotations
2
2
3
3
import fnmatch
4
+ import logging
4
5
import threading
6
+ import time
5
7
from pathlib import Path
6
- from typing import Any
7
8
from typing import Callable
8
9
from typing import Generator
10
+ from typing import Iterable
11
+ from typing import Tuple
12
+ from typing import TypeVar
9
13
10
14
import watchfiles
11
15
from django .utils import autoreload
12
16
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 .warning (e , exc_info = True )
43
+ ts = current_ts
44
+ yield default
45
+
13
46
14
47
class MutableWatcher :
15
48
"""
@@ -34,17 +67,21 @@ def set_roots(self, roots: set[Path]) -> None:
34
67
def stop (self ) -> None :
35
68
self .stop_event .set ()
36
69
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 ()
38
72
while True :
39
73
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 ,
48
85
):
49
86
if self .change_event .is_set ():
50
87
break
0 commit comments