1""":module: watchdog.events
2:synopsis: File system events and event handlers.
3:author: [email protected] (Yesudeep Mangalapilly)
4:author: [email protected] (Mickaël Schoentgen)
5
6Event Classes
7-------------
8.. autoclass:: FileSystemEvent
9   :members:
10   :show-inheritance:
11   :inherited-members:
12
13.. autoclass:: FileSystemMovedEvent
14   :members:
15   :show-inheritance:
16
17.. autoclass:: FileMovedEvent
18   :members:
19   :show-inheritance:
20
21.. autoclass:: DirMovedEvent
22   :members:
23   :show-inheritance:
24
25.. autoclass:: FileModifiedEvent
26   :members:
27   :show-inheritance:
28
29.. autoclass:: DirModifiedEvent
30   :members:
31   :show-inheritance:
32
33.. autoclass:: FileCreatedEvent
34   :members:
35   :show-inheritance:
36
37.. autoclass:: FileClosedEvent
38   :members:
39   :show-inheritance:
40
41.. autoclass:: FileClosedNoWriteEvent
42   :members:
43   :show-inheritance:
44
45.. autoclass:: FileOpenedEvent
46   :members:
47   :show-inheritance:
48
49.. autoclass:: DirCreatedEvent
50   :members:
51   :show-inheritance:
52
53.. autoclass:: FileDeletedEvent
54   :members:
55   :show-inheritance:
56
57.. autoclass:: DirDeletedEvent
58   :members:
59   :show-inheritance:
60
61
62Event Handler Classes
63---------------------
64.. autoclass:: FileSystemEventHandler
65   :members:
66   :show-inheritance:
67
68.. autoclass:: PatternMatchingEventHandler
69   :members:
70   :show-inheritance:
71
72.. autoclass:: RegexMatchingEventHandler
73   :members:
74   :show-inheritance:
75
76.. autoclass:: LoggingEventHandler
77   :members:
78   :show-inheritance:
79
80"""
81
82from __future__ import annotations
83
84import logging
85import os.path
86import re
87from dataclasses import dataclass, field
88from typing import TYPE_CHECKING
89
90from watchdog.utils.patterns import match_any_paths
91
92if TYPE_CHECKING:
93    from collections.abc import Generator
94
95EVENT_TYPE_MOVED = "moved"
96EVENT_TYPE_DELETED = "deleted"
97EVENT_TYPE_CREATED = "created"
98EVENT_TYPE_MODIFIED = "modified"
99EVENT_TYPE_CLOSED = "closed"
100EVENT_TYPE_CLOSED_NO_WRITE = "closed_no_write"
101EVENT_TYPE_OPENED = "opened"
102
103
104@dataclass(unsafe_hash=True)
105class FileSystemEvent:
106    """Immutable type that represents a file system event that is triggered
107    when a change occurs on the monitored file system.
108
109    All FileSystemEvent objects are required to be immutable and hence
110    can be used as keys in dictionaries or be added to sets.
111    """
112
113    src_path: bytes | str
114    dest_path: bytes | str = ""
115    event_type: str = field(default="", init=False)
116    is_directory: bool = field(default=False, init=False)
117
118    """
119    True if event was synthesized; False otherwise.
120    These are events that weren't actually broadcast by the OS, but
121    are presumed to have happened based on other, actual events.
122    """
123    is_synthetic: bool = field(default=False)
124
125
126class FileSystemMovedEvent(FileSystemEvent):
127    """File system event representing any kind of file system movement."""
128
129    event_type = EVENT_TYPE_MOVED
130
131
132# File events.
133
134
135class FileDeletedEvent(FileSystemEvent):
136    """File system event representing file deletion on the file system."""
137
138    event_type = EVENT_TYPE_DELETED
139
140
141class FileModifiedEvent(FileSystemEvent):
142    """File system event representing file modification on the file system."""
143
144    event_type = EVENT_TYPE_MODIFIED
145
146
147class FileCreatedEvent(FileSystemEvent):
148    """File system event representing file creation on the file system."""
149
150    event_type = EVENT_TYPE_CREATED
151
152
153class FileMovedEvent(FileSystemMovedEvent):
154    """File system event representing file movement on the file system."""
155
156
157class FileClosedEvent(FileSystemEvent):
158    """File system event representing file close on the file system."""
159
160    event_type = EVENT_TYPE_CLOSED
161
162
163class FileClosedNoWriteEvent(FileSystemEvent):
164    """File system event representing an unmodified file close on the file system."""
165
166    event_type = EVENT_TYPE_CLOSED_NO_WRITE
167
168
169class FileOpenedEvent(FileSystemEvent):
170    """File system event representing file close on the file system."""
171
172    event_type = EVENT_TYPE_OPENED
173
174
175# Directory events.
176
177
178class DirDeletedEvent(FileSystemEvent):
179    """File system event representing directory deletion on the file system."""
180
181    event_type = EVENT_TYPE_DELETED
182    is_directory = True
183
184
185class DirModifiedEvent(FileSystemEvent):
186    """File system event representing directory modification on the file system."""
187
188    event_type = EVENT_TYPE_MODIFIED
189    is_directory = True
190
191
192class DirCreatedEvent(FileSystemEvent):
193    """File system event representing directory creation on the file system."""
194
195    event_type = EVENT_TYPE_CREATED
196    is_directory = True
197
198
199class DirMovedEvent(FileSystemMovedEvent):
200    """File system event representing directory movement on the file system."""
201
202    is_directory = True
203
204
205class FileSystemEventHandler:
206    """Base file system event handler that you can override methods from."""
207
208    def dispatch(self, event: FileSystemEvent) -> None:
209        """Dispatches events to the appropriate methods.
210
211        :param event:
212            The event object representing the file system event.
213        :type event:
214            :class:`FileSystemEvent`
215        """
216        self.on_any_event(event)
217        getattr(self, f"on_{event.event_type}")(event)
218
219    def on_any_event(self, event: FileSystemEvent) -> None:
220        """Catch-all event handler.
221
222        :param event:
223            The event object representing the file system event.
224        :type event:
225            :class:`FileSystemEvent`
226        """
227
228    def on_moved(self, event: DirMovedEvent | FileMovedEvent) -> None:
229        """Called when a file or a directory is moved or renamed.
230
231        :param event:
232            Event representing file/directory movement.
233        :type event:
234            :class:`DirMovedEvent` or :class:`FileMovedEvent`
235        """
236
237    def on_created(self, event: DirCreatedEvent | FileCreatedEvent) -> None:
238        """Called when a file or directory is created.
239
240        :param event:
241            Event representing file/directory creation.
242        :type event:
243            :class:`DirCreatedEvent` or :class:`FileCreatedEvent`
244        """
245
246    def on_deleted(self, event: DirDeletedEvent | FileDeletedEvent) -> None:
247        """Called when a file or directory is deleted.
248
249        :param event:
250            Event representing file/directory deletion.
251        :type event:
252            :class:`DirDeletedEvent` or :class:`FileDeletedEvent`
253        """
254
255    def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
256        """Called when a file or directory is modified.
257
258        :param event:
259            Event representing file/directory modification.
260        :type event:
261            :class:`DirModifiedEvent` or :class:`FileModifiedEvent`
262        """
263
264    def on_closed(self, event: FileClosedEvent) -> None:
265        """Called when a file opened for writing is closed.
266
267        :param event:
268            Event representing file closing.
269        :type event:
270            :class:`FileClosedEvent`
271        """
272
273    def on_closed_no_write(self, event: FileClosedNoWriteEvent) -> None:
274        """Called when a file opened for reading is closed.
275
276        :param event:
277            Event representing file closing.
278        :type event:
279            :class:`FileClosedNoWriteEvent`
280        """
281
282    def on_opened(self, event: FileOpenedEvent) -> None:
283        """Called when a file is opened.
284
285        :param event:
286            Event representing file opening.
287        :type event:
288            :class:`FileOpenedEvent`
289        """
290
291
292class PatternMatchingEventHandler(FileSystemEventHandler):
293    """Matches given patterns with file paths associated with occurring events.
294    Uses pathlib's `PurePath.match()` method. `patterns` and `ignore_patterns`
295    are expected to be a list of strings.
296    """
297
298    def __init__(
299        self,
300        *,
301        patterns: list[str] | None = None,
302        ignore_patterns: list[str] | None = None,
303        ignore_directories: bool = False,
304        case_sensitive: bool = False,
305    ):
306        super().__init__()
307
308        self._patterns = patterns
309        self._ignore_patterns = ignore_patterns
310        self._ignore_directories = ignore_directories
311        self._case_sensitive = case_sensitive
312
313    @property
314    def patterns(self) -> list[str] | None:
315        """(Read-only)
316        Patterns to allow matching event paths.
317        """
318        return self._patterns
319
320    @property
321    def ignore_patterns(self) -> list[str] | None:
322        """(Read-only)
323        Patterns to ignore matching event paths.
324        """
325        return self._ignore_patterns
326
327    @property
328    def ignore_directories(self) -> bool:
329        """(Read-only)
330        ``True`` if directories should be ignored; ``False`` otherwise.
331        """
332        return self._ignore_directories
333
334    @property
335    def case_sensitive(self) -> bool:
336        """(Read-only)
337        ``True`` if path names should be matched sensitive to case; ``False``
338        otherwise.
339        """
340        return self._case_sensitive
341
342    def dispatch(self, event: FileSystemEvent) -> None:
343        """Dispatches events to the appropriate methods.
344
345        :param event:
346            The event object representing the file system event.
347        :type event:
348            :class:`FileSystemEvent`
349        """
350        if self.ignore_directories and event.is_directory:
351            return
352
353        paths = []
354        if hasattr(event, "dest_path"):
355            paths.append(os.fsdecode(event.dest_path))
356        if event.src_path:
357            paths.append(os.fsdecode(event.src_path))
358
359        if match_any_paths(
360            paths,
361            included_patterns=self.patterns,
362            excluded_patterns=self.ignore_patterns,
363            case_sensitive=self.case_sensitive,
364        ):
365            super().dispatch(event)
366
367
368class RegexMatchingEventHandler(FileSystemEventHandler):
369    """Matches given regexes with file paths associated with occurring events.
370    Uses the `re` module.
371    """
372
373    def __init__(
374        self,
375        *,
376        regexes: list[str] | None = None,
377        ignore_regexes: list[str] | None = None,
378        ignore_directories: bool = False,
379        case_sensitive: bool = False,
380    ):
381        super().__init__()
382
383        if regexes is None:
384            regexes = [r".*"]
385        elif isinstance(regexes, str):
386            regexes = [regexes]
387        if ignore_regexes is None:
388            ignore_regexes = []
389        if case_sensitive:
390            self._regexes = [re.compile(r) for r in regexes]
391            self._ignore_regexes = [re.compile(r) for r in ignore_regexes]
392        else:
393            self._regexes = [re.compile(r, re.IGNORECASE) for r in regexes]
394            self._ignore_regexes = [re.compile(r, re.IGNORECASE) for r in ignore_regexes]
395        self._ignore_directories = ignore_directories
396        self._case_sensitive = case_sensitive
397
398    @property
399    def regexes(self) -> list[re.Pattern[str]]:
400        """(Read-only)
401        Regexes to allow matching event paths.
402        """
403        return self._regexes
404
405    @property
406    def ignore_regexes(self) -> list[re.Pattern[str]]:
407        """(Read-only)
408        Regexes to ignore matching event paths.
409        """
410        return self._ignore_regexes
411
412    @property
413    def ignore_directories(self) -> bool:
414        """(Read-only)
415        ``True`` if directories should be ignored; ``False`` otherwise.
416        """
417        return self._ignore_directories
418
419    @property
420    def case_sensitive(self) -> bool:
421        """(Read-only)
422        ``True`` if path names should be matched sensitive to case; ``False``
423        otherwise.
424        """
425        return self._case_sensitive
426
427    def dispatch(self, event: FileSystemEvent) -> None:
428        """Dispatches events to the appropriate methods.
429
430        :param event:
431            The event object representing the file system event.
432        :type event:
433            :class:`FileSystemEvent`
434        """
435        if self.ignore_directories and event.is_directory:
436            return
437
438        paths = []
439        if hasattr(event, "dest_path"):
440            paths.append(os.fsdecode(event.dest_path))
441        if event.src_path:
442            paths.append(os.fsdecode(event.src_path))
443
444        if any(r.match(p) for r in self.ignore_regexes for p in paths):
445            return
446
447        if any(r.match(p) for r in self.regexes for p in paths):
448            super().dispatch(event)
449
450
451class LoggingEventHandler(FileSystemEventHandler):
452    """Logs all the events captured."""
453
454    def __init__(self, *, logger: logging.Logger | None = None) -> None:
455        super().__init__()
456        self.logger = logger or logging.root
457
458    def on_moved(self, event: DirMovedEvent | FileMovedEvent) -> None:
459        super().on_moved(event)
460
461        what = "directory" if event.is_directory else "file"
462        self.logger.info("Moved %s: from %s to %s", what, event.src_path, event.dest_path)
463
464    def on_created(self, event: DirCreatedEvent | FileCreatedEvent) -> None:
465        super().on_created(event)
466
467        what = "directory" if event.is_directory else "file"
468        self.logger.info("Created %s: %s", what, event.src_path)
469
470    def on_deleted(self, event: DirDeletedEvent | FileDeletedEvent) -> None:
471        super().on_deleted(event)
472
473        what = "directory" if event.is_directory else "file"
474        self.logger.info("Deleted %s: %s", what, event.src_path)
475
476    def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
477        super().on_modified(event)
478
479        what = "directory" if event.is_directory else "file"
480        self.logger.info("Modified %s: %s", what, event.src_path)
481
482    def on_closed(self, event: FileClosedEvent) -> None:
483        super().on_closed(event)
484
485        self.logger.info("Closed modified file: %s", event.src_path)
486
487    def on_closed_no_write(self, event: FileClosedNoWriteEvent) -> None:
488        super().on_closed_no_write(event)
489
490        self.logger.info("Closed read file: %s", event.src_path)
491
492    def on_opened(self, event: FileOpenedEvent) -> None:
493        super().on_opened(event)
494
495        self.logger.info("Opened file: %s", event.src_path)
496
497
498def generate_sub_moved_events(
499    src_dir_path: bytes | str,
500    dest_dir_path: bytes | str,
501) -> Generator[DirMovedEvent | FileMovedEvent]:
502    """Generates an event list of :class:`DirMovedEvent` and
503    :class:`FileMovedEvent` objects for all the files and directories within
504    the given moved directory that were moved along with the directory.
505
506    :param src_dir_path:
507        The source path of the moved directory.
508    :param dest_dir_path:
509        The destination path of the moved directory.
510    :returns:
511        An iterable of file system events of type :class:`DirMovedEvent` and
512        :class:`FileMovedEvent`.
513    """
514    for root, directories, filenames in os.walk(dest_dir_path):  # type: ignore[type-var]
515        for directory in directories:
516            full_path = os.path.join(root, directory)  # type: ignore[call-overload]
517            renamed_path = full_path.replace(dest_dir_path, src_dir_path) if src_dir_path else ""
518            yield DirMovedEvent(renamed_path, full_path, is_synthetic=True)
519        for filename in filenames:
520            full_path = os.path.join(root, filename)  # type: ignore[call-overload]
521            renamed_path = full_path.replace(dest_dir_path, src_dir_path) if src_dir_path else ""
522            yield FileMovedEvent(renamed_path, full_path, is_synthetic=True)
523
524
525def generate_sub_created_events(src_dir_path: bytes | str) -> Generator[DirCreatedEvent | FileCreatedEvent]:
526    """Generates an event list of :class:`DirCreatedEvent` and
527    :class:`FileCreatedEvent` objects for all the files and directories within
528    the given moved directory that were moved along with the directory.
529
530    :param src_dir_path:
531        The source path of the created directory.
532    :returns:
533        An iterable of file system events of type :class:`DirCreatedEvent` and
534        :class:`FileCreatedEvent`.
535    """
536    for root, directories, filenames in os.walk(src_dir_path):  # type: ignore[type-var]
537        for directory in directories:
538            full_path = os.path.join(root, directory)  # type: ignore[call-overload]
539            yield DirCreatedEvent(full_path, is_synthetic=True)
540        for filename in filenames:
541            full_path = os.path.join(root, filename)  # type: ignore[call-overload]
542            yield FileCreatedEvent(full_path, is_synthetic=True)
543