From f910ec4f5c68bf5843d9affcc7d4784f23ac9877 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 1 May 2022 07:03:20 -0500 Subject: [PATCH] Remove build artifacts from the repo Update typo in README in the AllHandler --- .gitignore | 3 + PyNotify.egg-info/PKG-INFO | 242 ----------------- PyNotify.egg-info/SOURCES.txt | 12 - PyNotify.egg-info/dependency_links.txt | 1 - PyNotify.egg-info/top_level.txt | 1 - README.rst | 2 +- build/lib/pynotify/__init__.py | 11 - build/lib/pynotify/event.py | 70 ----- build/lib/pynotify/event_handler.py | 29 --- build/lib/pynotify/event_type.py | 33 --- build/lib/pynotify/notifier.py | 344 ------------------------- dist/PyNotify-0.1-py3-none-any.whl | Bin 9546 -> 0 bytes dist/PyNotify-0.1.tar.gz | Bin 8630 -> 0 bytes 13 files changed, 4 insertions(+), 744 deletions(-) delete mode 100644 PyNotify.egg-info/PKG-INFO delete mode 100644 PyNotify.egg-info/SOURCES.txt delete mode 100644 PyNotify.egg-info/dependency_links.txt delete mode 100644 PyNotify.egg-info/top_level.txt delete mode 100644 build/lib/pynotify/__init__.py delete mode 100644 build/lib/pynotify/event.py delete mode 100644 build/lib/pynotify/event_handler.py delete mode 100644 build/lib/pynotify/event_type.py delete mode 100644 build/lib/pynotify/notifier.py delete mode 100644 dist/PyNotify-0.1-py3-none-any.whl delete mode 100644 dist/PyNotify-0.1.tar.gz diff --git a/.gitignore b/.gitignore index f5efb7f..7f353a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ *__pycache__ .venv/ *.swp +*egg-info +build/ +dist/ diff --git a/PyNotify.egg-info/PKG-INFO b/PyNotify.egg-info/PKG-INFO deleted file mode 100644 index e16fcb9..0000000 --- a/PyNotify.egg-info/PKG-INFO +++ /dev/null @@ -1,242 +0,0 @@ -Metadata-Version: 2.1 -Name: PyNotify -Version: 0.1 -Summary: An asyncio interface to the Linux inotify API -Home-page: https://github.com/mcriley821/PyNotify -Author: Matt Riley -License: LICENSE.txt -Project-URL: Issues, https://github.com/mcriley821/PyNotify/issues -Keywords: inotify,async,asyncio,asynchronous,monitor,file monitor -Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3 -Classifier: Operating System :: POSIX :: Linux -Classifier: Framework :: AsyncIO -Classifier: Development Status :: 3 - Alpha -Classifier: Intended Audience :: Developers -Classifier: Intended Audience :: System Administrators -Classifier: License :: OSI Approved :: The Unlicense (Unlicense) -Classifier: Natural Language :: English -Classifier: Topic :: System :: Filesystems -Classifier: Topic :: System :: Monitoring -Requires-Python: >=3.10 -Description-Content-Type: text/x-rst - -PyNotify -======== -PyNotify is an async Python interface to the Linux inotify API. - -See `man inotify `_ -for more information regarding inotify details. - -See the `documentation `_! - -Install -------- -To install manually, clone the repo and pip install: - -.. code:: bash - - git clone https://github.com/mcriley821/PyNotify.git - cd PyNotify && pip install . - - -Description ------------ -PyNotify uses the `ctypes `_ -module to interface with the inotify API to allow the user to create -'*watches*' for monitoring filesystem events. These events are parsed -into Event objects, which are then handled by EventHandler objects. - -Any number of EventHandlers can be added to a Notifier instance to handle a -specific watch. This is done when requesting a watch via ``Notifier.add_watch`` -When an Event is emitted for the corresponding watch, each EventHandler is -queried for capability of handling said Event. The Event is subsequently -passed to the EventHandler if it is capable. - -Usage ------ - -Simple use case ---------------- - -As an example, an EventHandler that handles all event types for a watch -could be defined as so: - -.. role:: python(code) - :language: python - -.. code:: python - - class AllHandler: - def handle_event(self, event: Event) -> None: - # just print out what is happening - print(f"File {event.name} {event.type.name} at {event.path}") - - def can_handle_event_type(self, type: EventType) -> bool: - return EventType.ALL & type != 0 - -The :python:`AllHandler` can now be added to a watch via -:python:`Notifier.add_watch`: - -.. code:: python - - async def main(): - with pynotify.Notifier() as notifier: - notifier.add_watch(pathlib.Path.cwd(), AllHandler()) - await notifier.run() - -A slightly more interesting example ------------------------------------ - -.. code:: python - - class OpenHandler: - def handle_event(self, event: Event) -> None: - ... - - def can_handle_event_type(self, type: EventType) -> bool: - return EventType.OPEN & type != 0 - - class CloseHandler: - def handle_event(self, event: Event) -> None: - ... - - def can_handle_event_type(self, type: EventType) -> bool: - return EventType.CLOSE & type != 0 - - async def stop_loop(stop_event: asyncio.Event): - await asyncio.sleep(10) - stop_event.set() - - async def main(): - with pynotify.Notifier() as notifier: - path = pathlib.Path.cwd() - stop_event = asyncio.Event() - - notifier.add_watch(path, OpenHandler(), CloseHandler(), - only_event_types=EventType.OPEN | EventType.CLOSE) - await asyncio.gather( - notifier.run(stop_event=stop_event), - stop_loop(stop_event)) - -The above example will run the Notifier run-loop for 10 seconds, generating -only open and close Events for the watch on the current working directory. - -Adding/Modifying/Removing watches ---------------------------------- - -Watches can be added as simply as we've seen above. There are a few more -options that can be specified when adding a watch: - -.. code:: python - - async def main(): - with pynotify.Notifier() as notifier: - path = pathlib.Path.cwd() - notifier.add_watch( - path, # path to add a watch on - - # any number of handlers for the watch - AllHandler(), OpenHandler(), CloseHandler(), - - # restrict EventTypes generated by the watch - only_event_types=EventTypes.OPEN, - - # raises if False and path is a symlink - follow_symlinks=False, - - # raises if True and path is not a directory - if_directory_only=True, - - # if True, generate a single event then remove the watch - oneshot=False, - - # See the docs for more info on this flag - exclude_unlinks=True) - - -EventTypes for a watch can be modified after it has been added to a Notifier: - -.. code:: python - - async def main(): - with pynotify.Notifier() as notifier: - path = pathlib.Path.cwd() - notifier.add_watch(path) # generates all EventTypes by default - ... - # generate only CLOSE Events - notifier.modify_watch_event_type(path, EventType.CLOSE) - - # merge EventTypes to generate both CLOSE and OPEN Events - notifier.modify_watch_event_type(path, EventType.OPEN, merge=True) - -Watches are easily removed: - -.. code:: python - - async def main(): - with pynotify.Notifier() as notifier: - path = pathlib.Path.cwd() - notifier.add_watch(path) - ... - notifier.remove_watch(path) - # notifier.remove_watch(path) # raises, since path not being watched - notifier.remove_watch(path, raises=False) # don't raise - - -Adding/Removing/Clearing EventHandlers --------------------------------------- -EventHandlers can be added when adding a watch, and can be added or removed -after a watch has already been established: - -.. code:: python - - async def main(): - with pynotify.Notifier() as notifier: - path = pathlib.Path.cwd() - open_handler = OpenHandler() - notifier.add_watch(path, open_handler) # add open_handler to watch - - all_handler = AllHandler() - # add all_handler and a CloseHandler - notifier.add_handlers(path, all_handler, CloseHandler()) - - # remove only the all_handler - notifier.remove_handlers(path, all_handler) - - # clear all handlers on the watch - notifier.clear_handlers(path) - -Note in the above example that the :python:`Notifier.add_watches` and -:python:`Notifier.remove_handlers` method can take any number of EventHandlers -to add or remove. Also, duplicate handlers for a watch are not possible, and -removing a handler that isn't on a watch will do nothing: - -.. code:: python - - async def main(): - with pynotify.Notifier() as notifier: - path = pathlib.Path.cwd() - open_handler = OpenHandler() - - notifier.add_watch(path, open_handler) - - # does nothing, since open_handler already on the watch! - notifier.add_handlers(path, open_handlers) - - notifier.remove_handlers(path, open_handler) # no more handlers - - # does nothing, since open_handler isn't on the watch - notifier.remove_handlers(path, open_handler) - -FAQ ---- -To be filled as questions arise... - - -License -------- -The UNLICENSE. See https://www.unlicense.org for more info. - - - diff --git a/PyNotify.egg-info/SOURCES.txt b/PyNotify.egg-info/SOURCES.txt deleted file mode 100644 index ef6b67c..0000000 --- a/PyNotify.egg-info/SOURCES.txt +++ /dev/null @@ -1,12 +0,0 @@ -README.rst -pyproject.toml -setup.cfg -PyNotify.egg-info/PKG-INFO -PyNotify.egg-info/SOURCES.txt -PyNotify.egg-info/dependency_links.txt -PyNotify.egg-info/top_level.txt -pynotify/__init__.py -pynotify/event.py -pynotify/event_handler.py -pynotify/event_type.py -pynotify/notifier.py \ No newline at end of file diff --git a/PyNotify.egg-info/dependency_links.txt b/PyNotify.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/PyNotify.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/PyNotify.egg-info/top_level.txt b/PyNotify.egg-info/top_level.txt deleted file mode 100644 index f63c452..0000000 --- a/PyNotify.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -pynotify diff --git a/README.rst b/README.rst index fb3827b..050dec3 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ could be defined as so: class AllHandler: def handle_event(self, event: Event) -> None: # just print out what is happening - print(f"File {event.name} {event.type.name} at {event.path}") + print(f"{event.type.name} at {event.file_path}") def can_handle_event_type(self, type: EventType) -> bool: return EventType.ALL & type != 0 diff --git a/build/lib/pynotify/__init__.py b/build/lib/pynotify/__init__.py deleted file mode 100644 index 3b0e3ea..0000000 --- a/build/lib/pynotify/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python - -#: An inotify watch descriptor. A simple type alias for :py:class:`int` -WatchDescriptor = int - -from .event_type import EventType -from .event import Event -from .event_handler import EventHandler -from .notifier import Notifier - -__all__ = ["Event", "EventType", "EventHandler", "Notifier", "WatchDescriptor"] diff --git a/build/lib/pynotify/event.py b/build/lib/pynotify/event.py deleted file mode 100644 index 206088e..0000000 --- a/build/lib/pynotify/event.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -from __future__ import annotations -from dataclasses import dataclass -from typing import Callable -from pathlib import Path -import struct - -from . import EventType, WatchDescriptor - -ISDIR = 0x4000_0000 -UNMOUNTED = 0x0000_2000 - -@dataclass(frozen=True) -class Event: - """Events_ port an inotify_event struct into a Python class, - with some additional attributes. - - :note: An Event_ is a frozen :py:func:`~dataclasses.dataclass` - :raises dataclasses.FrozenInstanceError: - Upon attempted attribute change - """ - - #: Corresponding inotify watch descriptor of this Event_ - watch_descriptor: WatchDescriptor - - type: EventType #: EventType_ of this Event_ - is_directory: bool #: :data:`True` if the watched file is a directory - unmounted: bool #: :data:`True` if the watched file was unmounted - cookie: int #: Corresponding inotify cookie - file_name: str #: File name that caused this Event_ - - #: :class:`~pathlib.Path` to the file that caused this Event_ - file_path: Path - - @staticmethod - def from_buffer( - wd_to_path: Callable[ [WatchDescriptor], Path], - buffer: bytes, - offset: int = 0) -> tuple[Event, int]: - """Starting at *offset*, unpack *buffer* to create - an Event_ object. - - :param wd_to_path: A callable to convert a :data:`WatchDescriptor` - into a :class:`~pathlib.Path` - :param buffer: The buffer to unpack from - :param offset: An offset into *buffer* to begin unpacking - - :return: :class:`tuple` of the new Event_ and new *offset* - """ - # Unpack raw inotify_event struct - wd, mask, cookie, _len = struct.unpack_from("@iIII", buffer, offset) - offset += struct.calcsize("@iIII") - file_name, = struct.unpack_from(f"@{_len}s", buffer, offset) - offset += struct.calcsize(f"@{_len}s") - - # Process into Event attributes - file_name = file_name.strip(b'\0').decode() - file_path = wd_to_path(wd) / file_name - type = EventType(mask & EventType.ALL) - - is_dir = (mask & ISDIR) != 0 - unmounted = (mask & UNMOUNTED) != 0 - - # Create and return - event = Event(watch_descriptor=wd, type=type, - is_directory=is_dir, unmounted=unmounted, - cookie=cookie, file_name=file_name, file_path=file_path) - - return event, offset - diff --git a/build/lib/pynotify/event_handler.py b/build/lib/pynotify/event_handler.py deleted file mode 100644 index fa6b3d6..0000000 --- a/build/lib/pynotify/event_handler.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python -from typing import Protocol - -from . import Event, EventType - - -class EventHandler(Protocol): - """Outlines an EventHandler_ object. - - Conforming objects can be added to a Notifier_ instance via - :func:`Notifier.add_watch`. - """ - - def handle_event(self, event: Event) -> None: - """Handle the provided Event_ *event* - - :param event: An Event_ generated by the corresponding watch - added with :func:`Notifier.add_watch` - """ - - def can_handle_event_type(self, type: EventType) -> bool: - """A method required by a Notifier_ to query this EventHandler_ - on whether it can handle the provided EventType_ *type* - - :param type: EventType_ to be handled - - :return bool: :data:`True` if this EventHandler_ can handle the - provided EventType_ *type* - """ diff --git a/build/lib/pynotify/event_type.py b/build/lib/pynotify/event_type.py deleted file mode 100644 index 5276071..0000000 --- a/build/lib/pynotify/event_type.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -from enum import IntFlag - - -class EventType(IntFlag): - """EventTypes_ map to different filesystem events. - - In the man_ page, event types are listings under the - *inotify events* section. (They also correspond in name.) - """ - - ACCESS = 0x001 #: File was accessed - MODIFY = 0x002 #: File was modified - ATTRIB = 0x004 #: File attribute metadata changed - CLOSE_WRITE = 0x008 #: File opened for writing was closed - CLOSE_NOWRITE = 0x010 #: File opened for reading only was closed - OPEN = 0x020 #: File was opened - MOVED_FROM = 0x040 #: A file was moved from the watch directory - MOVED_TO = 0x080 #: A file was moved to the watch directory - CREATE = 0x100 #: File was created - DELETE = 0x200 #: File was deleted - DELETE_SELF = 0x400 #: File was deleted - MOVE_SELF = 0x800 #: File was moved - - #: File was closed: (:py:attr:`CLOSE_WRITE` | :py:attr:`CLOSE_NOWRITE`) - CLOSE = CLOSE_WRITE | CLOSE_NOWRITE - - #: File was moved: (:py:attr:`MOVED_FROM` | :py:attr:`MOVED_TO`) - MOVED = MOVED_FROM | MOVED_TO - - #: Convenience EventType_ for all above EventTypes_ - ALL = 0xfff # combined value for brevity - diff --git a/build/lib/pynotify/notifier.py b/build/lib/pynotify/notifier.py deleted file mode 100644 index 59a5ff5..0000000 --- a/build/lib/pynotify/notifier.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python -from __future__ import annotations -import asyncio -import ctypes -import errno -import os - -from ctypes import c_int, c_uint, c_char_p -from ctypes.util import find_library - -from fcntl import ioctl -from termios import FIONREAD -from signal import SIGINT - -from pathlib import Path - -from typing import AsyncIterable - -from . import Event, EventType, EventHandler, WatchDescriptor - - -ONLYDIR = 24 # shift for 0x0100_0000 -DONT_FOLLOW = 25 # shift for 0x0200_0000 -EXCL_UNLINK = 26 # shift for 0x0400_0000 -ONESHOT = 31 # shift for 0x8000_0000 - -MASK_ADD = 29 # shift for 0x2000_0000 - - -class TwoWayDict(dict): - """Dict subclass to automatically add a value: key pair - whenever a key: value pair is added. - - This is for convenience sake. Instead of tracking two - dicts in the owning object, using this class requires - only one. - """ - def __setitem__(self, key, value): - """Add the key: value pair and the value: key pair""" - dict.__setitem__(self, key, value) - dict.__setitem__(self, value, key) - - -def load_libc(path: Path | None = None) -> ctypes.CDLL: - """Load libc and return a ctypes.CDLL. - - :param path: An optional path to libc.so - - :raises FileNotFoundError: When libc can't be found - """ - if path is None: - path = find_library("c") - if path is None: - raise FileNotFoundError("Could not find libc!") - return ctypes.CDLL(path) - - -class Notifier: - """Notifier - """ - def __init__(self, - async_loop: asyncio.AbstractEventLoop | None = None, - libc_path: Path | None = None): - self._libc = load_libc(libc_path) - self._define_inotify_functions() - - self._loop = async_loop if async_loop else asyncio.get_running_loop() - self._wd_paths: TwoWayDict[WatchDescriptor, Path] = TwoWayDict() - self._queue = asyncio.Queue() - self._handlers: dict[WatchDescriptor, set[EventHandler]] = {} - - self._fd = self._inotify_init() - self._loop.add_reader(self._fd, self._on_fd_ready_to_read) - - def _define_inotify_functions(self): - """Define inotify functions from libc as required by ctypes. - Note that return values are error-checked by the - _handle_inotify_return method.""" - self._libc.inotify_init.restype = self._handle_inotify_return - self._libc.inotify_init.argtypes = tuple() - - self._libc.inotify_add_watch.restype = self._handle_inotify_return - self._libc.inotify_add_watch.argtypes = (c_int, c_char_p, c_uint) - - self._libc.inotify_rm_watch.restype = self._handle_inotify_return - self._libc.inotify_rm_watch.argtypes = (c_int, c_int) - - @staticmethod - def _handle_inotify_return(value: int) -> int: - """Handle the return from an inotify function. Each inotify function - will return -1 to indicate an error and set errno accordingly. - Otherwise the return is valid.""" - if value == -1: - err = ctypes.get_errno() - raise RuntimeError(f"{errno.errorcode[err]}: {os.strerror(err)}") - return value - - def _inotify_init(self) -> int: - """Initializes a new inotify instance and returns a - file descriptor associated with the new inotify event queue.""" - return self._libc.inotify_init() - - def _on_fd_ready_to_read(self): - """Callback method when the asyncio loop determines that the - inotify file descriptor is ready to read.""" - raw_data = self._read_from_fd() - self._create_and_put_events(raw_data) - - def _read_from_fd(self) -> bytes: - """Read and return all bytes available on the file descriptor. - - :return: raw :class:`bytes` from the file descriptor - """ - available = c_int() - try: - ioctl(self._fd, FIONREAD, available) - except OSError: # happens if last watch is removed - return b"" - return os.read(self._fd, available.value) - - def _create_and_put_events(self, raw_data: bytes): - """Deserialize Event objects from the raw data and put - the Event on the queue. - - :param raw_data: The raw bytes to convert to Events_ - """ - offset = 0 - while offset != len(raw_data): - event, offset = Event.from_buffer( - self.watch_descriptor_to_path, raw_data, offset) - self._queue.put_nowait(event) - - def watch_descriptor_to_path(self, - watch_descriptor: WatchDescriptor) -> Path: - """Return the corresponding :class:`Path` of a WatchDescriptor_ - - :param watch_descriptor: The WatchDescriptor_ to convert to a - :class:`Path` - """ - return self._wd_paths[watch_descriptor] - - def add_watch(self, - file_path: Path, - *handlers: EventHandler, - only_event_types: EventType = EventType.ALL, - follow_symlinks: bool = True, - if_directory_only: bool = False, - oneshot: bool = False, - exclude_unlinks: bool = True): - """Add a watch to the specified *file_path*. - - :param file_path: :class:`Path` to watch - :param handlers: Any number of EventHandlers_ to handle - Events_ from the created watch - - :param only_event_types: Generate an Event_ only for the - specified EventTypes_ - - :param follow_symlinks: If :data:`True`, watch even if *file_path* - is a symlink - - :param if_directory_only: If :data:`True`, watch only if *file_path* - is a directory - - :param oneshot: If :data:`True`, delete the watch on *file_path* - after the first Event_ is generated - - :param exclude_unlinks: Stop watching children when unlinked from - a watch directory. See the man_ for - more details. - - :raises FileNotFoundError: If *file_path* does not exist - :raises ValueError: If a watch already exists for *file_path* - :raises OSError: If *follow_symlinks* is :data:`False` and - *file_path* is a symlink - :raises OSError: If *if_directory_only* is :data:`True` and - *file_path* is not a directory - """ - file_path = file_path.expanduser() - if not file_path.exists(): - raise FileNotFoundError(f"Cannot find file: {file_path}") - if file_path in self._wd_paths: - raise ValueError(f"File '{file_path}' already has a watch!") - - bytes_path = str(file_path).encode() - - mask = ( only_event_types - | (follow_symlinks << DONT_FOLLOW) - | (if_directory_only << ONLYDIR) - | (exclude_unlinks << EXCL_UNLINK) - | (oneshot << ONESHOT) ) - - wd = self._libc.inotify_add_watch(self._fd, bytes_path, mask) - self._wd_paths[wd] = file_path - self._handlers[wd] = set(handlers) - - def modify_watch_event_types(self, - descriptor: Path | WatchDescriptor, - new_types: EventType, - merge: bool = False): - """Modify the possible Events_ that can be generated for - the watch at *descriptor* by modifying the EventTypes_ for - said watch. - - :param descriptor: A :class:`Path` or WatchDescriptor_ to change - the EventTypes_ for - - :param new_types: EventTypes_ of Events_ to generate - :param merge: If :data:`True` merge *new_types* with the existing - EventTypes_ for the watch - - :raises ValueError: If there is no watch on *descriptor* - - """ - if isinstance(descriptor, Path): - descriptor = descriptor.expanduser() - - if not descriptor in self._wd_paths: - raise ValueError(f"No watch on descriptor '{descriptor}'!") - - if isinstance(descriptor, Path): - file_path = descriptor - else: - file_path = self._wd_paths[descriptor] - bytes_path = str(file_path).encode() - - # inotify_add_watch also modifies existing watch masks - mask = new_types | (merge << MASK_ADD) - _ = self._libc.inotify_add_watch(self._fd, bytes_path, mask) - - def remove_watch(self, - descriptor: Path | WatchDescriptor, - raises: bool = True): - """Remove a watch on the *descriptor* - - :param descriptor: A :class:`Path` or WatchDescriptor_ to remove - :param raises: If :data:`True`, raise a :class:`ValueError` if - the *descriptor* does not have a watch - - :raises ValueError: If *raises* and there is no watch on *descriptor* - """ - if isinstance(descriptor, Path): - descriptor = descriptor.expanduser() - if descriptor not in self._wd_paths: - if raises: - raise ValueError(f"No watch on descriptor '{descriptor}'!") - return - - wd = self._wd_paths.pop(descriptor) - del self._wd_paths[wd] - - if isinstance(wd, Path): - wd = descriptor - - self._libc.inotify_rm_watch(self._fd, wd) - - def add_handlers(self, - descriptor: Path | WatchDescriptor, - *handlers: EventHandler): - """Add the EventHandlers_ *handlers* to the watch on *descriptor* - - .. note:: Skips a handler if it is already on the *descriptor* - - :param descriptor: A :class:`Path` or WatchDescriptor_ to add - *handlers* to - :param handlers: New EventHandlers_ to add to *descriptor* - """ - if isinstance(descriptor, Path): - descriptor = self._wd_paths[descriptor] - self._handlers[descriptor] |= set(handlers) - - def clear_handlers(self, descriptor: Path | WatchDescriptor): - """Clear all EventHandlers_ for the watch on *descriptor* - - :param descriptor: A :class:`Path` or WatchDescriptor_ to - clear EventHandlers_ from - - :raises ValueError: If there is no watch on *descriptor* - """ - if descriptor not in self._wd_paths: - raise ValueError(f"No watch on descriptor '{descriptor}'!") - if isinstance(descriptor, Path): - descriptor = self._wd_paths[descriptor] - self._handlers[descriptor].clear() - - def remove_handlers(self, - descriptor: Path | WatchDescriptor, - *handlers: EventHandler): - """Remove the EventHandlers_ *handlers* from the watch on *descriptor* - - :param descriptor: A :class:`Path` or WatchDescriptor_ to remove - EventHandlers_ from - :param handlers: EventHandlers_ to remove from *descriptor* - - :raises ValueError: If there is no watch on *descriptor* - """ - if descriptor not in self._wd_paths: - raise ValueError("No watch on descriptor '{descriptor}'!") - if isinstance(descriptor, Path): - descriptor = self._wd_paths[descriptor] - self._handlers[descriptor] -= set(handlers) - - async def run(self, - stop_event: asyncio.Event | None = None, - handle_once: bool = False, - warn_unhandled: bool = True): - """Asynchronously generate Events_ until *stop_event* is set. - When an Event_ is generated, the :func:`EventHandler.handle_event` - method for each EventHandler_ for the watch, from - :func:`Notifier.add_watch`, is called with the generated Event_. - - :param stop_event: :class:`asyncio.Event` to stop generating - Events_ when set. - :param handle_once: If :data:`True` the generated Event_ is - discarded after it is handled by the first - capable EventHandler_. - :param warn_unhandled: Emit a warning via :func:`warnings.warn` - if an Event_ is not handled. - """ - while stop_event is None or not stop_event.is_set(): - event = await self._queue.get() - handled = False - for handler in self._handlers[event.watch_descriptor]: - if not handler.can_handle_event_type(event.type): - continue - handler.handle_event(event) - handled = True - if handle_once: - break - if warn_unhandled and not handled: - warnings.warn("Unhandled Event! {event}", RuntimeWarning) - - def close(self): - """Close the inotify fd""" - os.close(self._fd) - - def __enter__(self): - """Nothing special on entry""" - return self - - def __exit__(self, *exception): - """Close the inotify fd when exiting""" - self.close() - diff --git a/dist/PyNotify-0.1-py3-none-any.whl b/dist/PyNotify-0.1-py3-none-any.whl deleted file mode 100644 index 2eccb677e8be0a095a545848654035c03faf159f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9546 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