543 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			543 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
""":module: watchdog.events
 | 
						|
:synopsis: File system events and event handlers.
 | 
						|
:author: yesudeep@google.com (Yesudeep Mangalapilly)
 | 
						|
:author: contact@tiger-222.fr (Mickaël Schoentgen)
 | 
						|
 | 
						|
Event Classes
 | 
						|
-------------
 | 
						|
.. autoclass:: FileSystemEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
   :inherited-members:
 | 
						|
 | 
						|
.. autoclass:: FileSystemMovedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: FileMovedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: DirMovedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: FileModifiedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: DirModifiedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: FileCreatedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: FileClosedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: FileClosedNoWriteEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: FileOpenedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: DirCreatedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: FileDeletedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: DirDeletedEvent
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
 | 
						|
Event Handler Classes
 | 
						|
---------------------
 | 
						|
.. autoclass:: FileSystemEventHandler
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: PatternMatchingEventHandler
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: RegexMatchingEventHandler
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
.. autoclass:: LoggingEventHandler
 | 
						|
   :members:
 | 
						|
   :show-inheritance:
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import logging
 | 
						|
import os.path
 | 
						|
import re
 | 
						|
from dataclasses import dataclass, field
 | 
						|
from typing import TYPE_CHECKING
 | 
						|
 | 
						|
from watchdog.utils.patterns import match_any_paths
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from collections.abc import Generator
 | 
						|
 | 
						|
EVENT_TYPE_MOVED = "moved"
 | 
						|
EVENT_TYPE_DELETED = "deleted"
 | 
						|
EVENT_TYPE_CREATED = "created"
 | 
						|
EVENT_TYPE_MODIFIED = "modified"
 | 
						|
EVENT_TYPE_CLOSED = "closed"
 | 
						|
EVENT_TYPE_CLOSED_NO_WRITE = "closed_no_write"
 | 
						|
EVENT_TYPE_OPENED = "opened"
 | 
						|
 | 
						|
 | 
						|
@dataclass(unsafe_hash=True)
 | 
						|
class FileSystemEvent:
 | 
						|
    """Immutable type that represents a file system event that is triggered
 | 
						|
    when a change occurs on the monitored file system.
 | 
						|
 | 
						|
    All FileSystemEvent objects are required to be immutable and hence
 | 
						|
    can be used as keys in dictionaries or be added to sets.
 | 
						|
    """
 | 
						|
 | 
						|
    src_path: bytes | str
 | 
						|
    dest_path: bytes | str = ""
 | 
						|
    event_type: str = field(default="", init=False)
 | 
						|
    is_directory: bool = field(default=False, init=False)
 | 
						|
 | 
						|
    """
 | 
						|
    True if event was synthesized; False otherwise.
 | 
						|
    These are events that weren't actually broadcast by the OS, but
 | 
						|
    are presumed to have happened based on other, actual events.
 | 
						|
    """
 | 
						|
    is_synthetic: bool = field(default=False)
 | 
						|
 | 
						|
 | 
						|
class FileSystemMovedEvent(FileSystemEvent):
 | 
						|
    """File system event representing any kind of file system movement."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_MOVED
 | 
						|
 | 
						|
 | 
						|
# File events.
 | 
						|
 | 
						|
 | 
						|
class FileDeletedEvent(FileSystemEvent):
 | 
						|
    """File system event representing file deletion on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_DELETED
 | 
						|
 | 
						|
 | 
						|
class FileModifiedEvent(FileSystemEvent):
 | 
						|
    """File system event representing file modification on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_MODIFIED
 | 
						|
 | 
						|
 | 
						|
class FileCreatedEvent(FileSystemEvent):
 | 
						|
    """File system event representing file creation on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_CREATED
 | 
						|
 | 
						|
 | 
						|
class FileMovedEvent(FileSystemMovedEvent):
 | 
						|
    """File system event representing file movement on the file system."""
 | 
						|
 | 
						|
 | 
						|
class FileClosedEvent(FileSystemEvent):
 | 
						|
    """File system event representing file close on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_CLOSED
 | 
						|
 | 
						|
 | 
						|
