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