I gang
This commit is contained in:
@@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user