class FileClosedNoWriteEvent(FileSystemEvent):
 | 
						|
    """File system event representing an unmodified file close on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_CLOSED_NO_WRITE
 | 
						|
 | 
						|
 | 
						|
class FileOpenedEvent(FileSystemEvent):
 | 
						|
    """File system event representing file close on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_OPENED
 | 
						|
 | 
						|
 | 
						|
# Directory events.
 | 
						|
 | 
						|
 | 
						|
class DirDeletedEvent(FileSystemEvent):
 | 
						|
    """File system event representing directory deletion on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_DELETED
 | 
						|
    is_directory = True
 | 
						|
 | 
						|
 | 
						|
class DirModifiedEvent(FileSystemEvent):
 | 
						|
    """File system event representing directory modification on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_MODIFIED
 | 
						|
    is_directory = True
 | 
						|
 | 
						|
 | 
						|
class DirCreatedEvent(FileSystemEvent):
 | 
						|
    """File system event representing directory creation on the file system."""
 | 
						|
 | 
						|
    event_type = EVENT_TYPE_CREATED
 | 
						|
    is_directory = True
 | 
						|
 | 
						|
 | 
						|
class DirMovedEvent(FileSystemMovedEvent):
 | 
						|
    """File system event representing directory movement on the file system."""
 | 
						|
 | 
						|
    is_directory = True
 | 
						|
 | 
						|
 | 
						|
class FileSystemEventHandler:
 | 
						|
    """Base file system event handler that you can override methods from."""
 | 
						|
 | 
						|
    def dispatch(self, event: FileSystemEvent) -> None:
 | 
						|
        """Dispatches events to the appropriate methods.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            The event object representing the file system event.
 | 
						|
        :type event:
 | 
						|
            :class:`FileSystemEvent`
 | 
						|
        """
 | 
						|
        self.on_any_event(event)
 | 
						|
        getattr(self, f"on_{event.event_type}")(event)
 | 
						|
 | 
						|
    def on_any_event(self, event: FileSystemEvent) -> None:
 | 
						|
        """Catch-all event handler.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            The event object representing the file system event.
 | 
						|
        :type event:
 | 
						|
            :class:`FileSystemEvent`
 | 
						|
        """
 | 
						|
 | 
						|
    def on_moved(self, event: DirMovedEvent | FileMovedEvent) -> None:
 | 
						|
        """Called when a file or a directory is moved or renamed.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            Event representing file/directory movement.
 | 
						|
        :type event:
 | 
						|
            :class:`DirMovedEvent` or :class:`FileMovedEvent`
 | 
						|
        """
 | 
						|
 | 
						|
    def on_created(self, event: DirCreatedEvent | FileCreatedEvent) -> None:
 | 
						|
        """Called when a file or directory is created.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            Event representing file/directory creation.
 | 
						|
        :type event:
 | 
						|
            :class:`DirCreatedEvent` or :class:`FileCreatedEvent`
 | 
						|
        """
 | 
						|
 | 
						|
    def on_deleted(self, event: DirDeletedEvent | FileDeletedEvent) -> None:
 | 
						|
        """Called when a file or directory is deleted.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            Event representing file/directory deletion.
 | 
						|
        :type event:
 | 
						|
            :class:`DirDeletedEvent` or :class:`FileDeletedEvent`
 | 
						|
        """
 | 
						|
 | 
						|
    def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
 | 
						|
        """Called when a file or directory is modified.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            Event representing file/directory modification.
 | 
						|
        :type event:
 | 
						|
            :class:`DirModifiedEvent` or :class:`FileModifiedEvent`
 | 
						|
        """
 | 
						|
 | 
						|
    def on_closed(self, event: FileClosedEvent) -> None:
 | 
						|
        """Called when a file opened for writing is closed.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            Event representing file closing.
 | 
						|
        :type event:
 | 
						|
            :class:`FileClosedEvent`
 | 
						|
        """
 | 
						|
 | 
						|
    def on_closed_no_write(self, event: FileClosedNoWriteEvent) -> None:
 | 
						|
        """Called when a file opened for reading is closed.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            Event representing file closing.
 | 
						|
        :type event:
 | 
						|
            :class:`FileClosedNoWriteEvent`
 | 
						|
        """
 | 
						|
 | 
						|
    def on_opened(self, event: FileOpenedEvent) -> None:
 | 
						|
        """Called when a file is opened.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            Event representing file opening.
 | 
						|
        :type event:
 | 
						|
            :class:`FileOpenedEvent`
 | 
						|
        """
 | 
						|
 | 
						|
 | 
						|
