This commit is contained in:
2026-04-09 21:54:18 +02:00
commit ad33255b88
8906 changed files with 1437726 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
""":module: watchdog.utils
:synopsis: Utility classes and functions.
:author: yesudeep@google.com (Yesudeep Mangalapilly)
:author: contact@tiger-222.fr (Mickaël Schoentgen)
Classes
-------
.. autoclass:: BaseThread
:members:
:show-inheritance:
:inherited-members:
"""
from __future__ import annotations
import sys
import threading
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from types import ModuleType
from watchdog.tricks import Trick
class UnsupportedLibcError(Exception):
pass
class WatchdogShutdownError(Exception):
"""Semantic exception used to signal an external shutdown event."""
class BaseThread(threading.Thread):
"""Convenience class for creating stoppable threads."""
def __init__(self) -> None:
threading.Thread.__init__(self)
if hasattr(self, "daemon"):
self.daemon = True
else:
self.setDaemon(True)
self._stopped_event = threading.Event()
@property
def stopped_event(self) -> threading.Event:
return self._stopped_event
def should_keep_running(self) -> bool:
"""Determines whether the thread should continue running."""
return not self._stopped_event.is_set()
def on_thread_stop(self) -> None:
"""Override this method instead of :meth:`stop()`.
:meth:`stop()` calls this method.
This method is called immediately after the thread is signaled to stop.
"""
def stop(self) -> None:
"""Signals the thread to stop."""
self._stopped_event.set()
self.on_thread_stop()
def on_thread_start(self) -> None:
"""Override this method instead of :meth:`start()`. :meth:`start()`
calls this method.
This method is called right before this thread is started and this
object's run() method is invoked.
"""
def start(self) -> None:
self.on_thread_start()
threading.Thread.start(self)
def load_module(module_name: str) -> ModuleType:
"""Imports a module given its name and returns a handle to it."""
try:
__import__(module_name)
except ImportError as e:
error = f"No module named {module_name}"
raise ImportError(error) from e
return sys.modules[module_name]
def load_class(dotted_path: str) -> type[Trick]:
"""Loads and returns a class definition provided a 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
-----
You will need to ensure that the module you are trying to load
exists in the Python path.
Examples
--------
- module.name.ClassName # Provided module.name is in the Python path.
- module.ClassName # Provided module is in the Python path.
What won't work:
- 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:
error = f"Dotted module path {dotted_path} must contain a module name and a classname"
raise ValueError(error)
klass_name = dotted_path_split[-1]
module_name = ".".join(dotted_path_split[:-1])
module = load_module(module_name)
if hasattr(module, klass_name):
return getattr(module, klass_name)
error = f"Module {module_name} does not have class attribute {klass_name}"
raise AttributeError(error)

View File

@@ -0,0 +1,90 @@
"""Utility collections or "bricks".
:module: watchdog.utils.bricks
:author: yesudeep@google.com (Yesudeep Mangalapilly)
:author: lalinsky@gmail.com (Lukáš Lalinský)
:author: python@rcn.com (Raymond Hettinger)
:author: contact@tiger-222.fr (Mickaël Schoentgen)
Classes
=======
.. autoclass:: OrderedSetQueue
:members:
:show-inheritance:
:inherited-members:
.. autoclass:: OrderedSet
"""
from __future__ import annotations
import queue
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
class SkipRepeatsQueue(queue.Queue):
"""Thread-safe implementation of an special queue where a
put of the last-item put'd will be dropped.
The implementation leverages locking already implemented in the base class
redefining only the primitives.
Queued items must be immutable and hashable so that they can be used
as dictionary keys. You must implement **only read-only properties** and
the :meth:`Item.__hash__()`, :meth:`Item.__eq__()`, and
:meth:`Item.__ne__()` methods for items to be hashable.
An example implementation follows::
class Item:
def __init__(self, a, b):
self._a = a
self._b = b
@property
def a(self):
return self._a
@property
def b(self):
return self._b
def _key(self):
return (self._a, self._b)
def __eq__(self, item):
return self._key() == item._key()
def __ne__(self, item):
return self._key() != item._key()
def __hash__(self):
return hash(self._key())
based on the OrderedSetQueue below
"""
def _init(self, maxsize: int) -> None:
super()._init(maxsize)
self._last_item = None
def put(self, item: Any, block: bool = True, timeout: float | None = None) -> None: # noqa: FBT001,FBT002
"""This method will be used by `eventlet`, when enabled, so we cannot use force proper keyword-only
arguments nor touch the signature. Also, the `timeout` argument will be ignored in that case.
"""
if self._last_item is None or item != self._last_item:
super().put(item, block, timeout)
def _put(self, item: Any) -> None:
super()._put(item)
self._last_item = item
def _get(self) -> Any:
item = super()._get()
if item is self._last_item:
self._last_item = None
return item

View File

@@ -0,0 +1,77 @@
""":module: watchdog.utils.delayed_queue
:author: thomas.amland@gmail.com (Thomas Amland)
:author: contact@tiger-222.fr (Mickaël Schoentgen)
"""
from __future__ import annotations
import threading
import time
from collections import deque
from typing import Callable, Generic, TypeVar
T = TypeVar("T")
class DelayedQueue(Generic[T]):
def __init__(self, delay: float) -> None:
self.delay_sec = delay
self._lock = threading.Lock()
self._not_empty = threading.Condition(self._lock)
self._queue: deque[tuple[T, float, bool]] = deque()
self._closed = False
def put(self, element: T, *, delay: bool = False) -> None:
"""Add element to queue."""
self._lock.acquire()
self._queue.append((element, time.time(), delay))
self._not_empty.notify()
self._lock.release()
def close(self) -> None:
"""Close queue, indicating no more items will be added."""
self._closed = True
# Interrupt the blocking _not_empty.wait() call in get
self._not_empty.acquire()
self._not_empty.notify()
self._not_empty.release()
def get(self) -> T | None:
"""Remove and return an element from the queue, or this queue has been
closed raise the Closed exception.
"""
while True:
# wait for element to be added to queue
self._not_empty.acquire()
while len(self._queue) == 0 and not self._closed:
self._not_empty.wait()
if self._closed:
self._not_empty.release()
return None
head, insert_time, delay = self._queue[0]
self._not_empty.release()
# wait for delay if required
if delay:
time_left = insert_time + self.delay_sec - time.time()
while time_left > 0:
time.sleep(time_left)
time_left = insert_time + self.delay_sec - time.time()
# return element if it's still in the queue
with self._lock:
if len(self._queue) > 0 and self._queue[0][0] is head:
self._queue.popleft()
return head
def remove(self, predicate: Callable[[T], bool]) -> T | None:
"""Remove and return the first items for which predicate is True,
ignoring delay.
"""
with self._lock:
for i, (elem, *_) in enumerate(self._queue):
if predicate(elem):
del self._queue[i]
return elem
return None

View File

@@ -0,0 +1,424 @@
""":module: watchdog.utils.dirsnapshot
:synopsis: Directory snapshots and comparison.
:author: yesudeep@google.com (Yesudeep Mangalapilly)
:author: contact@tiger-222.fr (Mickaël Schoentgen)
.. ADMONITION:: Where are the moved events? They "disappeared"
This implementation does not take partition boundaries
into consideration. It will only work when the directory
tree is entirely on the same file system. More specifically,
any part of the code that depends on inode numbers can
break if partition boundaries are crossed. In these cases,
the snapshot diff will represent file/directory movement as
created and deleted events.
Classes
-------
.. autoclass:: DirectorySnapshot
:members:
:show-inheritance:
.. autoclass:: DirectorySnapshotDiff
:members:
:show-inheritance:
.. autoclass:: EmptyDirectorySnapshot
:members:
:show-inheritance:
"""
from __future__ import annotations
import contextlib
import errno
import os
from stat import S_ISDIR
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterator
from typing import Any, Callable
class DirectorySnapshotDiff:
"""Compares two directory snapshots and creates an object that represents
the difference between the two snapshots.
:param ref:
The reference directory snapshot.
:type ref:
:class:`DirectorySnapshot`
:param snapshot:
The directory snapshot which will be compared
with the reference snapshot.
:type snapshot:
:class:`DirectorySnapshot`
:param ignore_device:
A boolean indicating whether to ignore the device id or not.
By default, a file may be uniquely identified by a combination of its first
inode and its device id. The problem is that the device id may (or may not)
change between system boots. This problem would cause the DirectorySnapshotDiff
to think a file has been deleted and created again but it would be the
exact same file.
Set to True only if you are sure you will always use the same device.
:type ignore_device:
:class:`bool`
"""
def __init__(
self,
ref: DirectorySnapshot,
snapshot: DirectorySnapshot,
*,
ignore_device: bool = False,
) -> None:
created = snapshot.paths - ref.paths
deleted = ref.paths - snapshot.paths
if ignore_device:
def get_inode(directory: DirectorySnapshot, full_path: bytes | str) -> int | tuple[int, int]:
return directory.inode(full_path)[0]
else:
def get_inode(directory: DirectorySnapshot, full_path: bytes | str) -> int | tuple[int, int]:
return directory.inode(full_path)
# check that all unchanged paths have the same inode
for path in ref.paths & snapshot.paths:
if get_inode(ref, path) != get_inode(snapshot, path):
created.add(path)
deleted.add(path)
# find moved paths
moved: set[tuple[bytes | str, bytes | str]] = set()
for path in set(deleted):
inode = ref.inode(path)
new_path = snapshot.path(inode)
if new_path:
# file is not deleted but moved
deleted.remove(path)
moved.add((path, new_path))
for path in set(created):
inode = snapshot.inode(path)
old_path = ref.path(inode)
if old_path:
created.remove(path)
moved.add((old_path, path))
# find modified paths
# first check paths that have not moved
modified: set[bytes | str] = set()
for path in ref.paths & snapshot.paths:
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):
modified.add(old_path)
self._dirs_created = [path for path in created if snapshot.isdir(path)]
self._dirs_deleted = [path for path in deleted if ref.isdir(path)]
self._dirs_modified = [path for path in modified if ref.isdir(path)]
self._dirs_moved = [(frm, to) for (frm, to) in moved if ref.isdir(frm)]
self._files_created = list(created - set(self._dirs_created))
self._files_deleted = list(deleted - set(self._dirs_deleted))
self._files_modified = list(modified - set(self._dirs_modified))
self._files_moved = list(moved - set(self._dirs_moved))
def __str__(self) -> str:
return self.__repr__()
def __repr__(self) -> str:
fmt = (
"<{0} files(created={1}, deleted={2}, modified={3}, moved={4}),"
" folders(created={5}, deleted={6}, modified={7}, moved={8})>"
)
return fmt.format(
type(self).__name__,
len(self._files_created),
len(self._files_deleted),
len(self._files_modified),
len(self._files_moved),
len(self._dirs_created),
len(self._dirs_deleted),
len(self._dirs_modified),
len(self._dirs_moved),
)
@property
def files_created(self) -> list[bytes | str]:
"""List of files that were created."""
return self._files_created
@property
def files_deleted(self) -> list[bytes | str]:
"""List of files that were deleted."""
return self._files_deleted
@property
def files_modified(self) -> list[bytes | str]:
"""List of files that were modified."""
return self._files_modified
@property
def files_moved(self) -> list[tuple[bytes | str, bytes | str]]:
"""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.
"""
return self._files_moved
@property
def dirs_modified(self) -> list[bytes | str]:
"""List of directories that were modified."""
return self._dirs_modified
@property
def dirs_moved(self) -> list[tuple[bytes | str, bytes | str]]:
"""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.
"""
return self._dirs_moved
@property
def dirs_deleted(self) -> list[bytes | str]:
"""List of directories that were deleted."""
return self._dirs_deleted
@property
def dirs_created(self) -> list[bytes | str]:
"""List of directories that were created."""
return self._dirs_created
class ContextManager:
"""Context manager that creates two directory snapshots and a
diff object that represents the difference between the two snapshots.
:param path:
The directory path for which a snapshot should be taken.
:type path:
``str``
:param recursive:
``True`` if the entire directory tree should be included in the
snapshot; ``False`` otherwise.
:type recursive:
``bool``
:param stat:
Use custom stat function that returns a stat structure for path.
Currently only st_dev, st_ino, st_mode and st_mtime are needed.
A function taking a ``path`` as argument which will be called
for every entry in the directory tree.
:param listdir:
Use custom listdir function. For details see ``os.scandir``.
:param ignore_device:
A boolean indicating whether to ignore the device id or not.
By default, a file may be uniquely identified by a combination of its first
inode and its device id. The problem is that the device id may (or may not)
change between system boots. This problem would cause the DirectorySnapshotDiff
to think a file has been deleted and created again but it would be the
exact same file.
Set to True only if you are sure you will always use the same device.
:type ignore_device:
:class:`bool`
"""
def __init__(
self,
path: str,
*,
recursive: bool = True,
stat: Callable[[str], os.stat_result] = os.stat,
listdir: Callable[[str | None], Iterator[os.DirEntry]] = os.scandir,
ignore_device: bool = False,
) -> None:
self.path = path
self.recursive = recursive
self.stat = stat
self.listdir = listdir
self.ignore_device = ignore_device
def __enter__(self) -> None:
self.pre_snapshot = self.get_snapshot()
def __exit__(self, *args: object) -> None:
self.post_snapshot = self.get_snapshot()
self.diff = DirectorySnapshotDiff(
self.pre_snapshot,
self.post_snapshot,
ignore_device=self.ignore_device,
)
def get_snapshot(self) -> DirectorySnapshot:
return DirectorySnapshot(
path=self.path,
recursive=self.recursive,
stat=self.stat,
listdir=self.listdir,
)
class DirectorySnapshot:
"""A snapshot of stat information of files in a directory.
:param path:
The directory path for which a snapshot should be taken.
:type path:
``str``
:param recursive:
``True`` if the entire directory tree should be included in the
snapshot; ``False`` otherwise.
:type recursive:
``bool``
:param stat:
Use custom stat function that returns a stat structure for path.
Currently only st_dev, st_ino, st_mode and st_mtime are needed.
A function taking a ``path`` as argument which will be called
for every entry in the directory tree.
:param listdir:
Use custom listdir function. For details see ``os.scandir``.
"""
def __init__(
self,
path: str,
*,
recursive: bool = True,
stat: Callable[[str], os.stat_result] = os.stat,
listdir: Callable[[str | None], Iterator[os.DirEntry]] = os.scandir,
) -> None:
self.recursive = recursive
self.stat = stat
self.listdir = listdir
self._stat_info: dict[bytes | str, os.stat_result] = {}
self._inode_to_path: dict[tuple[int, int], bytes | str] = {}
st = self.stat(path)
self._stat_info[path] = st
self._inode_to_path[(st.st_ino, st.st_dev)] = path
for p, st in self.walk(path):
i = (st.st_ino, st.st_dev)
self._inode_to_path[i] = p
self._stat_info[p] = st
def walk(self, root: str) -> Iterator[tuple[str, os.stat_result]]:
try:
paths = [os.path.join(root, entry.name) for entry in self.listdir(root)]
except OSError as e:
# Directory may have been deleted between finding it in the directory
# list of its parent and trying to delete its contents. If this
# happens we treat it as empty. Likewise if the directory was replaced
# with a file of the same name (less likely, but possible).
if e.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
return
else:
raise
entries = []
for p in paths:
with contextlib.suppress(OSError):
entry = (p, self.stat(p))
entries.append(entry)
yield entry
if self.recursive:
for path, st in entries:
with contextlib.suppress(PermissionError):
if S_ISDIR(st.st_mode):
yield from self.walk(path)
@property
def paths(self) -> set[bytes | str]:
"""Set of file/directory paths in the snapshot."""
return set(self._stat_info.keys())
def path(self, uid: tuple[int, int]) -> bytes | str | None:
"""Returns path for id. None if id is unknown to this snapshot."""
return self._inode_to_path.get(uid)
def inode(self, path: bytes | str) -> tuple[int, int]:
"""Returns an id for path."""
st = self._stat_info[path]
return (st.st_ino, st.st_dev)
def isdir(self, path: bytes | str) -> bool:
return S_ISDIR(self._stat_info[path].st_mode)
def mtime(self, path: bytes | str) -> float:
return self._stat_info[path].st_mtime
def size(self, path: bytes | str) -> int:
return self._stat_info[path].st_size
def stat_info(self, path: bytes | str) -> os.stat_result:
"""Returns a stat information object for the specified path from
the snapshot.
Attached information is subject to change. Do not use unless
you specify `stat` in constructor. Use :func:`inode`, :func:`mtime`,
:func:`isdir` instead.
:param path:
The path for which stat information should be obtained
from a snapshot.
"""
return self._stat_info[path]
def __sub__(self, previous_dirsnap: DirectorySnapshot) -> DirectorySnapshotDiff:
"""Allow subtracting a DirectorySnapshot object instance from
another.
:returns:
A :class:`DirectorySnapshotDiff` object.
"""
return DirectorySnapshotDiff(previous_dirsnap, self)
def __str__(self) -> str:
return self.__repr__()
def __repr__(self) -> str:
return str(self._stat_info)
class EmptyDirectorySnapshot(DirectorySnapshot):
"""Class to implement an empty snapshot. This is used together with
DirectorySnapshot and DirectorySnapshotDiff in order to get all the files/folders
in the directory as created.
"""
def __init__(self) -> None:
pass
@staticmethod
def path(_: Any) -> None:
"""Mock up method to return the path of the received inode. As the snapshot
is intended to be empty, it always returns None.
:returns:
None.
"""
return
@property
def paths(self) -> set:
"""Mock up method to return a set of file/directory paths in the snapshot. As
the snapshot is intended to be empty, it always returns an empty set.
:returns:
An empty set.
"""
return set()

View File

@@ -0,0 +1,68 @@
# echo.py: Tracing function calls using Python decorators.
#
# Written by Thomas Guest <tag@wordaligned.org>
# Please see http://wordaligned.org/articles/echo
#
# Place into the public domain.
"""Echo calls made to functions 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
commonly referred to as "tracing", but Python already has a trace module).
Alternatively, echo.echo can be used to decorate functions. Calls to the
decorated function will be echoed.
Example:
-------
@echo.echo
def my_function(args):
pass
"""
from __future__ import annotations
import functools
import sys
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any, Callable
def format_arg_value(arg_val: tuple[str, tuple[Any, ...]]) -> str:
"""Return a string representing a (name, value) pair."""
arg, val = arg_val
return f"{arg}={val!r}"
def echo(fn: Callable, write: Callable[[str], int | None] = sys.stdout.write) -> Callable:
"""Echo calls to a function.
Returns a decorated version of the input function which "echoes" calls
made to it by writing out the function's name and the arguments it was
called with.
"""
# Unpack function's arg count, arg names, arg defaults
code = fn.__code__
argcount = code.co_argcount
argnames = code.co_varnames[:argcount]
fn_defaults: tuple[Any] = fn.__defaults__ or ()
argdefs = dict(list(zip(argnames[-len(fn_defaults) :], fn_defaults)))
@functools.wraps(fn)
def wrapped(*v: Any, **k: Any) -> Callable:
# Collect function arguments by chaining together positional,
# defaulted, extra positional and keyword arguments.
positional = list(map(format_arg_value, list(zip(argnames, v))))
defaulted = [format_arg_value((a, argdefs[a])) for a in argnames[len(v) :] if a not in k]
nameless = list(map(repr, v[argcount:]))
keyword = list(map(format_arg_value, list(k.items())))
args = positional + defaulted + nameless + keyword
write(f"{fn.__name__}({', '.join(args)})\n")
return fn(*v, **k)
return wrapped

View File

@@ -0,0 +1,66 @@
from __future__ import annotations
import logging
import threading
from typing import TYPE_CHECKING
from watchdog.utils import BaseThread
if TYPE_CHECKING:
from typing import Callable
from watchdog.events import FileSystemEvent
logger = logging.getLogger(__name__)
class EventDebouncer(BaseThread):
"""Background thread for debouncing event handling.
When an event is received, wait until the configured debounce interval
passes before calling the callback. If additional events are received
before the interval passes, reset the timer and keep waiting. When the
debouncing interval passes, the callback will be called with a list of
events in the order in which they were received.
"""
def __init__(
self,
debounce_interval_seconds: int,
events_callback: Callable[[list[FileSystemEvent]], None],
) -> None:
super().__init__()
self.debounce_interval_seconds = debounce_interval_seconds
self.events_callback = events_callback
self._events: list[FileSystemEvent] = []
self._cond = threading.Condition()
def handle_event(self, event: FileSystemEvent) -> None:
with self._cond:
self._events.append(event)
self._cond.notify()
def stop(self) -> None:
with self._cond:
super().stop()
self._cond.notify()
def run(self) -> None:
with self._cond:
while True:
# Wait for first event (or shutdown).
self._cond.wait()
if self.debounce_interval_seconds:
# Wait for additional events (or shutdown) until the debounce interval passes.
while self.should_keep_running():
if not self._cond.wait(timeout=self.debounce_interval_seconds):
break
if not self.should_keep_running():
break
events = self._events
self._events = []
self.events_callback(events)

View File

@@ -0,0 +1,99 @@
""":module: watchdog.utils.patterns
:synopsis: Common wildcard searching/filtering functionality for files.
:author: boris.staletic@gmail.com (Boris Staletic)
:author: yesudeep@gmail.com (Yesudeep Mangalapilly)
:author: contact@tiger-222.fr (Mickaël Schoentgen)
"""
from __future__ import annotations
# Non-pure path objects are only allowed on their respective OS's.
# Thus, these utilities require "pure" path objects that don't access the filesystem.
# Since pathlib doesn't have a `case_sensitive` parameter, we have to approximate it
# by converting input paths to `PureWindowsPath` and `PurePosixPath` where:
# - `PureWindowsPath` is always case-insensitive.
# - `PurePosixPath` is always case-sensitive.
# Reference: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.match
from pathlib import PurePosixPath, PureWindowsPath
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterator
def _match_path(
raw_path: str,
included_patterns: set[str],
excluded_patterns: set[str],
*,
case_sensitive: bool,
) -> bool:
"""Internal function same as :func:`match_path` but does not check arguments."""
path: PurePosixPath | PureWindowsPath
if case_sensitive:
path = PurePosixPath(raw_path)
else:
included_patterns = {pattern.lower() for pattern in included_patterns}
excluded_patterns = {pattern.lower() for pattern in excluded_patterns}
path = PureWindowsPath(raw_path)
common_patterns = included_patterns & excluded_patterns
if common_patterns:
error = f"conflicting patterns `{common_patterns}` included and excluded"
raise ValueError(error)
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: list[str],
*,
included_patterns: list[str] | None = None,
excluded_patterns: list[str] | None = None,
case_sensitive: bool = True,
) -> Iterator[str]:
"""Filters from a set of paths based on acceptable patterns and
ignorable patterns.
:param paths:
A list of path names that will be filtered based on matching and
ignored patterns.
:param included_patterns:
Allow filenames matching wildcard patterns specified in this list.
If no pattern list is specified, ["*"] is used as the default pattern,
which matches all files.
:param excluded_patterns:
Ignores filenames matching wildcard patterns specified in this list.
If no pattern list is specified, no files are ignored.
:param case_sensitive:
``True`` if matching should be case-sensitive; ``False`` otherwise.
:returns:
A list of pathnames that matched the allowable patterns and passed
through the ignored patterns.
"""
included = set(["*"] if included_patterns is None else included_patterns)
excluded = set([] if excluded_patterns is None else excluded_patterns)
for path in paths:
if _match_path(path, included, excluded, case_sensitive=case_sensitive):
yield path
def match_any_paths(
paths: list[str],
*,
included_patterns: list[str] | None = None,
excluded_patterns: list[str] | None = None,
case_sensitive: bool = True,
) -> bool:
"""Matches from a set of paths based on acceptable patterns and
ignorable patterns.
See ``filter_paths()`` for signature details.
"""
return any(
filter_paths(
paths,
included_patterns=included_patterns,
excluded_patterns=excluded_patterns,
case_sensitive=case_sensitive,
),
)

View File

@@ -0,0 +1,44 @@
from __future__ import annotations
import sys
PLATFORM_WINDOWS = "windows"
PLATFORM_LINUX = "linux"
PLATFORM_BSD = "bsd"
PLATFORM_DARWIN = "darwin"
PLATFORM_UNKNOWN = "unknown"
def get_platform_name() -> str:
if sys.platform.startswith("win"):
return PLATFORM_WINDOWS
if sys.platform.startswith("darwin"):
return PLATFORM_DARWIN
if sys.platform.startswith("linux"):
return PLATFORM_LINUX
if sys.platform.startswith(("dragonfly", "freebsd", "netbsd", "openbsd", "bsd")):
return PLATFORM_BSD
return PLATFORM_UNKNOWN
__platform__ = get_platform_name()
def is_linux() -> bool:
return __platform__ == PLATFORM_LINUX
def is_bsd() -> bool:
return __platform__ == PLATFORM_BSD
def is_darwin() -> bool:
return __platform__ == PLATFORM_DARWIN
def is_windows() -> bool:
return __platform__ == PLATFORM_WINDOWS

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from watchdog.utils import BaseThread
if TYPE_CHECKING:
import subprocess
from typing import Callable
logger = logging.getLogger(__name__)
class ProcessWatcher(BaseThread):
def __init__(self, popen_obj: subprocess.Popen, process_termination_callback: Callable[[], None] | None) -> None:
super().__init__()
self.popen_obj = popen_obj
self.process_termination_callback = process_termination_callback
def run(self) -> None:
while self.popen_obj.poll() is None:
if self.stopped_event.wait(timeout=0.1):
return
try:
if not self.stopped_event.is_set() and self.process_termination_callback:
self.process_termination_callback()
except Exception:
logger.exception("Error calling process termination callback")