2626from typing import Any , BinaryIO
2727
2828import watchdog .events
29- import watchdog .observers . polling
29+ import watchdog .observers
3030
3131_SCRIPT_TEMPLATE_STR = """
3232var livereload = function(epoch, requestId) {
@@ -133,14 +133,15 @@ def __init__(
133133
134134 self ._shutdown = False
135135 self .serve_thread = threading .Thread (target = lambda : self .serve_forever (shutdown_delay ))
136- self .observer = watchdog .observers .polling . PollingObserver (timeout = polling_interval )
136+ self .observer = watchdog .observers .Observer (timeout = polling_interval )
137137
138138 self ._watched_paths : dict [str , int ] = {}
139139 self ._watch_refs : dict [str , Any ] = {}
140+ self ._extra_watch_refs : dict [str , list [Any ]] = {}
140141
141142 def watch (self , path : str , func : None = None , * , recursive : bool = True ) -> None :
142143 """Add the 'path' to watched paths, call the function and reload when any file changes under it."""
143- path = os .path .abspath (path )
144+ path = os .path .realpath (path )
144145 if not (func is None or func is self .builder ): # type: ignore[unreachable]
145146 raise TypeError ("Plugins can no longer pass a 'func' parameter to watch()." )
146147
@@ -162,14 +163,79 @@ def callback(event):
162163 log .debug (f"Watching '{ path } '" )
163164 self ._watch_refs [path ] = self .observer .schedule (handler , path , recursive = recursive )
164165
166+ # Watch symlink targets outside the watched tree so that native file system
167+ # observers (inotify, FSEvents) detect changes to symlinked files.
168+ if recursive and os .path .isdir (path ):
169+ self ._watch_symlink_targets (path , callback )
170+
171+ def _watch_symlink_targets (self , root : str , callback : Callable ) -> None :
172+ file_targets : set [str ] = set ()
173+ dir_targets : set [str ] = set ()
174+ self ._collect_symlink_targets (root , root , file_targets , dir_targets )
175+ if not file_targets and not dir_targets :
176+ return
177+
178+ def filtered_callback (event : Any ) -> None :
179+ if event .is_directory :
180+ return None
181+ src = os .path .realpath (event .src_path )
182+ if src in file_targets :
183+ return callback (event )
184+ for d in dir_targets :
185+ if src .startswith (d + os .sep ):
186+ return callback (event )
187+ return None
188+
189+ filtered_handler = watchdog .events .FileSystemEventHandler ()
190+ filtered_handler .on_any_event = filtered_callback # type: ignore[method-assign]
191+
192+ extra_refs : list [Any ] = []
193+ watched_dirs : set [str ] = set ()
194+ for target in file_targets :
195+ parent = os .path .dirname (target )
196+ if parent not in watched_dirs :
197+ watched_dirs .add (parent )
198+ extra_refs .append (self .observer .schedule (filtered_handler , parent , recursive = False ))
199+ for d in dir_targets :
200+ if d not in watched_dirs :
201+ watched_dirs .add (d )
202+ extra_refs .append (self .observer .schedule (filtered_handler , d , recursive = True ))
203+ if extra_refs :
204+ self ._extra_watch_refs [root ] = extra_refs
205+
206+ def _collect_symlink_targets (
207+ self , scan_dir : str , root : str , file_targets : set [str ], dir_targets : set [str ]
208+ ) -> None :
209+ try :
210+ entries = list (os .scandir (scan_dir ))
211+ except OSError :
212+ return
213+ for entry in entries :
214+ if entry .is_symlink ():
215+ try :
216+ target = os .path .realpath (entry .path )
217+ except OSError :
218+ continue
219+ if target == root or target .startswith (root + os .sep ):
220+ continue
221+ if os .path .isdir (target ):
222+ dir_targets .add (target )
223+ self ._collect_symlink_targets (target , root , file_targets , dir_targets )
224+ elif os .path .isfile (target ):
225+ file_targets .add (target )
226+ elif entry .is_dir (follow_symlinks = False ):
227+ self ._collect_symlink_targets (entry .path , root , file_targets , dir_targets )
228+
165229 def unwatch (self , path : str ) -> None :
166230 """Stop watching file changes for path. Raises if there was no corresponding `watch` call."""
167- path = os .path .abspath (path )
231+ path = os .path .realpath (path )
168232
169233 self ._watched_paths [path ] -= 1
170234 if self ._watched_paths [path ] <= 0 :
171235 self ._watched_paths .pop (path )
172236 self .observer .unschedule (self ._watch_refs .pop (path ))
237+ for ref in self ._extra_watch_refs .pop (path , []):
238+ self .observer .unschedule (ref )
173239
174240 def serve (self , * , open_in_browser = False ):
175241 self .server_bind ()
0 commit comments