class PatternMatchingEventHandler(FileSystemEventHandler):
 | 
						|
    """Matches given patterns with file paths associated with occurring events.
 | 
						|
    Uses pathlib's `PurePath.match()` method. `patterns` and `ignore_patterns`
 | 
						|
    are expected to be a list of strings.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        patterns: list[str] | None = None,
 | 
						|
        ignore_patterns: list[str] | None = None,
 | 
						|
        ignore_directories: bool = False,
 | 
						|
        case_sensitive: bool = False,
 | 
						|
    ):
 | 
						|
        super().__init__()
 | 
						|
 | 
						|
        self._patterns = patterns
 | 
						|
        self._ignore_patterns = ignore_patterns
 | 
						|
        self._ignore_directories = ignore_directories
 | 
						|
        self._case_sensitive = case_sensitive
 | 
						|
 | 
						|
    @property
 | 
						|
    def patterns(self) -> list[str] | None:
 | 
						|
        """(Read-only)
 | 
						|
        Patterns to allow matching event paths.
 | 
						|
        """
 | 
						|
        return self._patterns
 | 
						|
 | 
						|
    @property
 | 
						|
    def ignore_patterns(self) -> list[str] | None:
 | 
						|
        """(Read-only)
 | 
						|
        Patterns to ignore matching event paths.
 | 
						|
        """
 | 
						|
        return self._ignore_patterns
 | 
						|
 | 
						|
    @property
 | 
						|
    def ignore_directories(self) -> bool:
 | 
						|
        """(Read-only)
 | 
						|
        ``True`` if directories should be ignored; ``False`` otherwise.
 | 
						|
        """
 | 
						|
        return self._ignore_directories
 | 
						|
 | 
						|
    @property
 | 
						|
    def case_sensitive(self) -> bool:
 | 
						|
        """(Read-only)
 | 
						|
        ``True`` if path names should be matched sensitive to case; ``False``
 | 
						|
        otherwise.
 | 
						|
        """
 | 
						|
        return self._case_sensitive
 | 
						|
 | 
						|
    def dispatch(self, event: FileSystemEvent) -> None:
 | 
						|
        """Dispatches events to the appropriate methods.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            The event object representing the file system event.
 | 
						|
        :type event:
 | 
						|
            :class:`FileSystemEvent`
 | 
						|
        """
 | 
						|
        if self.ignore_directories and event.is_directory:
 | 
						|
            return
 | 
						|
 | 
						|
        paths = []
 | 
						|
        if hasattr(event, "dest_path"):
 | 
						|
            paths.append(os.fsdecode(event.dest_path))
 | 
						|
        if event.src_path:
 | 
						|
            paths.append(os.fsdecode(event.src_path))
 | 
						|
 | 
						|
        if match_any_paths(
 | 
						|
            paths,
 | 
						|
            included_patterns=self.patterns,
 | 
						|
            excluded_patterns=self.ignore_patterns,
 | 
						|
            case_sensitive=self.case_sensitive,
 | 
						|
        ):
 | 
						|
            super().dispatch(event)
 | 
						|
 | 
						|
 | 
						|
class RegexMatchingEventHandler(FileSystemEventHandler):
 | 
						|
    """Matches given regexes with file paths associated with occurring events.
 | 
						|
    Uses the `re` module.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        regexes: list[str] | None = None,
 | 
						|
        ignore_regexes: list[str] | None = None,
 | 
						|
        ignore_directories: bool = False,
 | 
						|
        case_sensitive: bool = False,
 | 
						|
    ):
 | 
						|
        super().__init__()
 | 
						|
 | 
						|
        if regexes is None:
 | 
						|
            regexes = [r".*"]
 | 
						|
        elif isinstance(regexes, str):
 | 
						|
            regexes = [regexes]
 | 
						|
        if ignore_regexes is None:
 | 
						|
            ignore_regexes = []
 | 
						|
        if case_sensitive:
 | 
						|
            self._regexes = [re.compile(r) for r in regexes]
 | 
						|
            self._ignore_regexes = [re.compile(r) for r in ignore_regexes]
 | 
						|
        else:
 | 
						|
            self._regexes = [re.compile(r, re.IGNORECASE) for r in regexes]
 | 
						|
            self._ignore_regexes = [re.compile(r, re.IGNORECASE) for r in ignore_regexes]
 | 
						|
        self._ignore_directories = ignore_directories
 | 
						|
        self._case_sensitive = case_sensitive
 | 
						|
 | 
						|
    @property
 | 
						|
    def regexes(self) -> list[re.Pattern[str]]:
 | 
						|
        """(Read-only)
 | 
						|
        Regexes to allow matching event paths.
 | 
						|
        """
 | 
						|
        return self._regexes
 | 
						|
 | 
						|
    @property
 | 
						|
    def ignore_regexes(self) -> list[re.Pattern[str]]:
 | 
						|
        """(Read-only)
 | 
						|
        Regexes to ignore matching event paths.
 | 
						|
        """
 | 
						|
        return self._ignore_regexes
 | 
						|
 | 
						|
    @property
 | 
						|
    def ignore_directories(self) -> bool:
 | 
						|
        """(Read-only)
 | 
						|
        ``True`` if directories should be ignored; ``False`` otherwise.
 | 
						|
        """
 | 
						|
        return self._ignore_directories
 | 
						|
 | 
						|
    @property
 | 
						|
    def case_sensitive(self) -> bool:
 | 
						|
        """(Read-only)
 | 
						|
        ``True`` if path names should be matched sensitive to case; ``False``
 | 
						|
        otherwise.
 | 
						|
        """
 | 
						|
        return self._case_sensitive
 | 
						|
 | 
						|
    def dispatch(self, event: FileSystemEvent) -> None:
 | 
						|
        """Dispatches events to the appropriate methods.
 | 
						|
 | 
						|
        :param event:
 | 
						|
            The event object representing the file system event.
 | 
						|
        :type event:
 | 
						|
            :class:`FileSystemEvent`
 | 
						|
        """
 | 
						|
        if self.ignore_directories and event.is_directory:
 | 
						|
            return
 | 
						|
 | 
						|
        paths = []
 | 
						|
        if hasattr(event, "dest_path"):
 | 
						|
            paths.append(os.fsdecode(event.dest_path))
 | 
						|
        if event.src_path:
 | 
						|
            paths.append(os.fsdecode(event.src_path))
 | 
						|
 | 
						|
        if any(r.match(p) for r in self.ignore_regexes for p in paths):
 | 
						|
            return
 | 
						|
 | 
						|
        if any(r.match(p) for r in self.regexes for p in paths):
 | 
						|
            super().dispatch(event)
 | 
						|
 | 
						|
 | 
						|
