src/eric7/EricCore/EricFileSystemWatcher.py

branch
eric7
changeset 10679
4d3e0ce54322
child 10685
a9134b4e8ed0
equal deleted inserted replaced
10678:665f1084ebf9 10679:4d3e0ce54322
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a QFileSystemWatcher replacement based on the 'watchdog' package.
8 """
9
10 import os
11
12 from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
13 from watchdog.events import EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED, FileSystemEventHandler
14 from watchdog.observers import Observer
15
16
17 class _EricFileSystemEventHandler(QObject, FileSystemEventHandler):
18 """
19 Class implementing a QObject based file system event handler for watchdog.
20
21 @signal directoryChanged(path: str) emitted when the directory at a given path is
22 modified or removed from disk
23 @signal directoryCreated(path: str) emitted when a directory is created
24 @signal directoryDeleted(path: str) emitted when a directory is removed from disk
25 @signal directoryModified(path: str) emitted when a directory is modified
26 @signal directoryMoved(srcPath: str, dstPath: str) emitted when the directory at a
27 given source path is renamed or moved to destination path
28 @signal fileChanged(path: str) emitted when the file at a given path is modified
29 or removed from disk
30 @signal fileCreated(path: str) emitted when a file is created
31 @signal fileDeleted(path: str) emitted when a file is removed from disk
32 @signal fileModified(path: str) emitted when a file is modified
33 @signal fileMoved(srcPath: str, dstPath: str) emitted when the file at a
34 given source path is renamed or moved to destination path
35 """
36
37 directoryChanged = pyqtSignal(str) # compatibility with QFileSystemWatcher
38 directoryCreated = pyqtSignal(str)
39 directoryDeleted = pyqtSignal(str)
40 directoryModified = pyqtSignal(str)
41 directoryMoved = pyqtSignal(str, str)
42
43 fileChanged = pyqtSignal(str) # compatibility with QFileSystemWatcher
44 fileCreated = pyqtSignal(str)
45 fileDeleted = pyqtSignal(str)
46 fileModified = pyqtSignal(str)
47 fileMoved = pyqtSignal(str, str)
48
49 def __init__(self, parent=None):
50 """
51 Constructor
52
53 @param parent reference to the parent object (defaults to None)
54 @type QObject (optional)
55 """
56 super().__init__(parent=parent)
57
58 def on_any_event(self, event):
59 """
60 Private method handling any file system event.
61
62 @param event event to be handled
63 @type watchdog.events.FileSystemEvent
64 """
65 super().on_any_event(event)
66
67 if event.event_type not in (EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED):
68 if event.is_directory:
69 self.directoryChanged.emit(event.src_path)
70 else:
71 self.fileChanged.emit(event.src_path)
72
73 def on_created(self, event):
74 """
75 Private method to handle a creation event.
76
77 @param event event to be handled
78 @type watchdog.events.FileCreatedEvent or watchdog.event.DirCreatedEvent
79 """
80 super().on_created(event)
81
82 if event.is_directory:
83 self.directoryCreated.emit(event.src_path)
84 else:
85 self.fileCreated.emit(event.src_path)
86
87 def on_deleted(self, event):
88 """
89 Private method to handle a deletion event.
90
91 @param event event to be handled
92 @type watchdog.events.FileDeletedEvent or watchdog.event.DirDeletedEvent
93 """
94 super().on_deleted(event)
95
96 if event.is_directory:
97 self.directoryDeleted.emit(event.src_path)
98 else:
99 self.fileDeleted.emit(event.src_path)
100
101 def on_modified(self, event):
102 """
103 Private method to handle a modification event.
104
105 @param event event to be handled
106 @type watchdog.events.FileModifiedEvent or watchdog.event.DirModifiedEvent
107 """
108 super().on_modified(event)
109
110 if event.is_directory:
111 self.directoryModified.emit(event.src_path)
112 else:
113 self.fileModified.emit(event.src_path)
114
115 def on_moved(self, event):
116 """
117 Private method to handle a move or rename event.
118
119 @param event event to be handled
120 @type watchdog.events.FileMovedEvent or watchdog.event.DirMovedEvent
121 """
122 super().on_moved(event)
123
124 if event.is_directory:
125 self.directoryMoved.emit(event.src_path, event.dest_path)
126 else:
127 self.fileMoved.emit(event.src_path, event.dest_path)
128
129
130 class EricFileSystemWatcher(QObject):
131 """
132 Class implementing a file system monitor based on the 'watchdog' package as a
133 replacement for 'QFileSystemWatcher'.
134
135 This class has more fine grained signaling capability compared to
136 QFileSystemWatcher. The 'directoryChanged' and 'fileChanged' signals are emitted
137 to keep the API compatible with QFileSystemWatcher.
138
139 @signal directoryChanged(path: str) emitted when the directory at a given path is
140 modified or removed from disk
141 @signal directoryCreated(path: str) emitted when a directory is created
142 @signal directoryDeleted(path: str) emitted when a directory is removed from disk
143 @signal directoryModified(path: str) emitted when a directory is modified
144 @signal directoryMoved(srcPath: str, dstPath: str) emitted when the directory at a
145 given source path is renamed or moved to destination path
146 @signal fileChanged(path: str) emitted when the file at a given path is modified
147 or removed from disk
148 @signal fileCreated(path: str) emitted when a file is created
149 @signal fileDeleted(path: str) emitted when a file is removed from disk
150 @signal fileModified(path: str) emitted when a file is modified
151 @signal fileMoved(srcPath: str, dstPath: str) emitted when the file at a
152 given source path is renamed or moved to destination path
153 """
154
155 directoryChanged = pyqtSignal(str) # compatibility with QFileSystemWatcher
156 directoryCreated = pyqtSignal(str)
157 directoryDeleted = pyqtSignal(str)
158 directoryModified = pyqtSignal(str)
159 directoryMoved = pyqtSignal(str, str)
160
161 fileChanged = pyqtSignal(str) # compatibility with QFileSystemWatcher
162 fileCreated = pyqtSignal(str)
163 fileDeleted = pyqtSignal(str)
164 fileModified = pyqtSignal(str)
165 fileMoved = pyqtSignal(str, str)
166
167 def __init__(self, parent=None):
168 """
169 Constructor
170
171 @param parent reference to the parent object (defaults to None)
172 @type QObject (optional)
173 """
174 super().__init__(parent=parent)
175
176 self.__paths = {}
177 # key: file/directory path, value: tuple with created watch and monitor count
178
179 self.__eventHandler = _EricFileSystemEventHandler(parent=self)
180 self.__eventHandler.directoryChanged.connect(self.__directoryChanged)
181 self.__eventHandler.directoryCreated.connect(self.directoryCreated)
182 self.__eventHandler.directoryDeleted.connect(self.directoryDeleted)
183 self.__eventHandler.directoryModified.connect(self.directoryModified)
184 self.__eventHandler.directoryMoved.connect(self.directoryMoved)
185 self.__eventHandler.fileChanged.connect(self.__fileChanged)
186 self.__eventHandler.fileCreated.connect(self.fileCreated)
187 self.__eventHandler.fileDeleted.connect(self.fileDeleted)
188 self.__eventHandler.fileModified.connect(self.fileModified)
189 self.__eventHandler.fileMoved.connect(self.fileMoved)
190
191 self.__observer = Observer()
192 self.__observer.start()
193
194 def __del__(self):
195 """
196 Special method called when an instance object is about to be destroyed.
197 """
198 self.shutdown()
199
200 @pyqtSlot(str)
201 def __directoryChanged(self, path):
202 """
203 Private slot handling any change of a directory at a given path.
204
205 It emits the signal 'directoryChanged', if the path is in the list of
206 watched paths. This behavior is compatible with the QFileSystemWatcher signal
207 of identical name.
208
209 @param path path name of the changed directory
210 @type str
211 """
212 if path in self.__paths:
213 self.directoryChanged.emit(path)
214
215 @pyqtSlot(str)
216 def __fileChanged(self, path):
217 """
218 Private slot handling any change of a file at a given path.
219
220 It emits the signal 'fileChanged', if the path is in the list of
221 watched paths. This behavior is compatible with the QFileSystemWatcher signal
222 of identical name.
223
224 @param path path name of the changed file
225 @type str
226 """
227 if path in self.__paths:
228 self.fileChanged.emit(path)
229
230 def addPath(self, path, recursive=False):
231 """
232 Public method to add the given path to the list of watched paths.
233
234 If the given path is a directory, a recursive watch may be requested.
235 Otherwise only the given directory is watched for changes but none of its
236 subdirectories.
237
238 The path is not added, if it does not exist or if it is already being monitored
239 by the file system watcher.
240
241 @param path file or directory path to be added to the watched paths
242 @type str
243 @param recursive flag indicating a watch for the complete tree rooted at the
244 given path (for directory paths only) (defaults to False)
245 @type bool (optional)
246 @return flag indicating a successful creation of a watch for the given path
247 @rtype bool
248 """
249 if os.path.exists(path):
250 try:
251 self.__paths[path][1] += 1
252 return True
253 except KeyError:
254 watch = self.__observer.schedule(
255 self.__eventHandler,
256 path,
257 recursive=recursive and os.path.isdir(path),
258 )
259 if watch is not None:
260 self.__paths[watch.path] = [watch, 1]
261 return True
262
263 return False
264
265 def addPaths(self, paths, recursive=False):
266 """
267 Public method to add each path of the given list to the file system watched.
268
269 If a path of the given paths list is a directory, a recursive watch may be
270 requested. Otherwise only the given directory is watched for changes but none
271 of its subdirectories. This applies to all directory paths of the given list.
272
273 A path of the list is not added, if it does not exist or if it is already being
274 monitored by the file system watcher.
275
276 @param paths list of file or directory paths to be added to the watched paths
277 @type list of str
278 @param recursive flag indicating a watch for the complete tree rooted at the
279 given path (for directory paths only) (defaults to False)
280 @type bool (optional)
281 @return list of paths that could not be added to the list of monitored paths
282 @rtype list of str
283 """
284 failedPaths = []
285
286 for path in paths:
287 ok = self.addPath(path, recursive=recursive)
288 if not ok:
289 failedPaths.append(path)
290
291 return failedPaths
292
293 def removePath(self, path):
294 """
295 Public method to remove a given path from the list of monitored paths.
296
297 @param path directory or file path to be removed
298 @type str
299 @return flag indicating a successful removal. The only reason for a failure is,
300 if the given path is not currently being monitored.
301 @rtype bool
302 """
303 try:
304 self.__paths[path][1] -= 1
305 if self.__paths[path][1] == 0:
306 watch, _ = self.__paths.pop(path)
307 self.__observer.unschedule(watch)
308 return True
309 except KeyError:
310 return False
311
312 def removePaths(self, paths):
313 """
314 Public method to remove the specified paths from the list of monitored paths.
315
316 @param paths list of directory or file paths to be removed
317 @type list of str
318 @return list of paths that could not be removed from the list of monitored paths
319 @rtype list of str
320 """
321 failedPaths = []
322
323 for path in paths:
324 ok = self.removePath(path)
325 if not ok:
326 failedPaths.append(path)
327
328 return failedPaths
329
330 def directories(self):
331 """
332 Public method to return a list of paths to directories that are being watched.
333
334 @return list of watched directory paths
335 @rtype list of str
336 """
337 return [p for p in self.__paths if os.path.isdir(p)]
338
339 def files(self):
340 """
341 Public method to return a list of paths to files that are being watched.
342
343 @return list of watched file paths
344 @rtype list of str
345 """
346 return [p for p in self.__paths if os.path.isfile(p)]
347
348 def paths(self):
349 """
350 Public method to return a list of paths that are being watched.
351
352 @return list of all watched paths
353 @rtype list of str
354 """
355 return list(self.__paths.keys())
356
357 def shutdown(self):
358 """
359 Public method to shut down the file system watcher instance.
360
361 This needs to be done in order to stop the monitoring threads doing their
362 work in the background. If this method was not called explicitly when the
363 instance is about to be destroyed, the special method '__del__' will do that.
364 """
365 if self.__observer is not None:
366 self.__observer.stop()
367 self.__observer.join()
368 self.__observer = None
369
370
371 _GlobalFileSystemWatcher = None
372
373
374 def instance():
375 """
376 Function to get a reference to the global file system monitor object.
377
378 If the global file system monitor does not exist yet, it will be created
379 automatically.
380
381 @return reference to the global file system monitor object
382 @rtype EricFileSystemWatcher
383 """
384 global _GlobalFileSystemWatcher
385
386 if _GlobalFileSystemWatcher is None:
387 _GlobalFileSystemWatcher = EricFileSystemWatcher()
388
389 return _GlobalFileSystemWatcher

eric ide

mercurial