From 5edb8b922b43c5ebd513bed4e7830cb32a45b69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 12 Feb 2024 14:25:11 +0100 Subject: [PATCH] [core] Run ruff, apply several fixes - remove execution rights from `events.py`, and `watchmedo.py`, files - code simpliciations here and there - use a proxy function to guess the best observer class import --- .cirrus.yml | 2 +- .github/workflows/tests.yml | 21 +-- .isort.cfg | 5 - changelog.rst | 1 + pyproject.toml | 74 +++++++++- requirements-tests.txt | 3 +- setup.cfg | 15 -- src/watchdog/events.py | 46 ++----- src/watchdog/observers/__init__.py | 65 +++++---- src/watchdog/observers/api.py | 63 ++++----- src/watchdog/observers/fsevents.py | 27 +--- src/watchdog/observers/fsevents2.py | 18 +-- src/watchdog/observers/inotify.py | 17 +-- src/watchdog/observers/inotify_c.py | 78 +++++------ src/watchdog/observers/kqueue.py | 130 ++++++------------ src/watchdog/observers/polling.py | 21 ++- .../observers/read_directory_changes.py | 6 +- src/watchdog/observers/winapi.py | 10 +- src/watchdog/tricks/__init__.py | 17 +-- src/watchdog/utils/__init__.py | 28 ++-- src/watchdog/utils/bricks.py | 4 +- src/watchdog/utils/delayed_queue.py | 5 +- src/watchdog/utils/dirsnapshot.py | 67 ++++----- src/watchdog/utils/echo.py | 8 +- src/watchdog/utils/patterns.py | 13 +- src/watchdog/utils/platform.py | 13 +- src/watchdog/watchmedo.py | 71 ++++------ tox.ini | 27 ++-- 28 files changed, 359 insertions(+), 496 deletions(-) delete mode 100644 .isort.cfg mode change 100755 => 100644 src/watchdog/events.py mode change 100755 => 100644 src/watchdog/watchmedo.py diff --git a/.cirrus.yml b/.cirrus.yml index 488ce7915..b305a1ad3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -16,6 +16,6 @@ task: - python3.8 -m pip install -U pip - python3.8 -m pip install -r requirements-tests.txt lint_script: - - python3.8 -m flake8 docs src tests tools + - python3.8 -m ruff src tests_script: - python3.8 -bb -m pytest tests diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9f9fdca07..308f7442e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,8 +5,6 @@ on: branches: - master pull_request: - branches: - - '**' concurrency: group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name != 'pull_request' && github.sha || '' }} @@ -21,12 +19,12 @@ jobs: fail-fast: false matrix: tox: + - name: Types + environment: types + timeout: 15 - name: Test environment: py timeout: 15 - - name: mypy - environment: mypy - timeout: 15 os: - name: Linux matrix: linux @@ -50,17 +48,8 @@ jobs: - "pypy-3.9" include: - tox: - name: Flake8 - environment: flake8 - timeout: 5 - python: "3.11" - os: - name: Linux - emoji: šŸ§ - runs-on: [ubuntu-latest] - - tox: - name: isort - environment: isort-ci + name: Linter + environment: lint timeout: 5 python: "3.11" os: diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 755720270..000000000 --- a/.isort.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[settings] -line_length = 120 -profile=black -skip_gitignore=true -add_imports=from __future__ import annotations diff --git a/changelog.rst b/changelog.rst index 53d2d8be3..9fb97311c 100644 --- a/changelog.rst +++ b/changelog.rst @@ -8,6 +8,7 @@ Changelog 2024-xx-xx ā€¢ `full history `__ +- [core] Run ``ruff``, apply several fixes (`#1033 `__) - [inotify] Fix missing ``event_filter`` for the full emitter (`#1032 `__) - Thanks to our beloved contributors: @mraspaud diff --git a/pyproject.toml b/pyproject.toml index 9fdacdf5c..9075ee5cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,71 @@ -[tool.black] -target-version = ["py38"] +[tool.mypy] +# Ensure we know what we do +warn_redundant_casts = true +warn_unused_ignores = true +warn_unused_configs = true + +# Imports management +ignore_missing_imports = true +follow_imports = "skip" + +# Ensure full coverage +disallow_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_calls = true + +# Restrict dynamic typing (a little) +# e.g. `x: List[Any]` or x: List` +# disallow_any_generics = true + +strict_equality = true + +[tool.pytest] +addopts = """ + --showlocals + -v + --cov=watchdog + --cov-report=term-missing +""" + +[tool.ruff] line-length = 120 -safe = true +indent-width = 4 +target-version = "py38" + +[tool.ruff.lint] +extend-select = ["ALL"] +ignore = [ + "ARG", + "ANN", # TODO: remove + "ANN002", + "ANN003", + "ANN401", + "B006", + "B023", # TODO: remove + "B028", + "BLE001", + "C90", + "COM", + "D", + "EM", + "ERA", + "FBT", + "FIX", + "ISC001", + "N", # Requires a major version number bump + "PERF203", # TODO: remove + "PL", + "PTH", # TODO: remove? + "S", + "TD", + "TRY003", + "UP", # TODO: when minimum python version will be 3.10 +] +fixable = ["ALL"] -[tool.isort] -profile = "black" +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = true diff --git a/requirements-tests.txt b/requirements-tests.txt index 44a39e6eb..60e375bf4 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,10 +1,9 @@ eventlet -flake8 flaky -isort pytest pytest-cov pytest-timeout +ruff sphinx mypy types-PyYAML diff --git a/setup.cfg b/setup.cfg index 8c5532bd0..61c7f61c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,21 +10,6 @@ source-dir = docs/source build-dir = docs/build all_files = 1 -[flake8] -ignore = - # E203 whitespace before ':', but E203 is not PEP 8 compliant - E203 - # W503 line break before binary operator, but W503 is not PEP 8 compliant - W503 -max-line-length = 120 - [upload_sphinx] # Requires sphinx-pypi-upload to work. upload-dir = docs/build/html - -[tool:pytest] -addopts = - --showlocals - -v - --cov=watchdog - --cov-report=term-missing diff --git a/src/watchdog/events.py b/src/watchdog/events.py old mode 100755 new mode 100644 index 51c6c5bf8..9b11faa46 --- a/src/watchdog/events.py +++ b/src/watchdog/events.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -:module: watchdog.events +""":module: watchdog.events :synopsis: File system events and event handlers. :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) @@ -111,8 +110,7 @@ @dataclass(unsafe_hash=True) class FileSystemEvent: - """ - Immutable type that represents a file system event that is triggered + """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 @@ -186,9 +184,7 @@ class DirDeletedEvent(FileSystemEvent): class DirModifiedEvent(FileSystemEvent): - """ - File system event representing directory modification on the file system. - """ + """File system event representing directory modification on the file system.""" event_type = EVENT_TYPE_MODIFIED is_directory = True @@ -208,9 +204,7 @@ class DirMovedEvent(FileSystemMovedEvent): class FileSystemEventHandler: - """ - Base file system event handler that you can override methods from. - """ + """Base file system event handler that you can override methods from.""" def dispatch(self, event: FileSystemEvent) -> None: """Dispatches events to the appropriate methods. @@ -295,9 +289,7 @@ def on_opened(self, event: FileSystemEvent) -> None: class PatternMatchingEventHandler(FileSystemEventHandler): - """ - Matches given patterns with file paths associated with occurring events. - """ + """Matches given patterns with file paths associated with occurring events.""" def __init__( self, @@ -315,32 +307,28 @@ def __init__( @property def patterns(self): - """ - (Read-only) + """(Read-only) Patterns to allow matching event paths. """ return self._patterns @property def ignore_patterns(self): - """ - (Read-only) + """(Read-only) Patterns to ignore matching event paths. """ return self._ignore_patterns @property def ignore_directories(self): - """ - (Read-only) + """(Read-only) ``True`` if directories should be ignored; ``False`` otherwise. """ return self._ignore_directories @property def case_sensitive(self): - """ - (Read-only) + """(Read-only) ``True`` if path names should be matched sensitive to case; ``False`` otherwise. """ @@ -373,9 +361,7 @@ def dispatch(self, event: FileSystemEvent) -> None: class RegexMatchingEventHandler(FileSystemEventHandler): - """ - Matches given regexes with file paths associated with occurring events. - """ + """Matches given regexes with file paths associated with occurring events.""" def __init__( self, @@ -403,32 +389,28 @@ def __init__( @property def regexes(self): - """ - (Read-only) + """(Read-only) Regexes to allow matching event paths. """ return self._regexes @property def ignore_regexes(self): - """ - (Read-only) + """(Read-only) Regexes to ignore matching event paths. """ return self._ignore_regexes @property def ignore_directories(self): - """ - (Read-only) + """(Read-only) ``True`` if directories should be ignored; ``False`` otherwise. """ return self._ignore_directories @property def case_sensitive(self): - """ - (Read-only) + """(Read-only) ``True`` if path names should be matched sensitive to case; ``False`` otherwise. """ diff --git a/src/watchdog/observers/__init__.py b/src/watchdog/observers/__init__.py index ab92c6186..006d470bb 100644 --- a/src/watchdog/observers/__init__.py +++ b/src/watchdog/observers/__init__.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -:module: watchdog.observers +""":module: watchdog.observers :synopsis: Observer that picks a native implementation if available. :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) @@ -52,47 +51,57 @@ from __future__ import annotations +import contextlib import sys import warnings +from typing import TYPE_CHECKING from watchdog.utils import UnsupportedLibc -from .api import BaseObserverSubclassCallable +if TYPE_CHECKING: + from watchdog.observers.api import BaseObserverSubclassCallable -Observer: BaseObserverSubclassCallable +def _get_observer_cls() -> BaseObserverSubclassCallable: + if sys.platform.startswith("linux"): + with contextlib.suppress(UnsupportedLibc): + from watchdog.observers.inotify import InotifyObserver -if sys.platform.startswith("linux"): - try: - from .inotify import InotifyObserver as Observer - except UnsupportedLibc: - from .polling import PollingObserver as Observer + return InotifyObserver -elif sys.platform.startswith("darwin"): - try: - from .fsevents import FSEventsObserver as Observer - except Exception: + elif sys.platform.startswith("darwin"): try: - from .kqueue import KqueueObserver as Observer - - warnings.warn("Failed to import fsevents. Fall back to kqueue") + from watchdog.observers.fsevents import FSEventsObserver except Exception: - from .polling import PollingObserver as Observer + try: + from watchdog.observers.kqueue import KqueueObserver + + warnings.warn("Failed to import fsevents. Fall back to kqueue") + except Exception: + warnings.warn("Failed to import fsevents and kqueue. Fall back to polling.") + else: + return KqueueObserver + else: + return FSEventsObserver + + elif sys.platform.startswith("win"): + try: + from watchdog.observers.read_directory_changes import WindowsApiObserver + except Exception: + warnings.warn("Failed to import `read_directory_changes`. Fall back to polling.") + else: + return WindowsApiObserver + + elif sys.platform in {"dragonfly", "freebsd", "netbsd", "openbsd", "bsd"}: + from watchdog.observers.kqueue import KqueueObserver - warnings.warn("Failed to import fsevents and kqueue. Fall back to polling.") + return KqueueObserver -elif sys.platform in ("dragonfly", "freebsd", "netbsd", "openbsd", "bsd"): - from .kqueue import KqueueObserver as Observer + from watchdog.observers.polling import PollingObserver -elif sys.platform.startswith("win"): - try: - from .read_directory_changes import WindowsApiObserver as Observer - except Exception: - from .polling import PollingObserver as Observer + return PollingObserver - warnings.warn("Failed to import read_directory_changes. Fall back to polling.") -else: - from .polling import PollingObserver as Observer +Observer = _get_observer_cls() __all__ = ["Observer"] diff --git a/src/watchdog/observers/api.py b/src/watchdog/observers/api.py index 50550f328..ba5fc49fe 100644 --- a/src/watchdog/observers/api.py +++ b/src/watchdog/observers/api.py @@ -15,6 +15,7 @@ from __future__ import annotations +import contextlib import queue import threading from pathlib import Path @@ -26,7 +27,6 @@ DEFAULT_OBSERVER_TIMEOUT = 1 # in seconds. -# Collection classes class EventQueue(SkipRepeatsQueue): """Thread-safe event queue based on a special queue that skips adding the same event (:class:`FileSystemEvent`) multiple times consecutively. @@ -48,10 +48,7 @@ class ObservedWatch: """ def __init__(self, path, recursive, event_filter=None): - if isinstance(path, Path): - self._path = str(path) - else: - self._path = path + self._path = str(path) if isinstance(path, Path) else path self._is_recursive = recursive self._event_filter = frozenset(event_filter) if event_filter is not None else None @@ -94,8 +91,7 @@ def __repr__(self): # Observer classes class EventEmitter(BaseThread): - """ - Producer thread base class subclassed by event emitters + """Producer thread base class subclassed by event emitters that generate events and populate a queue with them. :param event_queue: @@ -125,21 +121,16 @@ def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT, event_fi @property def timeout(self): - """ - Blocking timeout for reading events. - """ + """Blocking timeout for reading events.""" return self._timeout @property def watch(self): - """ - The watch associated with this emitter. - """ + """The watch associated with this emitter.""" return self._watch def queue_event(self, event): - """ - Queues a single event. + """Queues a single event. :param event: Event to be queued. @@ -167,8 +158,7 @@ def run(self): class EventDispatcher(BaseThread): - """ - Consumer thread base class subclassed by event observer threads + """Consumer thread base class subclassed by event observer threads that dispatch events from an event queue to appropriate event handlers. :param timeout: @@ -178,7 +168,7 @@ class EventDispatcher(BaseThread): ``float`` """ - _stop_event = object() + stop_event = object() """Event inserted into the queue to signal a requested stop.""" def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT): @@ -193,16 +183,15 @@ def timeout(self): def stop(self): BaseThread.stop(self) - try: - self.event_queue.put_nowait(EventDispatcher._stop_event) - except queue.Full: - pass + with contextlib.suppress(queue.Full): + self.event_queue.put_nowait(EventDispatcher.stop_event) @property def event_queue(self): """The event queue which is populated with file system events by emitters and from which events are dispatched by a dispatcher - thread.""" + thread. + """ return self._event_queue def dispatch_events(self, event_queue): @@ -233,9 +222,9 @@ def __init__(self, emitter_class, timeout=DEFAULT_OBSERVER_TIMEOUT): self._emitter_class = emitter_class self._lock = threading.RLock() self._watches = set() - self._handlers = dict() + self._handlers = {} self._emitters = set() - self._emitter_for_watch = dict() + self._emitter_for_watch = {} def _add_emitter(self, emitter): self._emitter_for_watch[emitter.watch] = emitter @@ -245,19 +234,15 @@ def _remove_emitter(self, emitter): del self._emitter_for_watch[emitter.watch] self._emitters.remove(emitter) emitter.stop() - try: + with contextlib.suppress(RuntimeError): emitter.join() - except RuntimeError: - pass def _clear_emitters(self): for emitter in self._emitters: emitter.stop() for emitter in self._emitters: - try: + with contextlib.suppress(RuntimeError): emitter.join() - except RuntimeError: - pass self._emitters.clear() self._emitter_for_watch.clear() @@ -284,8 +269,7 @@ def start(self): super().start() def schedule(self, event_handler, path, recursive=False, event_filter=None): - """ - Schedules watching a path and calls appropriate methods specified + """Schedules watching a path and calls appropriate methods specified in the given event handler in response to file system events. :param event_handler: @@ -317,8 +301,12 @@ def schedule(self, event_handler, path, recursive=False, event_filter=None): # If we don't have an emitter for this watch already, create it. if self._emitter_for_watch.get(watch) is None: - emitter = self._emitter_class(event_queue=self.event_queue, watch=watch, timeout=self.timeout, - event_filter=event_filter) + emitter = self._emitter_class( + event_queue=self.event_queue, + watch=watch, + timeout=self.timeout, + event_filter=event_filter, + ) if self.is_alive(): emitter.start() self._add_emitter(emitter) @@ -378,7 +366,8 @@ def unschedule(self, watch): def unschedule_all(self): """Unschedules all watches and detaches all associated event - handlers.""" + handlers. + """ with self._lock: self._handlers.clear() self._clear_emitters() @@ -389,7 +378,7 @@ def on_thread_stop(self): def dispatch_events(self, event_queue): entry = event_queue.get(block=True) - if entry is EventDispatcher._stop_event: + if entry is EventDispatcher.stop_event: return event, watch = entry diff --git a/src/watchdog/observers/fsevents.py b/src/watchdog/observers/fsevents.py index 40dce1adc..d75e2c741 100644 --- a/src/watchdog/observers/fsevents.py +++ b/src/watchdog/observers/fsevents.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -:module: watchdog.observers.fsevents +""":module: watchdog.observers.fsevents :synopsis: FSEvents based emitter implementation. :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) @@ -50,9 +49,7 @@ class FSEventsEmitter(EventEmitter): - - """ - macOS FSEvents Emitter class. + """macOS FSEvents Emitter class. :param event_queue: The event queue to fill with events. @@ -97,15 +94,11 @@ def on_thread_stop(self): def queue_event(self, event): # fsevents defaults to be recursive, so if the watch was meant to be non-recursive then we need to drop # all the events here which do not have a src_path / dest_path that matches the watched path - if self._watch.is_recursive: + if self._watch.is_recursive or not self._is_recursive_event(event): logger.debug("queue_event %s", event) EventEmitter.queue_event(self, event) else: - if not self._is_recursive_event(event): - logger.debug("queue_event %s", event) - EventEmitter.queue_event(self, event) - else: - logger.debug("drop event %s", event) + logger.debug("drop event %s", event) def _is_recursive_event(self, event): src_path = event.src_path if event.is_directory else os.path.dirname(event.src_path) @@ -168,7 +161,7 @@ def queue_events(self, timeout, events): if logger.getEffectiveLevel() <= logging.DEBUG: for event in events: flags = ", ".join(attr for attr in dir(event) if getattr(event, attr) is True) - logger.debug(f"{event}: {flags}") + logger.debug("{}: {}", event, flags) if time.monotonic() - self._start_time > 60: # Event history is no longer needed, let's free some memory. @@ -319,18 +312,12 @@ def run(self): def on_thread_start(self): if self.suppress_history: - if isinstance(self.watch.path, bytes): - watch_path = os.fsdecode(self.watch.path) - else: - watch_path = self.watch.path - + watch_path = os.fsdecode(self.watch.path) if isinstance(self.watch.path, bytes) else self.watch.path self._starting_state = DirectorySnapshot(watch_path) def _encode_path(self, path): """Encode path only if bytes were passed to this emitter.""" - if isinstance(self.watch.path, bytes): - return os.fsencode(path) - return path + return os.fsencode(path) if isinstance(self.watch.path, bytes) else path class FSEventsObserver(BaseObserver): diff --git a/src/watchdog/observers/fsevents2.py b/src/watchdog/observers/fsevents2.py index 4b8872e88..8d35fcb82 100644 --- a/src/watchdog/observers/fsevents2.py +++ b/src/watchdog/observers/fsevents2.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -:module: watchdog.observers.fsevents2 +""":module: watchdog.observers.fsevents2 :synopsis: FSEvents based emitter implementation. :platforms: macOS """ @@ -73,7 +72,7 @@ logger = logging.getLogger(__name__) message = "watchdog.observers.fsevents2 is deprecated and will be removed in a future release." -warnings.warn(message, DeprecationWarning) +warnings.warn(message, category=DeprecationWarning) logger.warning(message) @@ -126,19 +125,16 @@ def stop(self): def _callback(self, streamRef, clientCallBackInfo, numEvents, eventPaths, eventFlags, eventIDs): events = [NativeEvent(path, flags, _id) for path, flags, _id in zip(eventPaths, eventFlags, eventIDs)] - logger.debug(f"FSEvents callback. Got {numEvents} events:") + logger.debug("FSEvents callback. Got {} events:", numEvents) for e in events: logger.debug(e) self._queue.put(events) def read_events(self): - """ - Returns a list or one or more events, or None if there are no more + """Returns a list or one or more events, or None if there are no more events to be read. """ - if not self.is_alive(): - return None - return self._queue.get() + return self._queue.get() if self.is_alive() else None class NativeEvent: @@ -181,9 +177,7 @@ def __repr__(self): class FSEventsEmitter(EventEmitter): - """ - FSEvents based event emitter. Handles conversion of native events. - """ + """FSEvents based event emitter. Handles conversion of native events.""" def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT, event_filter=None): super().__init__(event_queue, watch, timeout, event_filter) diff --git a/src/watchdog/observers/inotify.py b/src/watchdog/observers/inotify.py index 1001e4598..395768175 100644 --- a/src/watchdog/observers/inotify.py +++ b/src/watchdog/observers/inotify.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -:module: watchdog.observers.inotify +""":module: watchdog.observers.inotify :synopsis: ``inotify(7)`` based emitter implementation. :author: Sebastien Martini :author: Luke McCarthy @@ -95,8 +94,7 @@ class InotifyEmitter(EventEmitter): - """ - inotify(7)-based event emitter. + """inotify(7)-based event emitter. :param event_queue: The event queue to fill with events. @@ -169,10 +167,7 @@ def queue_events(self, timeout, full_events=False): if event.is_directory and self.watch.is_recursive: for sub_event in generate_sub_created_events(src_path): self.queue_event(sub_event) - elif event.is_attrib: - cls = DirModifiedEvent if event.is_directory else FileModifiedEvent - self.queue_event(cls(src_path)) - elif event.is_modify: + elif event.is_attrib or event.is_modify: cls = DirModifiedEvent if event.is_directory else FileModifiedEvent self.queue_event(cls(src_path)) elif event.is_delete or (event.is_moved_from and not full_events): @@ -240,8 +235,7 @@ def get_event_mask_from_filter(self): class InotifyFullEmitter(InotifyEmitter): - """ - inotify(7)-based event emitter. By default this class produces move events even if they are not matched + """inotify(7)-based event emitter. By default this class produces move events even if they are not matched Such move events will have a ``None`` value for the unmatched part. :param event_queue: @@ -269,8 +263,7 @@ def queue_events(self, timeout, events=True): class InotifyObserver(BaseObserver): - """ - Observer thread that schedules watching directories and dispatches + """Observer thread that schedules watching directories and dispatches calls to event handlers. """ diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py index aec29e2a4..9dc7be473 100644 --- a/src/watchdog/observers/inotify_c.py +++ b/src/watchdog/observers/inotify_c.py @@ -15,6 +15,7 @@ from __future__ import annotations +import contextlib import ctypes import ctypes.util import errno @@ -29,7 +30,7 @@ libc = ctypes.CDLL(None) if not hasattr(libc, "inotify_init") or not hasattr(libc, "inotify_add_watch") or not hasattr(libc, "inotify_rm_watch"): - raise UnsupportedLibc(f"Unsupported libc version found: {libc._name}") + raise UnsupportedLibc(f"Unsupported libc version found: {libc._name}") # noqa:SLF001 inotify_add_watch = ctypes.CFUNCTYPE(c_int, c_int, c_char_p, c_uint32, use_errno=True)(("inotify_add_watch", libc)) @@ -113,8 +114,7 @@ class InotifyConstants: class inotify_event_struct(ctypes.Structure): - """ - Structure representation of the inotify_event structure + """Structure representation of the inotify_event structure (used in buffer size calculations):: struct inotify_event { @@ -126,13 +126,13 @@ class inotify_event_struct(ctypes.Structure): }; """ - _fields_ = [ + _fields_ = ( ("wd", c_int), ("mask", c_uint32), ("cookie", c_uint32), ("len", c_uint32), ("name", c_char_p), - ] + ) EVENT_SIZE = ctypes.sizeof(inotify_event_struct) @@ -141,8 +141,7 @@ class inotify_event_struct(ctypes.Structure): class Inotify: - """ - Linux inotify(7) API wrapper class. + """Linux inotify(7) API wrapper class. :param path: The directory path for which we want an inotify object. @@ -201,27 +200,24 @@ def clear_move_records(self): self._moved_from_events = {} def source_for_move(self, destination_event): - """ - The source path corresponding to the given MOVED_TO event. + """The source path corresponding to the given MOVED_TO event. If the source path is outside the monitored directories, None is returned instead. """ if destination_event.cookie in self._moved_from_events: return self._moved_from_events[destination_event.cookie].src_path - else: - return None + + return None def remember_move_from_event(self, event): - """ - Save this event as the source event for future MOVED_TO events to + """Save this event as the source event for future MOVED_TO events to reference. """ self._moved_from_events[event.cookie] = event def add_watch(self, path): - """ - Adds a watch for the given path. + """Adds a watch for the given path. :param path: Path to begin monitoring. @@ -230,8 +226,7 @@ def add_watch(self, path): self._add_watch(path, self._event_mask) def remove_watch(self, path): - """ - Removes a watch for the given path. + """Removes a watch for the given path. :param path: Path string for which the watch will be removed. @@ -243,24 +238,17 @@ def remove_watch(self, path): Inotify._raise_error() def close(self): - """ - Closes the inotify instance and removes all associated watches. - """ + """Closes the inotify instance and removes all associated watches.""" with self._lock: if self._path in self._wd_for_path: wd = self._wd_for_path[self._path] inotify_rm_watch(self._inotify_fd, wd) - try: + with contextlib.suppress(OSError): os.close(self._inotify_fd) - except OSError: - # descriptor may be invalid because file was deleted - pass def read_events(self, event_buffer_size=DEFAULT_EVENT_BUFFER_SIZE): - """ - Reads events from inotify and yields them. - """ + """Reads events from inotify and yields them.""" # HACK: We need to traverse the directory path # recursively and simulate events for newly # created subdirectories/files. This will handle @@ -270,7 +258,7 @@ def _recursive_simulate(src_path): events = [] for root, dirnames, filenames in os.walk(src_path): for dirname in dirnames: - try: + with contextlib.suppress(OSError): full_path = os.path.join(root, dirname) wd_dir = self._add_watch(full_path, self._event_mask) e = InotifyEvent( @@ -281,8 +269,6 @@ def _recursive_simulate(src_path): full_path, ) events.append(e) - except OSError: - pass for filename in filenames: full_path = os.path.join(root, filename) wd_parent_dir = self._wd_for_path[os.path.dirname(full_path)] @@ -303,10 +289,11 @@ def _recursive_simulate(src_path): except OSError as e: if e.errno == errno.EINTR: continue - elif e.errno == errno.EBADF: + + if e.errno == errno.EBADF: return [] - else: - raise + + raise break with self._lock: @@ -328,7 +315,7 @@ def _recursive_simulate(src_path): self._wd_for_path[inotify_event.src_path] = moved_wd self._path_for_wd[moved_wd] = inotify_event.src_path if self.is_recursive: - for _path, _wd in self._wd_for_path.copy().items(): + for _path in self._wd_for_path.copy(): if _path.startswith(move_src_path + os.path.sep.encode()): moved_wd = self._wd_for_path.pop(_path) _move_to_path = _path.replace(move_src_path, inotify_event.src_path) @@ -364,8 +351,7 @@ def _recursive_simulate(src_path): # Non-synchronized methods. def _add_dir_watch(self, path, recursive, mask): - """ - Adds a watch (optionally recursively) for the given directory path + """Adds a watch (optionally recursively) for the given directory path to monitor events specified by the mask. :param path: @@ -387,8 +373,7 @@ def _add_dir_watch(self, path, recursive, mask): self._add_watch(full_path, mask) def _add_watch(self, path, mask): - """ - Adds a watch for the given path to monitor events specified by the + """Adds a watch for the given path to monitor events specified by the mask. :param path: @@ -405,21 +390,21 @@ def _add_watch(self, path, mask): @staticmethod def _raise_error(): - """ - Raises errors for inotify failures. - """ + """Raises errors for inotify failures.""" err = ctypes.get_errno() + if err == errno.ENOSPC: raise OSError(errno.ENOSPC, "inotify watch limit reached") - elif err == errno.EMFILE: + + if err == errno.EMFILE: raise OSError(errno.EMFILE, "inotify instance limit reached") - elif err != errno.EACCES: + + if err != errno.EACCES: raise OSError(err, os.strerror(err)) @staticmethod def _parse_event_buffer(event_buffer): - """ - Parses an event buffer of ``inotify_event`` structs returned by + """Parses an event buffer of ``inotify_event`` structs returned by inotify:: struct inotify_event { @@ -443,8 +428,7 @@ def _parse_event_buffer(event_buffer): class InotifyEvent: - """ - Inotify event struct wrapper. + """Inotify event struct wrapper. :param wd: Watch descriptor diff --git a/src/watchdog/observers/kqueue.py b/src/watchdog/observers/kqueue.py index 6b54f5e00..665d2b34c 100644 --- a/src/watchdog/observers/kqueue.py +++ b/src/watchdog/observers/kqueue.py @@ -13,17 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - -# The `select` module varies between platforms. -# mypy may complain about missing module attributes -# depending on which platform it's running on. -# The comment below disables mypy's attribute check. -# -# mypy: disable-error-code=attr-defined -# -""" -:module: watchdog.observers.kqueue +""":module: watchdog.observers.kqueue :synopsis: ``kqueue(2)`` based emitter implementation. :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) @@ -74,6 +64,15 @@ """ + +# The `select` module varies between platforms. +# mypy may complain about missing module attributes depending on which platform it's running on. +# The comment below disables mypy's attribute check. +# mypy: disable-error-code=attr-defined + +from __future__ import annotations + +import contextlib import errno import os import os.path @@ -106,10 +105,7 @@ O_EVTONLY = 0x8000 # Pre-calculated values for the kevent filter, flags, and fflags attributes. -if platform.is_darwin(): - WATCHDOG_OS_OPEN_FLAGS = O_EVTONLY -else: - WATCHDOG_OS_OPEN_FLAGS = os.O_RDONLY | os.O_NONBLOCK +WATCHDOG_OS_OPEN_FLAGS = O_EVTONLY if platform.is_darwin() else os.O_RDONLY | os.O_NONBLOCK WATCHDOG_KQ_FILTER = select.KQ_FILTER_VNODE WATCHDOG_KQ_EV_FLAGS = select.KQ_EV_ADD | select.KQ_EV_ENABLE | select.KQ_EV_CLEAR WATCHDOG_KQ_FFLAGS = ( @@ -152,45 +148,37 @@ def is_renamed(kev): class KeventDescriptorSet: - - """ - Thread-safe kevent descriptor collection. - """ + """Thread-safe kevent descriptor collection.""" def __init__(self): # Set of KeventDescriptor self._descriptors = set() # Descriptor for a given path. - self._descriptor_for_path = dict() + self._descriptor_for_path = {} # Descriptor for a given fd. - self._descriptor_for_fd = dict() + self._descriptor_for_fd = {} # List of kevent objects. - self._kevents = list() + self._kevents = [] self._lock = threading.Lock() @property def kevents(self): - """ - List of kevents monitored. - """ + """List of kevents monitored.""" with self._lock: return self._kevents @property def paths(self): - """ - List of paths for which kevents have been created. - """ + """List of paths for which kevents have been created.""" with self._lock: return list(self._descriptor_for_path.keys()) def get_for_fd(self, fd): - """ - Given a file descriptor, returns the kevent descriptor object + """Given a file descriptor, returns the kevent descriptor object for it. :param fd: @@ -204,8 +192,7 @@ def get_for_fd(self, fd): return self._descriptor_for_fd[fd] def get(self, path): - """ - Obtains a :class:`KeventDescriptor` object for the specified path. + """Obtains a :class:`KeventDescriptor` object for the specified path. :param path: Path for which the descriptor will be obtained. @@ -215,8 +202,7 @@ def get(self, path): return self._get(path) def __contains__(self, path): - """ - Determines whether a :class:`KeventDescriptor has been registered + """Determines whether a :class:`KeventDescriptor has been registered for the specified path. :param path: @@ -227,8 +213,7 @@ def __contains__(self, path): return self._has_path(path) def add(self, path, is_directory): - """ - Adds a :class:`KeventDescriptor` to the collection for the given + """Adds a :class:`KeventDescriptor` to the collection for the given path. :param path: @@ -245,8 +230,7 @@ def add(self, path, is_directory): self._add_descriptor(KeventDescriptor(path, is_directory)) def remove(self, path): - """ - Removes the :class:`KeventDescriptor` object for the given path + """Removes the :class:`KeventDescriptor` object for the given path if it already exists. :param path: @@ -259,9 +243,7 @@ def remove(self, path): self._remove_descriptor(self._get(path)) def clear(self): - """ - Clears the collection and closes all open descriptors. - """ + """Clears the collection and closes all open descriptors.""" with self._lock: for descriptor in self._descriptors: descriptor.close() @@ -277,12 +259,12 @@ def _get(self, path): def _has_path(self, path): """Determines whether a :class:`KeventDescriptor` for the specified - path exists already in the collection.""" + path exists already in the collection. + """ return path in self._descriptor_for_path def _add_descriptor(self, descriptor): - """ - Adds a descriptor to the collection. + """Adds a descriptor to the collection. :param descriptor: An instance of :class:`KeventDescriptor` to be added. @@ -293,8 +275,7 @@ def _add_descriptor(self, descriptor): self._descriptor_for_fd[descriptor.fd] = descriptor def _remove_descriptor(self, descriptor): - """ - Removes a descriptor from the collection. + """Removes a descriptor from the collection. :param descriptor: An instance of :class:`KeventDescriptor` to be removed. @@ -307,9 +288,7 @@ def _remove_descriptor(self, descriptor): class KeventDescriptor: - - """ - A kevent descriptor convenience data structure to keep together: + """A kevent descriptor convenience data structure to keep together: * kevent * directory status @@ -360,13 +339,9 @@ def is_directory(self): return self._is_directory def close(self): - """ - Closes the file descriptor associated with a kevent descriptor. - """ - try: + """Closes the file descriptor associated with a kevent descriptor.""" + with contextlib.suppress(OSError): os.close(self.fd) - except OSError: - pass @property def key(self): @@ -386,9 +361,7 @@ def __repr__(self): class KqueueEmitter(EventEmitter): - - """ - kqueue(2)-based event emitter. + """kqueue(2)-based event emitter. .. ADMONITION:: About ``kqueue(2)`` behavior and this implementation @@ -452,8 +425,7 @@ def custom_stat(path, self=self): self._snapshot = DirectorySnapshot(watch.path, recursive=watch.is_recursive, stat=custom_stat) def _register_kevent(self, path, is_directory): - """ - Registers a kevent descriptor for the given path. + """Registers a kevent descriptor for the given path. :param path: Path for which a kevent descriptor will be created. @@ -495,8 +467,7 @@ def _register_kevent(self, path, is_directory): raise def _unregister_kevent(self, path): - """ - Convenience function to close the kevent descriptor for a + """Convenience function to close the kevent descriptor for a specified kqueue-monitored path. :param path: @@ -505,8 +476,7 @@ def _unregister_kevent(self, path): self._descriptors.remove(path) def queue_event(self, event): - """ - Handles queueing a single event object. + """Handles queueing a single event object. :param event: An instance of :class:`watchdog.events.FileSystemEvent` @@ -526,8 +496,7 @@ def queue_event(self, event): self._unregister_kevent(event.src_path) def _gen_kqueue_events(self, kev, ref_snapshot, new_snapshot): - """ - Generate events from the kevent list returned from the call to + """Generate events from the kevent list returned from the call to :meth:`select.kqueue.control`. .. NOTE:: kqueue only tells us about deletions, file modifications, @@ -543,8 +512,7 @@ def _gen_kqueue_events(self, kev, ref_snapshot, new_snapshot): # Kqueue does not specify the destination names for renames # to, so we have to process these using the a snapshot # of the directory. - for event in self._gen_renamed_events(src_path, descriptor.is_directory, ref_snapshot, new_snapshot): - yield event + yield from self._gen_renamed_events(src_path, descriptor.is_directory, ref_snapshot, new_snapshot) elif is_attrib_modified(kev): if descriptor.is_directory: yield DirModifiedEvent(src_path) @@ -567,14 +535,11 @@ def _gen_kqueue_events(self, kev, ref_snapshot, new_snapshot): yield FileDeletedEvent(src_path) def _parent_dir_modified(self, src_path): - """ - Helper to generate a DirModifiedEvent on the parent of src_path. - """ + """Helper to generate a DirModifiedEvent on the parent of src_path.""" return DirModifiedEvent(os.path.dirname(src_path)) def _gen_renamed_events(self, src_path, is_directory, ref_snapshot, new_snapshot): - """ - Compares information from two directory snapshots (one taken before + """Compares information from two directory snapshots (one taken before the rename operation and another taken right after) to determine the destination path of the file system object renamed, and yields the appropriate events to be queued. @@ -599,21 +564,18 @@ def _gen_renamed_events(self, src_path, is_directory, ref_snapshot, new_snapshot if dest_path is not None: dest_path = absolute_path(dest_path) if is_directory: - event = DirMovedEvent(src_path, dest_path) - yield event + yield DirMovedEvent(src_path, dest_path) else: yield FileMovedEvent(src_path, dest_path) yield self._parent_dir_modified(src_path) yield self._parent_dir_modified(dest_path) - if is_directory: + if is_directory and self.watch.is_recursive: # TODO: Do we need to fire moved events for the items # inside the directory tree? Does kqueue does this # all by itself? Check this and then enable this code # only if it doesn't already. # A: It doesn't. So I've enabled this block. - if self.watch.is_recursive: - for sub_event in generate_sub_moved_events(src_path, dest_path): - yield sub_event + yield from generate_sub_moved_events(src_path, dest_path) else: # If the new snapshot does not have an inode for the # old path, we haven't found the new name. Therefore, @@ -625,8 +587,7 @@ def _gen_renamed_events(self, src_path, is_directory, ref_snapshot, new_snapshot yield self._parent_dir_modified(src_path) def _read_events(self, timeout=None): - """ - Reads events from a call to the blocking + """Reads events from a call to the blocking :meth:`select.kqueue.control()` method. :param timeout: @@ -637,8 +598,7 @@ def _read_events(self, timeout=None): return self._kq.control(self._descriptors.kevents, MAX_EVENTS, timeout) def queue_events(self, timeout): - """ - Queues events by reading them from a call to the blocking + """Queues events by reading them from a call to the blocking :meth:`select.kqueue.control()` method. :param timeout: @@ -683,9 +643,7 @@ def on_thread_stop(self): class KqueueObserver(BaseObserver): - - """ - Observer thread that schedules watching directories and dispatches + """Observer thread that schedules watching directories and dispatches calls to event handlers. """ diff --git a/src/watchdog/observers/polling.py b/src/watchdog/observers/polling.py index af74a6c57..418f46ff7 100644 --- a/src/watchdog/observers/polling.py +++ b/src/watchdog/observers/polling.py @@ -14,8 +14,7 @@ # limitations under the License. -""" -:module: watchdog.observers.polling +""":module: watchdog.observers.polling :synopsis: Polling emitter implementation. :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) @@ -53,8 +52,7 @@ class PollingEmitter(EventEmitter): - """ - Platform-independent emitter that polls a directory to detect file + """Platform-independent emitter that polls a directory to detect file system changes. """ @@ -71,7 +69,10 @@ def __init__( self._snapshot: DirectorySnapshot = EmptyDirectorySnapshot() self._lock = threading.Lock() self._take_snapshot = lambda: DirectorySnapshot( - self.watch.path, self.watch.is_recursive, stat=stat, listdir=listdir + self.watch.path, + self.watch.is_recursive, + stat=stat, + listdir=listdir, ) def on_thread_start(self): @@ -121,8 +122,7 @@ def queue_events(self, timeout): class PollingObserver(BaseObserver): - """ - Platform-independent observer that polls a directory to detect file + """Platform-independent observer that polls a directory to detect file system changes. """ @@ -131,13 +131,10 @@ def __init__(self, timeout=DEFAULT_OBSERVER_TIMEOUT): class PollingObserverVFS(BaseObserver): - """ - File system independent observer that polls a directory to detect changes. - """ + """File system independent observer that polls a directory to detect changes.""" def __init__(self, stat, listdir, polling_interval=1): - """ - :param stat: stat function. See ``os.stat`` for details. + """:param stat: stat function. See ``os.stat`` for details. :param listdir: listdir function. See ``os.scandir`` for details. :type polling_interval: float :param polling_interval: interval in seconds between polling the file system. diff --git a/src/watchdog/observers/read_directory_changes.py b/src/watchdog/observers/read_directory_changes.py index fe038cc20..c0c10ecd7 100644 --- a/src/watchdog/observers/read_directory_changes.py +++ b/src/watchdog/observers/read_directory_changes.py @@ -44,8 +44,7 @@ class WindowsApiEmitter(EventEmitter): - """ - Windows API-based emitter that uses ReadDirectoryChangesW + """Windows API-based emitter that uses ReadDirectoryChangesW to detect file system changes for a watch. """ @@ -110,8 +109,7 @@ def queue_events(self, timeout): class WindowsApiObserver(BaseObserver): - """ - Observer thread that schedules watching directories and dispatches + """Observer thread that schedules watching directories and dispatches calls to event handlers. """ diff --git a/src/watchdog/observers/winapi.py b/src/watchdog/observers/winapi.py index a4956c11d..100158531 100644 --- a/src/watchdog/observers/winapi.py +++ b/src/watchdog/observers/winapi.py @@ -95,14 +95,14 @@ class OVERLAPPED(ctypes.Structure): - _fields_ = [ + _fields_ = ( ("Internal", LPVOID), ("InternalHigh", LPVOID), ("Offset", ctypes.wintypes.DWORD), ("OffsetHigh", ctypes.wintypes.DWORD), ("Pointer", LPVOID), ("hEvent", ctypes.wintypes.HANDLE), - ] + ) def _errcheck_bool(value, func, args): @@ -234,13 +234,13 @@ def _errcheck_dword(value, func, args): class FILE_NOTIFY_INFORMATION(ctypes.Structure): - _fields_ = [ + _fields_ = ( ("NextEntryOffset", ctypes.wintypes.DWORD), ("Action", ctypes.wintypes.DWORD), ("FileNameLength", ctypes.wintypes.DWORD), # ("FileName", (ctypes.wintypes.WCHAR * 1))] ("FileName", (ctypes.c_char * 1)), - ] + ) LPFNI = ctypes.POINTER(FILE_NOTIFY_INFORMATION) @@ -369,7 +369,7 @@ def read_directory_changes(handle, path, recursive): if _is_observed_path_deleted(handle, path): return _generate_observed_path_deleted_event() - raise e + raise return event_buffer.raw, int(nbytes.value) diff --git a/src/watchdog/tricks/__init__.py b/src/watchdog/tricks/__init__.py index 65bf074aa..96c3595ef 100644 --- a/src/watchdog/tricks/__init__.py +++ b/src/watchdog/tricks/__init__.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -:module: watchdog.tricks +""":module: watchdog.tricks :synopsis: Utility event handlers. :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) @@ -39,8 +38,10 @@ """ + from __future__ import annotations +import contextlib import functools import logging import os @@ -60,7 +61,6 @@ class Trick(PatternMatchingEventHandler): - """Your tricks should subclass this class.""" @classmethod @@ -80,7 +80,6 @@ def generate_yaml(cls): class LoggerTrick(Trick): - """A simple trick that does only logs events.""" @echo_events @@ -89,7 +88,6 @@ def on_any_event(self, event: FileSystemEvent) -> None: class ShellCommandTrick(Trick): - """Executes shell commands in response to matched events.""" def __init__( @@ -150,7 +148,8 @@ def on_any_event(self, event): process_watcher = ProcessWatcher(self.process, None) self._process_watchers.add(process_watcher) process_watcher.process_termination_callback = functools.partial( - self._process_watchers.discard, process_watcher + self._process_watchers.discard, + process_watcher, ) process_watcher.start() @@ -159,7 +158,6 @@ def is_process_running(self): class AutoRestartTrick(Trick): - """Starts a long-running subprocess and restarts it on matched events. The command parameter is a list of command arguments, such as @@ -268,11 +266,8 @@ def _stop_process(self): break time.sleep(0.25) else: - try: + with contextlib.suppress(OSError): kill_process(self.process.pid, 9) - except OSError: - # Process is already gone - pass self.process = None finally: self._is_process_stopping = False diff --git a/src/watchdog/utils/__init__.py b/src/watchdog/utils/__init__.py index aea5f4e07..d2d7016df 100644 --- a/src/watchdog/utils/__init__.py +++ b/src/watchdog/utils/__init__.py @@ -14,8 +14,7 @@ # limitations under the License. -""" -:module: watchdog.utils +""":module: watchdog.utils :synopsis: Utility classes and functions. :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) @@ -40,11 +39,7 @@ class UnsupportedLibc(Exception): class WatchdogShutdown(Exception): - """ - Semantic exception used to signal an external shutdown event. - """ - - pass + """Semantic exception used to signal an external shutdown event.""" class BaseThread(threading.Thread): @@ -72,7 +67,6 @@ def on_thread_stop(self): This method is called immediately after the thread is signaled to stop. """ - pass def stop(self): """Signals the thread to stop.""" @@ -84,9 +78,8 @@ def on_thread_start(self): calls this method. This method is called right before this thread is started and this - objectā€™s run() method is invoked. + object's run() method is invoked. """ - pass def start(self): self.on_thread_start() @@ -97,8 +90,8 @@ def load_module(module_name): """Imports a module given its name and returns a handle to it.""" try: __import__(module_name) - except ImportError: - raise ImportError(f"No module named {module_name}") + except ImportError as e: + raise ImportError(f"No module named {module_name}") from e return sys.modules[module_name] @@ -107,11 +100,13 @@ def load_class(dotted_path): specification the last part of the dotted path is the class name and there is at least one module name preceding the class name. - Notes: + Notes + ----- You will need to ensure that the module you are trying to load exists in the Python path. - Examples: + Examples + -------- - module.name.ClassName # Provided module.name is in the Python path. - module.ClassName # Provided module is in the Python path. @@ -119,6 +114,7 @@ def load_class(dotted_path): - ClassName - modle.name.ClassName # Typo in module name. - module.name.ClasNam # Typo in classname. + """ dotted_path_split = dotted_path.split(".") if len(dotted_path_split) <= 1: @@ -131,8 +127,8 @@ def load_class(dotted_path): return getattr(module, klass_name) # Finally create and return an instance of the class # return klass(*args, **kwargs) - else: - raise AttributeError(f"Module {module_name} does not have class attribute {klass_name}") + + raise AttributeError(f"Module {module_name} does not have class attribute {klass_name}") if TYPE_CHECKING or sys.version_info >= (3, 8): diff --git a/src/watchdog/utils/bricks.py b/src/watchdog/utils/bricks.py index 980a0ff67..45a804370 100644 --- a/src/watchdog/utils/bricks.py +++ b/src/watchdog/utils/bricks.py @@ -14,8 +14,7 @@ # limitations under the License. -""" -Utility collections or "bricks". +"""Utility collections or "bricks". :module: watchdog.utils.bricks :author: yesudeep@google.com (Yesudeep Mangalapilly) @@ -40,7 +39,6 @@ class SkipRepeatsQueue(queue.Queue): - """Thread-safe implementation of an special queue where a put of the last-item put'd will be dropped. diff --git a/src/watchdog/utils/delayed_queue.py b/src/watchdog/utils/delayed_queue.py index f3a5ead61..e6f118362 100644 --- a/src/watchdog/utils/delayed_queue.py +++ b/src/watchdog/utils/delayed_queue.py @@ -76,9 +76,10 @@ def get(self) -> Optional[T]: def remove(self, predicate: Callable[[T], bool]) -> Optional[T]: """Remove and return the first items for which predicate is True, - ignoring delay.""" + ignoring delay. + """ with self._lock: - for i, (elem, t, delay) in enumerate(self._queue): + for i, (elem, *_) in enumerate(self._queue): if predicate(elem): del self._queue[i] return elem diff --git a/src/watchdog/utils/dirsnapshot.py b/src/watchdog/utils/dirsnapshot.py index 0a67c33df..0b3c6b025 100644 --- a/src/watchdog/utils/dirsnapshot.py +++ b/src/watchdog/utils/dirsnapshot.py @@ -14,8 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -:module: watchdog.utils.dirsnapshot +""":module: watchdog.utils.dirsnapshot :synopsis: Directory snapshots and comparison. :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) @@ -46,8 +45,10 @@ """ + from __future__ import annotations +import contextlib import errno import os from stat import S_ISDIR @@ -55,8 +56,7 @@ class DirectorySnapshotDiff: - """ - Compares two directory snapshots and creates an object that represents + """Compares two directory snapshots and creates an object that represents the difference between the two snapshots. :param ref: @@ -126,9 +126,10 @@ def get_inode(directory: DirectorySnapshot, full_path: str) -> int | Tuple[int, # first check paths that have not moved modified: set[str] = set() for path in ref.paths & snapshot.paths: - if get_inode(ref, path) == get_inode(snapshot, path): - if ref.mtime(path) != snapshot.mtime(path) or ref.size(path) != snapshot.size(path): - modified.add(path) + if get_inode(ref, path) == get_inode(snapshot, path) and ( + ref.mtime(path) != snapshot.mtime(path) or ref.size(path) != snapshot.size(path) + ): + modified.add(path) for old_path, new_path in moved: if ref.mtime(old_path) != snapshot.mtime(new_path) or ref.size(old_path) != snapshot.size(new_path): @@ -181,8 +182,7 @@ def files_modified(self) -> List[str]: @property def files_moved(self) -> list[Tuple[str, str]]: - """ - List of files that were moved. + """List of files that were moved. Each event is a two-tuple the first item of which is the path that has been renamed to the second item in the tuple. @@ -191,15 +191,12 @@ def files_moved(self) -> list[Tuple[str, str]]: @property def dirs_modified(self) -> List[str]: - """ - List of directories that were modified. - """ + """List of directories that were modified.""" return self._dirs_modified @property def dirs_moved(self) -> List[tuple[str, str]]: - """ - List of directories that were moved. + """List of directories that were moved. Each event is a two-tuple the first item of which is the path that has been renamed to the second item in the tuple. @@ -208,21 +205,16 @@ def dirs_moved(self) -> List[tuple[str, str]]: @property def dirs_deleted(self) -> List[str]: - """ - List of directories that were deleted. - """ + """List of directories that were deleted.""" return self._dirs_deleted @property def dirs_created(self) -> List[str]: - """ - List of directories that were created. - """ + """List of directories that were created.""" return self._dirs_created class ContextManager: - """ - Context manager that creates two directory snapshots and a + """Context manager that creates two directory snapshots and a diff object that represents the difference between the two snapshots. :param path: @@ -289,8 +281,7 @@ def get_snapshot(self): class DirectorySnapshot: - """ - A snapshot of stat information of files in a directory. + """A snapshot of stat information of files in a directory. :param path: The directory path for which a snapshot should be taken. @@ -349,34 +340,25 @@ def walk(self, root: str) -> Iterator[Tuple[str, os.stat_result]]: entries = [] for p in paths: - try: + with contextlib.suppress(OSError): entry = (p, self.stat(p)) entries.append(entry) yield entry - except OSError: - continue if self.recursive: for path, st in entries: - try: + with contextlib.suppress(PermissionError): if S_ISDIR(st.st_mode): - for entry in self.walk(path): - yield entry - except PermissionError: - pass + yield from self.walk(path) @property def paths(self) -> set[str]: - """ - Set of file/directory paths in the snapshot. - """ + """Set of file/directory paths in the snapshot.""" return set(self._stat_info.keys()) - def path(self, id: Tuple[int, int]) -> Optional[str]: - """ - Returns path for id. None if id is unknown to this snapshot. - """ - return self._inode_to_path.get(id) + def path(self, uid: Tuple[int, int]) -> Optional[str]: + """Returns path for id. None if id is unknown to this snapshot.""" + return self._inode_to_path.get(uid) def inode(self, path: str) -> Tuple[int, int]: """Returns an id for path.""" @@ -393,8 +375,7 @@ def size(self, path: str) -> int: return self._stat_info[path].st_size def stat_info(self, path: str) -> os.stat_result: - """ - Returns a stat information object for the specified path from + """Returns a stat information object for the specified path from the snapshot. Attached information is subject to change. Do not use unless @@ -440,7 +421,7 @@ def path(_: Any) -> None: :returns: None. """ - return None + return @property def paths(self) -> set: diff --git a/src/watchdog/utils/echo.py b/src/watchdog/utils/echo.py index 2e4ade99f..e9e388a4e 100644 --- a/src/watchdog/utils/echo.py +++ b/src/watchdog/utils/echo.py @@ -5,7 +5,7 @@ # # Place into the public domain. -""" Echo calls made to functions and methods in a module. +"""Echo calls made to functions and methods in a module. "Echoing" a function call means printing out the name of the function and the values of its arguments before making the call (which is more @@ -25,10 +25,12 @@ decorated function will be echoed. Example: - +------- @echo.echo def my_function(args): pass + + """ from __future__ import annotations @@ -75,7 +77,7 @@ def method_name(method): def format_arg_value(arg_val): """Return a string representing a (name, value) pair. - >>> format_arg_value(('x', (1, 2, 3))) + >>> format_arg_value(("x", (1, 2, 3))) 'x=(1, 2, 3)' """ arg, val = arg_val diff --git a/src/watchdog/utils/patterns.py b/src/watchdog/utils/patterns.py index 0785c5cd1..902a4a6fa 100644 --- a/src/watchdog/utils/patterns.py +++ b/src/watchdog/utils/patterns.py @@ -27,13 +27,12 @@ def _match_path(path, included_patterns, excluded_patterns, case_sensitive): common_patterns = included_patterns & excluded_patterns if common_patterns: - raise ValueError("conflicting patterns `{}` included and excluded".format(common_patterns)) + raise ValueError(f"conflicting patterns `{common_patterns}` included and excluded") return any(path.match(p) for p in included_patterns) and not any(path.match(p) for p in excluded_patterns) def filter_paths(paths, included_patterns=None, excluded_patterns=None, case_sensitive=True): - """ - Filters from a set of paths based on acceptable patterns and + """Filters from a set of paths based on acceptable patterns and ignorable patterns. :param pathnames: A list of path names that will be filtered based on matching and @@ -60,8 +59,7 @@ def filter_paths(paths, included_patterns=None, excluded_patterns=None, case_sen def match_any_paths(paths, included_patterns=None, excluded_patterns=None, case_sensitive=True): - """ - Matches from a set of paths based on acceptable patterns and + """Matches from a set of paths based on acceptable patterns and ignorable patterns. :param pathnames: A list of path names that will be filtered based on matching and @@ -81,7 +79,4 @@ def match_any_paths(paths, included_patterns=None, excluded_patterns=None, case_ included = ["*"] if included_patterns is None else included_patterns excluded = [] if excluded_patterns is None else excluded_patterns - for path in paths: - if _match_path(path, set(included), set(excluded), case_sensitive): - return True - return False + return any(_match_path(path, set(included), set(excluded), case_sensitive) for path in paths) diff --git a/src/watchdog/utils/platform.py b/src/watchdog/utils/platform.py index fefa542b2..6098166c0 100644 --- a/src/watchdog/utils/platform.py +++ b/src/watchdog/utils/platform.py @@ -28,14 +28,17 @@ def get_platform_name(): if sys.platform.startswith("win"): return PLATFORM_WINDOWS - elif sys.platform.startswith("darwin"): + + if sys.platform.startswith("darwin"): return PLATFORM_DARWIN - elif sys.platform.startswith("linux"): + + if sys.platform.startswith("linux"): return PLATFORM_LINUX - elif sys.platform.startswith(("dragonfly", "freebsd", "netbsd", "openbsd", "bsd")): + + if sys.platform.startswith(("dragonfly", "freebsd", "netbsd", "openbsd", "bsd")): return PLATFORM_BSD - else: - return PLATFORM_UNKNOWN + + return PLATFORM_UNKNOWN __platform__ = get_platform_name() diff --git a/src/watchdog/watchmedo.py b/src/watchdog/watchmedo.py old mode 100755 new mode 100644 index 997de3933..7ff607ff2 --- a/src/watchdog/watchmedo.py +++ b/src/watchdog/watchmedo.py @@ -14,8 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -:module: watchdog.watchmedo +""":module: watchdog.watchmedo :author: yesudeep@google.com (Yesudeep Mangalapilly) :author: contact@tiger-222.fr (MickaĆ«l Schoentgen) :synopsis: ``watchmedo`` shell script utility. @@ -34,10 +33,12 @@ from textwrap import dedent from typing import TYPE_CHECKING -from watchdog.observers.api import BaseObserverSubclassCallable from watchdog.utils import WatchdogShutdown, load_class from watchdog.version import VERSION_STRING +if TYPE_CHECKING: + from watchdog.observers.api import BaseObserverSubclassCallable + logging.basicConfig(level=logging.INFO) CONFIG_KEY_TRICKS = "tricks" @@ -111,8 +112,7 @@ def decorator(func): def path_split(pathname_spec, separator=os.pathsep): - """ - Splits a pathname specification separated by an OS-dependent separator. + """Splits a pathname specification separated by an OS-dependent separator. :param pathname_spec: The pathname specification. @@ -123,8 +123,7 @@ def path_split(pathname_spec, separator=os.pathsep): def add_to_sys_path(pathnames, index=0): - """ - Adds specified paths at specified index into the sys.path list. + """Adds specified paths at specified index into the sys.path list. :param paths: A list of paths to add to the sys.path @@ -137,8 +136,7 @@ def add_to_sys_path(pathnames, index=0): def load_config(tricks_file_pathname): - """ - Loads the YAML configuration from the specified file. + """Loads the YAML configuration from the specified file. :param tricks_file_path: The path to the tricks configuration file. @@ -152,8 +150,7 @@ def load_config(tricks_file_pathname): def parse_patterns(patterns_spec, ignore_patterns_spec, separator=";"): - """ - Parses pattern argument specs and returns a two-tuple of + """Parses pattern argument specs and returns a two-tuple of (patterns, ignore_patterns). """ patterns = patterns_spec.split(separator) @@ -164,8 +161,7 @@ def parse_patterns(patterns_spec, ignore_patterns_spec, separator=";"): def observe_with(observer, event_handler, pathnames, recursive): - """ - Single observer thread with a scheduled path and event handler. + """Single observer thread with a scheduled path and event handler. :param observer: The observer thread. @@ -188,8 +184,7 @@ def observe_with(observer, event_handler, pathnames, recursive): def schedule_tricks(observer, tricks, pathname, recursive): - """ - Schedules tricks with the specified observer and for the given watch + """Schedules tricks with the specified observer and for the given watch path. :param observer: @@ -256,9 +251,7 @@ def schedule_tricks(observer, tricks, pathname, recursive): cmd_aliases=["tricks"], ) def tricks_from(args): - """ - Command to execute tricks from a tricks configuration file. - """ + """Command to execute tricks from a tricks configuration file.""" Observer: BaseObserverSubclassCallable if args.debug_force_polling: from watchdog.observers.polling import PollingObserver as Observer @@ -287,15 +280,13 @@ def tricks_from(args): try: tricks = config[CONFIG_KEY_TRICKS] - except KeyError: - raise KeyError(f"No {CONFIG_KEY_TRICKS!r} key specified in {tricks_file!r}.") + except KeyError as e: + raise KeyError(f"No {CONFIG_KEY_TRICKS!r} key specified in {tricks_file!r}.") from e if CONFIG_KEY_PYTHON_PATH in config: add_to_sys_path(config[CONFIG_KEY_PYTHON_PATH]) - dir_path = os.path.dirname(tricks_file) - if not dir_path: - dir_path = os.path.relpath(os.getcwd()) + dir_path = os.path.dirname(tricks_file) or os.path.relpath(os.getcwd()) schedule_tricks(observer, tricks, dir_path, args.recursive) observer.start() observers.append(observer) @@ -343,9 +334,7 @@ def tricks_from(args): cmd_aliases=["generate-tricks-yaml"], ) def tricks_generate_yaml(args): - """ - Command to generate Yaml configuration for tricks named on the command line. - """ + """Command to generate Yaml configuration for tricks named on the command line.""" import yaml python_paths = path_split(args.python_path) @@ -441,12 +430,10 @@ def tricks_generate_yaml(args): action="store_true", help="[debug] Forces Linux inotify(7).", ), - ] + ], ) def log(args): - """ - Command to log file system events to the console. - """ + """Command to log file system events to the console.""" from watchdog.tricks import LoggerTrick from watchdog.utils import echo @@ -563,12 +550,10 @@ def log(args): " executed to avoid multiple simultaneous instances.", ), argument("--debug-force-polling", action="store_true", help="[debug] Forces polling."), - ] + ], ) def shell_command(args): - """ - Command to execute shell commands in response to file system events. - """ + """Command to execute shell commands in response to file system events.""" from watchdog.tricks import ShellCommandTrick if not args.command: @@ -613,7 +598,7 @@ def shell_command(args): dest="directories", metavar="DIRECTORY", action="append", - help="Directory to watch. Use another -d or --directory option " "for each directory.", + help="Directory to watch. Use another -d or --directory option for each directory.", ), argument( "-p", @@ -666,7 +651,7 @@ def shell_command(args): dest="kill_after", default=10.0, type=float, - help="When stopping, kill the subprocess after the specified timeout " "in seconds (default 10.0).", + help="When stopping, kill the subprocess after the specified timeout in seconds (default 10.0).", ), argument( "--debounce-interval", @@ -683,13 +668,10 @@ def shell_command(args): action="store_false", help="Don't auto-restart the command after it exits.", ), - ] + ], ) def auto_restart(args): - """ - Command to start a long-running subprocess and restart it on matched events. - """ - + """Command to start a long-running subprocess and restart it on matched events.""" Observer: BaseObserverSubclassCallable if args.debug_force_polling: from watchdog.observers.polling import PollingObserver as Observer @@ -704,10 +686,7 @@ def auto_restart(args): args.directories = ["."] # Allow either signal name or number. - if args.signal.startswith("SIG"): - stop_signal = getattr(signal, args.signal) - else: - stop_signal = int(args.signal) + stop_signal = getattr(signal, args.signal) if args.signal.startswith("SIG") else int(args.signal) # Handle termination signals by raising a semantic exception which will # allow us to gracefully unwind and stop the observer @@ -771,7 +750,7 @@ def main(): try: log_level = _get_log_level_from_args(args) except LogLevelException as exc: - print(f"Error: {exc.args[0]}", file=sys.stderr) + print(f"Error: {exc.args[0]}", file=sys.stderr) # noqa:T201 command_parsers[args.top_command].print_help() return 1 logging.getLogger("watchdog").setLevel(log_level) diff --git a/tox.ini b/tox.ini index 2f5849ec1..ff6f9ed4c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,8 @@ envlist = py{312,311,310,39,38,py3} docs - mypy + types + lint skip_missing_interpreters = True [testenv] @@ -14,40 +15,28 @@ extras = commands = python -bb -m pytest {posargs} -[testenv:flake8] +[testenv:docs] usedevelop = true deps = -r requirements-tests.txt extras = watchmedo commands = - python -m flake8 docs tools src tests setup.py + sphinx-build -aEWb html docs/source docs/build/html -[testenv:docs] +[testenv:lint] usedevelop = true deps = -r requirements-tests.txt extras = watchmedo commands = - sphinx-build -aEWb html docs/source docs/build/html + python -m ruff format src + python -m ruff --fix src -[testenv:mypy] +[testenv:types] usedevelop = true deps = -r requirements-tests.txt commands = mypy - -[testenv:isort] -usedevelop = true -deps = - -r requirements-tests.txt -commands = - isort src/watchdog/ tests/ *.py - -[testenv:isort-ci] -usedevelop = {[testenv:isort]usedevelop} -deps = {[testenv:isort]deps} -commands = - isort --diff --check-only src/watchdog/ tests/ *.py