class LoggingEventHandler(FileSystemEventHandler):
 | 
						|
    """Logs all the events captured."""
 | 
						|
 | 
						|
    def __init__(self, *, logger: logging.Logger | None = None) -> None:
 | 
						|
        super().__init__()
 | 
						|
        self.logger = logger or logging.root
 | 
						|
 | 
						|
    def on_moved(self, event: DirMovedEvent | FileMovedEvent) -> None:
 | 
						|
        super().on_moved(event)
 | 
						|
 | 
						|
        what = "directory" if event.is_directory else "file"
 | 
						|
        self.logger.info("Moved %s: from %s to %s", what, event.src_path, event.dest_path)
 | 
						|
 | 
						|
    def on_created(self, event: DirCreatedEvent | FileCreatedEvent) -> None:
 | 
						|
        super().on_created(event)
 | 
						|
 | 
						|
        what = "directory" if event.is_directory else "file"
 | 
						|
        self.logger.info("Created %s: %s", what, event.src_path)
 | 
						|
 | 
						|
    def on_deleted(self, event: DirDeletedEvent | FileDeletedEvent) -> None:
 | 
						|
        super().on_deleted(event)
 | 
						|
 | 
						|
        what = "directory" if event.is_directory else "file"
 | 
						|
        self.logger.info("Deleted %s: %s", what, event.src_path)
 | 
						|
 | 
						|
    def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
 | 
						|
        super().on_modified(event)
 | 
						|
 | 
						|
        what = "directory" if event.is_directory else "file"
 | 
						|
        self.logger.info("Modified %s: %s", what, event.src_path)
 | 
						|
 | 
						|
    def on_closed(self, event: FileClosedEvent) -> None:
 | 
						|
        super().on_closed(event)
 | 
						|
 | 
						|
        self.logger.info("Closed modified file: %s", event.src_path)
 | 
						|
 | 
						|
    def on_closed_no_write(self, event: FileClosedNoWriteEvent) -> None:
 | 
						|
        super().on_closed_no_write(event)
 | 
						|
 | 
						|
        self.logger.info("Closed read file: %s", event.src_path)
 | 
						|
 | 
						|
    def on_opened(self, event: FileOpenedEvent) -> None:
 | 
						|
        super().on_opened(event)
 | 
						|
 | 
						|
        self.logger.info("Opened file: %s", event.src_path)
 | 
						|
 | 
						|
 | 
						|
def generate_sub_moved_events(
 | 
						|
    src_dir_path: bytes | str,
 | 
						|
    dest_dir_path: bytes | str,
 | 
						|
) -> Generator[DirMovedEvent | FileMovedEvent]:
 | 
						|
    """Generates an event list of :class:`DirMovedEvent` and
 | 
						|
    :class:`FileMovedEvent` objects for all the files and directories within
 | 
						|
    the given moved directory that were moved along with the directory.
 | 
						|
 | 
						|
    :param src_dir_path:
 | 
						|
        The source path of the moved directory.
 | 
						|
    :param dest_dir_path:
 | 
						|
        The destination path of the moved directory.
 | 
						|
    :returns:
 | 
						|
        An iterable of file system events of type :class:`DirMovedEvent` and
 | 
						|
        :class:`FileMovedEvent`.
 | 
						|
    """
 | 
						|
    for root, directories, filenames in os.walk(dest_dir_path):  # type: ignore[type-var]
 | 
						|
        for directory in directories:
 | 
						|
            full_path = os.path.join(root, directory)  # type: ignore[call-overload]
 | 
						|
            renamed_path = full_path.replace(dest_dir_path, src_dir_path) if src_dir_path else ""
 | 
						|
            yield DirMovedEvent(renamed_path, full_path, is_synthetic=True)
 | 
						|
        for filename in filenames:
 | 
						|
            full_path = os.path.join(root, filename)  # type: ignore[call-overload]
 | 
						|
            renamed_path = full_path.replace(dest_dir_path, src_dir_path) if src_dir_path else ""
 | 
						|
            yield FileMovedEvent(renamed_path, full_path, is_synthetic=True)
 | 
						|
 | 
						|
 | 
						|
def generate_sub_created_events(src_dir_path: bytes | str) -> Generator[DirCreatedEvent | FileCreatedEvent]:
 | 
						|
    """Generates an event list of :class:`DirCreatedEvent` and
 | 
						|
    :class:`FileCreatedEvent` objects for all the files and directories within
 | 
						|
    the given moved directory that were moved along with the directory.
 | 
						|
 | 
						|
    :param src_dir_path:
 | 
						|
        The source path of the created directory.
 | 
						|
    :returns:
 | 
						|
        An iterable of file system events of type :class:`DirCreatedEvent` and
 | 
						|
        :class:`FileCreatedEvent`.
 | 
						|
    """
 | 
						|
    for root, directories, filenames in os.walk(src_dir_path):  # type: ignore[type-var]
 | 
						|
        for directory in directories:
 | 
						|
            full_path = os.path.join(root, directory)  # type: ignore[call-overload]
 | 
						|
            yield DirCreatedEvent(full_path, is_synthetic=True)
 | 
						|
        for filename in filenames:
 | 
						|
            full_path = os.path.join(root, filename)  # type: ignore[call-overload]
 | 
						|
            yield FileCreatedEvent(full_path, is_synthetic=True)
 |