From 6d7ba58f880be618ade07f8ea080fe8c4bf8a896 Mon Sep 17 00:00:00 2001 From: cyfraeviolae Date: Wed, 3 Apr 2024 03:10:44 -0400 Subject: venv --- .../site-packages/watchfiles/__init__.py | 17 + .../site-packages/watchfiles/__main__.py | 4 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 631 bytes .../__pycache__/__main__.cpython-311.pyc | Bin 0 -> 318 bytes .../watchfiles/__pycache__/cli.cpython-311.pyc | Bin 0 -> 11318 bytes .../watchfiles/__pycache__/filters.cpython-311.pyc | Bin 0 -> 8116 bytes .../watchfiles/__pycache__/main.cpython-311.pyc | Bin 0 -> 17664 bytes .../watchfiles/__pycache__/run.cpython-311.pyc | Bin 0 -> 21477 bytes .../watchfiles/__pycache__/version.cpython-311.pyc | Bin 0 -> 296 bytes .../_rust_notify.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 5364528 bytes .../site-packages/watchfiles/_rust_notify.pyi | 111 ++++++ .../lib/python3.11/site-packages/watchfiles/cli.py | 224 +++++++++++ .../python3.11/site-packages/watchfiles/filters.py | 150 +++++++ .../python3.11/site-packages/watchfiles/main.py | 344 ++++++++++++++++ .../python3.11/site-packages/watchfiles/py.typed | 1 + .../lib/python3.11/site-packages/watchfiles/run.py | 441 +++++++++++++++++++++ .../python3.11/site-packages/watchfiles/version.py | 5 + 17 files changed, 1297 insertions(+) create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__init__.py create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__main__.py create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__pycache__/__init__.cpython-311.pyc create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__pycache__/__main__.cpython-311.pyc create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__pycache__/cli.cpython-311.pyc create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__pycache__/filters.cpython-311.pyc create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__pycache__/main.cpython-311.pyc create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__pycache__/run.cpython-311.pyc create mode 100644 venv/lib/python3.11/site-packages/watchfiles/__pycache__/version.cpython-311.pyc create mode 100755 venv/lib/python3.11/site-packages/watchfiles/_rust_notify.cpython-311-x86_64-linux-gnu.so create mode 100644 venv/lib/python3.11/site-packages/watchfiles/_rust_notify.pyi create mode 100644 venv/lib/python3.11/site-packages/watchfiles/cli.py create mode 100644 venv/lib/python3.11/site-packages/watchfiles/filters.py create mode 100644 venv/lib/python3.11/site-packages/watchfiles/main.py create mode 100644 venv/lib/python3.11/site-packages/watchfiles/py.typed create mode 100644 venv/lib/python3.11/site-packages/watchfiles/run.py create mode 100644 venv/lib/python3.11/site-packages/watchfiles/version.py (limited to 'venv/lib/python3.11/site-packages/watchfiles') diff --git a/venv/lib/python3.11/site-packages/watchfiles/__init__.py b/venv/lib/python3.11/site-packages/watchfiles/__init__.py new file mode 100644 index 0000000..877fbd5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/__init__.py @@ -0,0 +1,17 @@ +from .filters import BaseFilter, DefaultFilter, PythonFilter +from .main import Change, awatch, watch +from .run import arun_process, run_process +from .version import VERSION + +__version__ = VERSION +__all__ = ( + 'watch', + 'awatch', + 'run_process', + 'arun_process', + 'Change', + 'BaseFilter', + 'DefaultFilter', + 'PythonFilter', + 'VERSION', +) diff --git a/venv/lib/python3.11/site-packages/watchfiles/__main__.py b/venv/lib/python3.11/site-packages/watchfiles/__main__.py new file mode 100644 index 0000000..d396c2a --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/__main__.py @@ -0,0 +1,4 @@ +from .cli import cli + +if __name__ == '__main__': + cli() diff --git a/venv/lib/python3.11/site-packages/watchfiles/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..d0fdd17 Binary files /dev/null and b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/watchfiles/__pycache__/__main__.cpython-311.pyc b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..cecee55 Binary files /dev/null and b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/__main__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/watchfiles/__pycache__/cli.cpython-311.pyc b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/cli.cpython-311.pyc new file mode 100644 index 0000000..612625b Binary files /dev/null and b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/cli.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/watchfiles/__pycache__/filters.cpython-311.pyc b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/filters.cpython-311.pyc new file mode 100644 index 0000000..f861ae9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/filters.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/watchfiles/__pycache__/main.cpython-311.pyc b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..7529b39 Binary files /dev/null and b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/main.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/watchfiles/__pycache__/run.cpython-311.pyc b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/run.cpython-311.pyc new file mode 100644 index 0000000..948cac5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/run.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/watchfiles/__pycache__/version.cpython-311.pyc b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/version.cpython-311.pyc new file mode 100644 index 0000000..3506dd7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/watchfiles/__pycache__/version.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/watchfiles/_rust_notify.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/watchfiles/_rust_notify.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..5ee5943 Binary files /dev/null and b/venv/lib/python3.11/site-packages/watchfiles/_rust_notify.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/watchfiles/_rust_notify.pyi b/venv/lib/python3.11/site-packages/watchfiles/_rust_notify.pyi new file mode 100644 index 0000000..63eacda --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/_rust_notify.pyi @@ -0,0 +1,111 @@ +from typing import Any, List, Literal, Optional, Protocol, Set, Tuple, Union + +__all__ = 'RustNotify', 'WatchfilesRustInternalError' + +__version__: str +"""The package version as defined in `Cargo.toml`, modified to match python's versioning semantics.""" + +class AbstractEvent(Protocol): + def is_set(self) -> bool: ... + +class RustNotify: + """ + Interface to the Rust [notify](https://crates.io/crates/notify) crate which does + the heavy lifting of watching for file changes and grouping them into events. + """ + + def __init__( + self, + watch_paths: List[str], + debug: bool, + force_polling: bool, + poll_delay_ms: int, + recursive: bool, + ignore_permission_denied: bool, + ) -> None: + """ + Create a new `RustNotify` instance and start a thread to watch for changes. + + `FileNotFoundError` is raised if any of the paths do not exist. + + Args: + watch_paths: file system paths to watch for changes, can be directories or files + debug: if true, print details about all events to stderr + force_polling: if true, always use polling instead of file system notifications + poll_delay_ms: delay between polling for changes, only used if `force_polling=True` + recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in + the top-level directory, default is `True`. + ignore_permission_denied: if `True`, permission denied errors are ignored while watching changes. + """ + def watch( + self, + debounce_ms: int, + step_ms: int, + timeout_ms: int, + stop_event: Optional[AbstractEvent], + ) -> Union[Set[Tuple[int, str]], Literal['signal', 'stop', 'timeout']]: + """ + Watch for changes. + + This method will wait `timeout_ms` milliseconds for changes, but once a change is detected, + it will group changes and return in no more than `debounce_ms` milliseconds. + + The GIL is released during a `step_ms` sleep on each iteration to avoid + blocking python. + + Args: + debounce_ms: maximum time in milliseconds to group changes over before returning. + step_ms: time to wait for new changes in milliseconds, if no changes are detected + in this time, and at least one change has been detected, the changes are yielded. + timeout_ms: maximum time in milliseconds to wait for changes before returning, + `0` means wait indefinitely, `debounce_ms` takes precedence over `timeout_ms` once + a change is detected. + stop_event: event to check on every iteration to see if this function should return early. + The event should be an object which has an `is_set()` method which returns a boolean. + + Returns: + See below. + + Return values have the following meanings: + + * Change details as a `set` of `(event_type, path)` tuples, the event types are ints which match + [`Change`][watchfiles.Change], `path` is a string representing the path of the file that changed + * `'signal'` string, if a signal was received + * `'stop'` string, if the `stop_event` was set + * `'timeout'` string, if `timeout_ms` was exceeded + """ + def __enter__(self) -> 'RustNotify': + """ + Does nothing, but allows `RustNotify` to be used as a context manager. + + !!! note + + The watching thead is created when an instance is initiated, not on `__enter__`. + """ + def __exit__(self, *args: Any) -> None: + """ + Calls [`close`][watchfiles._rust_notify.RustNotify.close]. + """ + def close(self) -> None: + """ + Stops the watching thread. After `close` is called, the `RustNotify` instance can no + longer be used, calls to [`watch`][watchfiles._rust_notify.RustNotify.watch] will raise a `RuntimeError`. + + !!! note + + `close` is not required, just deleting the `RustNotify` instance will kill the thread + implicitly. + + As per [#163](https://github.com/samuelcolvin/watchfiles/issues/163) `close()` is only required because + in the event of an error, the traceback in `sys.exc_info` keeps a reference to `watchfiles.watch`'s + frame, so you can't rely on the `RustNotify` object being deleted, and thereby stopping + the watching thread. + """ + +class WatchfilesRustInternalError(RuntimeError): + """ + Raised when RustNotify encounters an unknown error. + + If you get this a lot, please check [github](https://github.com/samuelcolvin/watchfiles/issues) issues + and create a new issue if your problem is not discussed. + """ diff --git a/venv/lib/python3.11/site-packages/watchfiles/cli.py b/venv/lib/python3.11/site-packages/watchfiles/cli.py new file mode 100644 index 0000000..f1e1ddd --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/cli.py @@ -0,0 +1,224 @@ +import argparse +import logging +import os +import shlex +import sys +from pathlib import Path +from textwrap import dedent +from typing import Any, Callable, List, Optional, Tuple, Union, cast + +from . import Change +from .filters import BaseFilter, DefaultFilter, PythonFilter +from .run import detect_target_type, import_string, run_process +from .version import VERSION + +logger = logging.getLogger('watchfiles.cli') + + +def resolve_path(path_str: str) -> Path: + path = Path(path_str) + if not path.exists(): + raise FileNotFoundError(path) + else: + return path.resolve() + + +def cli(*args_: str) -> None: + """ + Watch one or more directories and execute either a shell command or a python function on file changes. + + Example of watching the current directory and calling a python function: + + watchfiles foobar.main + + Example of watching python files in two local directories and calling a shell command: + + watchfiles --filter python 'pytest --lf' src tests + + See https://watchfiles.helpmanual.io/cli/ for more information. + """ + args = args_ or sys.argv[1:] + parser = argparse.ArgumentParser( + prog='watchfiles', + description=dedent((cli.__doc__ or '').strip('\n')), + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument('target', help='Command or dotted function path to run') + parser.add_argument( + 'paths', nargs='*', default='.', help='Filesystem paths to watch, defaults to current directory' + ) + + parser.add_argument( + '--ignore-paths', + nargs='?', + type=str, + help=( + 'Specify directories to ignore, ' + 'to ignore multiple paths use a comma as separator, e.g. "env" or "env,node_modules"' + ), + ) + parser.add_argument( + '--target-type', + nargs='?', + type=str, + default='auto', + choices=['command', 'function', 'auto'], + help=( + 'Whether the target should be intercepted as a shell command or a python function, ' + 'defaults to "auto" which infers the target type from the target string' + ), + ) + parser.add_argument( + '--filter', + nargs='?', + type=str, + default='default', + help=( + 'Which files to watch, defaults to "default" which uses the "DefaultFilter", ' + '"python" uses the "PythonFilter", "all" uses no filter, ' + 'any other value is interpreted as a python function/class path which is imported' + ), + ) + parser.add_argument( + '--args', + nargs='?', + type=str, + help='Arguments to set on sys.argv before calling target function, used only if the target is a function', + ) + parser.add_argument('--verbose', action='store_true', help='Set log level to "debug", wins over `--verbosity`') + parser.add_argument( + '--non-recursive', action='store_true', help='Do not watch for changes in sub-directories recursively' + ) + parser.add_argument( + '--verbosity', + nargs='?', + type=str, + default='info', + choices=['warning', 'info', 'debug'], + help='Log level, defaults to "info"', + ) + parser.add_argument( + '--sigint-timeout', + nargs='?', + type=int, + default=5, + help='How long to wait for the sigint timeout before sending sigkill.', + ) + parser.add_argument( + '--grace-period', + nargs='?', + type=float, + default=0, + help='Number of seconds after the process is started before watching for changes.', + ) + parser.add_argument( + '--sigkill-timeout', + nargs='?', + type=int, + default=1, + help='How long to wait for the sigkill timeout before issuing a timeout exception.', + ) + parser.add_argument( + '--ignore-permission-denied', + action='store_true', + help='Ignore permission denied errors while watching files and directories.', + ) + parser.add_argument('--version', '-V', action='version', version=f'%(prog)s v{VERSION}') + arg_namespace = parser.parse_args(args) + + if arg_namespace.verbose: + log_level = logging.DEBUG + else: + log_level = getattr(logging, arg_namespace.verbosity.upper()) + + hdlr = logging.StreamHandler() + hdlr.setLevel(log_level) + hdlr.setFormatter(logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%H:%M:%S')) + wg_logger = logging.getLogger('watchfiles') + wg_logger.addHandler(hdlr) + wg_logger.setLevel(log_level) + + if arg_namespace.target_type == 'auto': + target_type = detect_target_type(arg_namespace.target) + else: + target_type = arg_namespace.target_type + + if target_type == 'function': + logger.debug('target_type=function, attempting import of "%s"', arg_namespace.target) + import_exit(arg_namespace.target) + if arg_namespace.args: + sys.argv = [arg_namespace.target] + shlex.split(arg_namespace.args) + elif arg_namespace.args: + logger.warning('--args is only used when the target is a function') + + try: + paths = [resolve_path(p) for p in arg_namespace.paths] + except FileNotFoundError as e: + print(f'path "{e}" does not exist', file=sys.stderr) + sys.exit(1) + + watch_filter, watch_filter_str = build_filter(arg_namespace.filter, arg_namespace.ignore_paths) + + logger.info( + 'watchfiles v%s 👀 path=%s target="%s" (%s) filter=%s...', + VERSION, + ', '.join(f'"{p}"' for p in paths), + arg_namespace.target, + target_type, + watch_filter_str, + ) + + run_process( + *paths, + target=arg_namespace.target, + target_type=target_type, + watch_filter=watch_filter, + debug=log_level == logging.DEBUG, + sigint_timeout=arg_namespace.sigint_timeout, + sigkill_timeout=arg_namespace.sigkill_timeout, + recursive=not arg_namespace.non_recursive, + ignore_permission_denied=arg_namespace.ignore_permission_denied, + grace_period=arg_namespace.grace_period, + ) + + +def import_exit(function_path: str) -> Any: + cwd = os.getcwd() + if cwd not in sys.path: + sys.path.append(cwd) + + try: + return import_string(function_path) + except ImportError as e: + print(f'ImportError: {e}', file=sys.stderr) + sys.exit(1) + + +def build_filter( + filter_name: str, ignore_paths_str: Optional[str] +) -> Tuple[Union[None, DefaultFilter, Callable[[Change, str], bool]], str]: + ignore_paths: List[Path] = [] + if ignore_paths_str: + ignore_paths = [Path(p).resolve() for p in ignore_paths_str.split(',')] + + if filter_name == 'default': + return DefaultFilter(ignore_paths=ignore_paths), 'DefaultFilter' + elif filter_name == 'python': + return PythonFilter(ignore_paths=ignore_paths), 'PythonFilter' + elif filter_name == 'all': + if ignore_paths: + logger.warning('"--ignore-paths" argument ignored as "all" filter was selected') + return None, '(no filter)' + + watch_filter_cls = import_exit(filter_name) + if isinstance(watch_filter_cls, type) and issubclass(watch_filter_cls, DefaultFilter): + return watch_filter_cls(ignore_paths=ignore_paths), watch_filter_cls.__name__ + + if ignore_paths: + logger.warning('"--ignore-paths" argument ignored as filter is not a subclass of DefaultFilter') + + if isinstance(watch_filter_cls, type) and issubclass(watch_filter_cls, BaseFilter): + return watch_filter_cls(), watch_filter_cls.__name__ + else: + watch_filter = cast(Callable[[Change, str], bool], watch_filter_cls) + return watch_filter, repr(watch_filter_cls) diff --git a/venv/lib/python3.11/site-packages/watchfiles/filters.py b/venv/lib/python3.11/site-packages/watchfiles/filters.py new file mode 100644 index 0000000..3ebe6c5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/filters.py @@ -0,0 +1,150 @@ +import logging +import os +import re +from pathlib import Path +from typing import TYPE_CHECKING, Optional, Sequence, Union + +__all__ = 'BaseFilter', 'DefaultFilter', 'PythonFilter' +logger = logging.getLogger('watchfiles.watcher') + + +if TYPE_CHECKING: + from .main import Change + + +class BaseFilter: + """ + Useful base class for creating filters. `BaseFilter` should be inherited and configured, rather than used + directly. + + The class supports ignoring files in 3 ways: + """ + + __slots__ = '_ignore_dirs', '_ignore_entity_regexes', '_ignore_paths' + ignore_dirs: Sequence[str] = () + """Full names of directories to ignore, an obvious example would be `.git`.""" + ignore_entity_patterns: Sequence[str] = () + """ + Patterns of files or directories to ignore, these are compiled into regexes. + + "entity" here refers to the specific file or directory - basically the result of `path.split(os.sep)[-1]`, + an obvious example would be `r'\\.py[cod]$'`. + """ + ignore_paths: Sequence[Union[str, Path]] = () + """ + Full paths to ignore, e.g. `/home/users/.cache` or `C:\\Users\\user\\.cache`. + """ + + def __init__(self) -> None: + self._ignore_dirs = set(self.ignore_dirs) + self._ignore_entity_regexes = tuple(re.compile(r) for r in self.ignore_entity_patterns) + self._ignore_paths = tuple(map(str, self.ignore_paths)) + + def __call__(self, change: 'Change', path: str) -> bool: + """ + Instances of `BaseFilter` subclasses can be used as callables. + Args: + change: The type of change that occurred, see [`Change`][watchfiles.Change]. + path: the raw path of the file or directory that changed. + + Returns: + True if the file should be included in changes, False if it should be ignored. + """ + parts = path.lstrip(os.sep).split(os.sep) + if any(p in self._ignore_dirs for p in parts): + return False + + entity_name = parts[-1] + if any(r.search(entity_name) for r in self._ignore_entity_regexes): + return False + elif self._ignore_paths and path.startswith(self._ignore_paths): + return False + else: + return True + + def __repr__(self) -> str: + args = ', '.join(f'{k}={getattr(self, k, None)!r}' for k in self.__slots__) + return f'{self.__class__.__name__}({args})' + + +class DefaultFilter(BaseFilter): + """ + The default filter, which ignores files and directories that you might commonly want to ignore. + """ + + ignore_dirs: Sequence[str] = ( + '__pycache__', + '.git', + '.hg', + '.svn', + '.tox', + '.venv', + 'site-packages', + '.idea', + 'node_modules', + '.mypy_cache', + '.pytest_cache', + '.hypothesis', + ) + """Directory names to ignore.""" + + ignore_entity_patterns: Sequence[str] = ( + r'\.py[cod]$', + r'\.___jb_...___$', + r'\.sw.$', + '~$', + r'^\.\#', + r'^\.DS_Store$', + r'^flycheck_', + ) + """File/Directory name patterns to ignore.""" + + def __init__( + self, + *, + ignore_dirs: Optional[Sequence[str]] = None, + ignore_entity_patterns: Optional[Sequence[str]] = None, + ignore_paths: Optional[Sequence[Union[str, Path]]] = None, + ) -> None: + """ + Args: + ignore_dirs: if not `None`, overrides the `ignore_dirs` value set on the class. + ignore_entity_patterns: if not `None`, overrides the `ignore_entity_patterns` value set on the class. + ignore_paths: if not `None`, overrides the `ignore_paths` value set on the class. + """ + if ignore_dirs is not None: + self.ignore_dirs = ignore_dirs + if ignore_entity_patterns is not None: + self.ignore_entity_patterns = ignore_entity_patterns + if ignore_paths is not None: + self.ignore_paths = ignore_paths + + super().__init__() + + +class PythonFilter(DefaultFilter): + """ + A filter for Python files, since this class inherits from [`DefaultFilter`][watchfiles.DefaultFilter] + it will ignore files and directories that you might commonly want to ignore as well as filtering out + all changes except in Python files (files with extensions `('.py', '.pyx', '.pyd')`). + """ + + def __init__( + self, + *, + ignore_paths: Optional[Sequence[Union[str, Path]]] = None, + extra_extensions: Sequence[str] = (), + ) -> None: + """ + Args: + ignore_paths: The paths to ignore, see [`BaseFilter`][watchfiles.BaseFilter]. + extra_extensions: extra extensions to ignore. + + `ignore_paths` and `extra_extensions` can be passed as arguments partly to support [CLI](../cli.md) usage where + `--ignore-paths` and `--extensions` can be passed as arguments. + """ + self.extensions = ('.py', '.pyx', '.pyd') + tuple(extra_extensions) + super().__init__(ignore_paths=ignore_paths) + + def __call__(self, change: 'Change', path: str) -> bool: + return path.endswith(self.extensions) and super().__call__(change, path) diff --git a/venv/lib/python3.11/site-packages/watchfiles/main.py b/venv/lib/python3.11/site-packages/watchfiles/main.py new file mode 100644 index 0000000..1df7e54 --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/main.py @@ -0,0 +1,344 @@ +import logging +import os +import sys +import warnings +from enum import IntEnum +from pathlib import Path +from typing import TYPE_CHECKING, AsyncGenerator, Callable, Generator, Optional, Set, Tuple, Union + +import anyio + +from ._rust_notify import RustNotify +from .filters import DefaultFilter + +__all__ = 'watch', 'awatch', 'Change', 'FileChange' +logger = logging.getLogger('watchfiles.main') + + +class Change(IntEnum): + """ + Enum representing the type of change that occurred. + """ + + added = 1 + """A new file or directory was added.""" + modified = 2 + """A file or directory was modified, can be either a metadata or data change.""" + deleted = 3 + """A file or directory was deleted.""" + + def raw_str(self) -> str: + return self.name + + +FileChange = Tuple[Change, str] +""" +A tuple representing a file change, first element is a [`Change`][watchfiles.Change] member, second is the path +of the file or directory that changed. +""" + +if TYPE_CHECKING: + import asyncio + from typing import Protocol + + import trio + + AnyEvent = Union[anyio.Event, asyncio.Event, trio.Event] + + class AbstractEvent(Protocol): + def is_set(self) -> bool: + ... + + +def watch( + *paths: Union[Path, str], + watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(), + debounce: int = 1_600, + step: int = 50, + stop_event: Optional['AbstractEvent'] = None, + rust_timeout: int = 5_000, + yield_on_timeout: bool = False, + debug: bool = False, + raise_interrupt: bool = True, + force_polling: Optional[bool] = None, + poll_delay_ms: int = 300, + recursive: bool = True, + ignore_permission_denied: Optional[bool] = None, +) -> Generator[Set[FileChange], None, None]: + """ + Watch one or more paths and yield a set of changes whenever files change. + + The paths watched can be directories or files, directories are watched recursively - changes in subdirectories + are also detected. + + #### Force polling + + Notify will fall back to file polling if it can't use file system notifications, but we also force notify + to us polling if the `force_polling` argument is `True`; if `force_polling` is unset (or `None`), we enable + force polling thus: + + * if the `WATCHFILES_FORCE_POLLING` environment variable exists and is not empty: + * if the value is `false`, `disable` or `disabled`, force polling is disabled + * otherwise, force polling is enabled + * otherwise, we enable force polling only if we detect we're running on WSL (Windows Subsystem for Linux) + + Args: + *paths: filesystem paths to watch. + watch_filter: callable used to filter out changes which are not important, you can either use a raw callable + or a [`BaseFilter`][watchfiles.BaseFilter] instance, + defaults to an instance of [`DefaultFilter`][watchfiles.DefaultFilter]. To keep all changes, use `None`. + debounce: maximum time in milliseconds to group changes over before yielding them. + step: time to wait for new changes in milliseconds, if no changes are detected in this time, and + at least one change has been detected, the changes are yielded. + stop_event: event to stop watching, if this is set, the generator will stop iteration, + this can be anything with an `is_set()` method which returns a bool, e.g. `threading.Event()`. + rust_timeout: maximum time in milliseconds to wait in the rust code for changes, `0` means no timeout. + yield_on_timeout: if `True`, the generator will yield upon timeout in rust even if no changes are detected. + debug: whether to print information about all filesystem changes in rust to stdout. + raise_interrupt: whether to re-raise `KeyboardInterrupt`s, or suppress the error and just stop iterating. + force_polling: See [Force polling](#force-polling) above. + poll_delay_ms: delay between polling for changes, only used if `force_polling=True`. + recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the + top-level directory, default is `True`. + ignore_permission_denied: if `True`, will ignore permission denied errors, otherwise will raise them by default. + Setting the `WATCHFILES_IGNORE_PERMISSION_DENIED` environment variable will set this value too. + + Yields: + The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s. + + ```py title="Example of watch usage" + from watchfiles import watch + + for changes in watch('./first/dir', './second/dir', raise_interrupt=False): + print(changes) + ``` + """ + force_polling = _default_force_polling(force_polling) + ignore_permission_denied = _default_ignore_permission_denied(ignore_permission_denied) + with RustNotify( + [str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive, ignore_permission_denied + ) as watcher: + while True: + raw_changes = watcher.watch(debounce, step, rust_timeout, stop_event) + if raw_changes == 'timeout': + if yield_on_timeout: + yield set() + else: + logger.debug('rust notify timeout, continuing') + elif raw_changes == 'signal': + if raise_interrupt: + raise KeyboardInterrupt + else: + logger.warning('KeyboardInterrupt caught, stopping watch') + return + elif raw_changes == 'stop': + return + else: + changes = _prep_changes(raw_changes, watch_filter) + if changes: + _log_changes(changes) + yield changes + else: + logger.debug('all changes filtered out, raw_changes=%s', raw_changes) + + +async def awatch( # noqa C901 + *paths: Union[Path, str], + watch_filter: Optional[Callable[[Change, str], bool]] = DefaultFilter(), + debounce: int = 1_600, + step: int = 50, + stop_event: Optional['AnyEvent'] = None, + rust_timeout: Optional[int] = None, + yield_on_timeout: bool = False, + debug: bool = False, + raise_interrupt: Optional[bool] = None, + force_polling: Optional[bool] = None, + poll_delay_ms: int = 300, + recursive: bool = True, + ignore_permission_denied: Optional[bool] = None, +) -> AsyncGenerator[Set[FileChange], None]: + """ + Asynchronous equivalent of [`watch`][watchfiles.watch] using threads to wait for changes. + Arguments match those of [`watch`][watchfiles.watch] except `stop_event`. + + All async methods use [anyio](https://anyio.readthedocs.io/en/latest/) to run the event loop. + + Unlike [`watch`][watchfiles.watch] `KeyboardInterrupt` cannot be suppressed by `awatch` so they need to be caught + where `asyncio.run` or equivalent is called. + + Args: + *paths: filesystem paths to watch. + watch_filter: matches the same argument of [`watch`][watchfiles.watch]. + debounce: matches the same argument of [`watch`][watchfiles.watch]. + step: matches the same argument of [`watch`][watchfiles.watch]. + stop_event: `anyio.Event` which can be used to stop iteration, see example below. + rust_timeout: matches the same argument of [`watch`][watchfiles.watch], except that `None` means + use `1_000` on Windows and `5_000` on other platforms thus helping with exiting on `Ctrl+C` on Windows, + see [#110](https://github.com/samuelcolvin/watchfiles/issues/110). + yield_on_timeout: matches the same argument of [`watch`][watchfiles.watch]. + debug: matches the same argument of [`watch`][watchfiles.watch]. + raise_interrupt: This is deprecated, `KeyboardInterrupt` will cause this coroutine to be cancelled and then + be raised by the top level `asyncio.run` call or equivalent, and should be caught there. + See [#136](https://github.com/samuelcolvin/watchfiles/issues/136) + force_polling: if true, always use polling instead of file system notifications, default is `None` where + `force_polling` is set to `True` if the `WATCHFILES_FORCE_POLLING` environment variable exists. + poll_delay_ms: delay between polling for changes, only used if `force_polling=True`. + recursive: if `True`, watch for changes in sub-directories recursively, otherwise watch only for changes in the + top-level directory, default is `True`. + ignore_permission_denied: if `True`, will ignore permission denied errors, otherwise will raise them by default. + Setting the `WATCHFILES_IGNORE_PERMISSION_DENIED` environment variable will set this value too. + + Yields: + The generator yields sets of [`FileChange`][watchfiles.main.FileChange]s. + + ```py title="Example of awatch usage" + import asyncio + from watchfiles import awatch + + async def main(): + async for changes in awatch('./first/dir', './second/dir'): + print(changes) + + if __name__ == '__main__': + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('stopped via KeyboardInterrupt') + ``` + + ```py title="Example of awatch usage with a stop event" + import asyncio + from watchfiles import awatch + + async def main(): + stop_event = asyncio.Event() + + async def stop_soon(): + await asyncio.sleep(3) + stop_event.set() + + stop_soon_task = asyncio.create_task(stop_soon()) + + async for changes in awatch('/path/to/dir', stop_event=stop_event): + print(changes) + + # cleanup by awaiting the (now complete) stop_soon_task + await stop_soon_task + + asyncio.run(main()) + ``` + """ + if raise_interrupt is not None: + warnings.warn( + 'raise_interrupt is deprecated, KeyboardInterrupt will cause this coroutine to be cancelled and then ' + 'be raised by the top level asyncio.run call or equivalent, and should be caught there. See #136.', + DeprecationWarning, + ) + + if stop_event is None: + stop_event_: 'AnyEvent' = anyio.Event() + else: + stop_event_ = stop_event + + force_polling = _default_force_polling(force_polling) + ignore_permission_denied = _default_ignore_permission_denied(ignore_permission_denied) + with RustNotify( + [str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive, ignore_permission_denied + ) as watcher: + timeout = _calc_async_timeout(rust_timeout) + CancelledError = anyio.get_cancelled_exc_class() + + while True: + async with anyio.create_task_group() as tg: + try: + raw_changes = await anyio.to_thread.run_sync(watcher.watch, debounce, step, timeout, stop_event_) + except (CancelledError, KeyboardInterrupt): + stop_event_.set() + # suppressing KeyboardInterrupt wouldn't stop it getting raised by the top level asyncio.run call + raise + tg.cancel_scope.cancel() + + if raw_changes == 'timeout': + if yield_on_timeout: + yield set() + else: + logger.debug('rust notify timeout, continuing') + elif raw_changes == 'stop': + return + elif raw_changes == 'signal': + # in theory the watch thread should never get a signal + raise RuntimeError('watch thread unexpectedly received a signal') + else: + changes = _prep_changes(raw_changes, watch_filter) + if changes: + _log_changes(changes) + yield changes + else: + logger.debug('all changes filtered out, raw_changes=%s', raw_changes) + + +def _prep_changes( + raw_changes: Set[Tuple[int, str]], watch_filter: Optional[Callable[[Change, str], bool]] +) -> Set[FileChange]: + # if we wanted to be really snazzy, we could move this into rust + changes = {(Change(change), path) for change, path in raw_changes} + if watch_filter: + changes = {c for c in changes if watch_filter(c[0], c[1])} + return changes + + +def _log_changes(changes: Set[FileChange]) -> None: + if logger.isEnabledFor(logging.INFO): # pragma: no branch + count = len(changes) + plural = '' if count == 1 else 's' + if logger.isEnabledFor(logging.DEBUG): + logger.debug('%d change%s detected: %s', count, plural, changes) + else: + logger.info('%d change%s detected', count, plural) + + +def _calc_async_timeout(timeout: Optional[int]) -> int: + """ + see https://github.com/samuelcolvin/watchfiles/issues/110 + """ + if timeout is None: + if sys.platform == 'win32': + return 1_000 + else: + return 5_000 + else: + return timeout + + +def _default_force_polling(force_polling: Optional[bool]) -> bool: + """ + See docstring for `watch` above for details. + + See samuelcolvin/watchfiles#167 and samuelcolvin/watchfiles#187 for discussion and rationale. + """ + if force_polling is not None: + return force_polling + env_var = os.getenv('WATCHFILES_FORCE_POLLING') + if env_var: + return env_var.lower() not in {'false', 'disable', 'disabled'} + else: + return _auto_force_polling() + + +def _auto_force_polling() -> bool: + """ + Whether to auto-enable force polling, it should be enabled automatically only on WSL. + + See samuelcolvin/watchfiles#187 for discussion. + """ + import platform + + uname = platform.uname() + return 'microsoft-standard' in uname.release.lower() and uname.system.lower() == 'linux' + + +def _default_ignore_permission_denied(ignore_permission_denied: Optional[bool]) -> bool: + if ignore_permission_denied is not None: + return ignore_permission_denied + env_var = os.getenv('WATCHFILES_IGNORE_PERMISSION_DENIED') + return bool(env_var) diff --git a/venv/lib/python3.11/site-packages/watchfiles/py.typed b/venv/lib/python3.11/site-packages/watchfiles/py.typed new file mode 100644 index 0000000..7cd6d6f --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The watchfiles package uses inline types. diff --git a/venv/lib/python3.11/site-packages/watchfiles/run.py b/venv/lib/python3.11/site-packages/watchfiles/run.py new file mode 100644 index 0000000..d1e2494 --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/run.py @@ -0,0 +1,441 @@ +import contextlib +import json +import logging +import os +import re +import shlex +import signal +import subprocess +import sys +from importlib import import_module +from multiprocessing import get_context +from multiprocessing.context import SpawnProcess +from pathlib import Path +from time import sleep +from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Set, Tuple, Union + +import anyio + +from .filters import DefaultFilter +from .main import Change, FileChange, awatch, watch + +if TYPE_CHECKING: + try: + from typing import Literal + except ImportError: + from typing_extensions import Literal # type: ignore[misc] + +__all__ = 'run_process', 'arun_process', 'detect_target_type', 'import_string' +logger = logging.getLogger('watchfiles.main') + + +def run_process( + *paths: Union[Path, str], + target: Union[str, Callable[..., Any]], + args: Tuple[Any, ...] = (), + kwargs: Optional[Dict[str, Any]] = None, + target_type: "Literal['function', 'command', 'auto']" = 'auto', + callback: Optional[Callable[[Set[FileChange]], None]] = None, + watch_filter: Optional[Callable[[Change, str], bool]] = DefaultFilter(), + grace_period: float = 0, + debounce: int = 1_600, + step: int = 50, + debug: bool = False, + sigint_timeout: int = 5, + sigkill_timeout: int = 1, + recursive: bool = True, + ignore_permission_denied: bool = False, +) -> int: + """ + Run a process and restart it upon file changes. + + `run_process` can work in two ways: + + * Using `multiprocessing.Process` † to run a python function + * Or, using `subprocess.Popen` to run a command + + !!! note + + **†** technically `multiprocessing.get_context('spawn').Process` to avoid forking and improve + code reload/import. + + Internally, `run_process` uses [`watch`][watchfiles.watch] with `raise_interrupt=False` so the function + exits cleanly upon `Ctrl+C`. + + Args: + *paths: matches the same argument of [`watch`][watchfiles.watch] + target: function or command to run + args: arguments to pass to `target`, only used if `target` is a function + kwargs: keyword arguments to pass to `target`, only used if `target` is a function + target_type: type of target. Can be `'function'`, `'command'`, or `'auto'` in which case + [`detect_target_type`][watchfiles.run.detect_target_type] is used to determine the type. + callback: function to call on each reload, the function should accept a set of changes as the sole argument + watch_filter: matches the same argument of [`watch`][watchfiles.watch] + grace_period: number of seconds after the process is started before watching for changes + debounce: matches the same argument of [`watch`][watchfiles.watch] + step: matches the same argument of [`watch`][watchfiles.watch] + debug: matches the same argument of [`watch`][watchfiles.watch] + sigint_timeout: the number of seconds to wait after sending sigint before sending sigkill + sigkill_timeout: the number of seconds to wait after sending sigkill before raising an exception + recursive: matches the same argument of [`watch`][watchfiles.watch] + + Returns: + number of times the function was reloaded. + + ```py title="Example of run_process running a function" + from watchfiles import run_process + + def callback(changes): + print('changes detected:', changes) + + def foobar(a, b): + print('foobar called with:', a, b) + + if __name__ == '__main__': + run_process('./path/to/dir', target=foobar, args=(1, 2), callback=callback) + ``` + + As well as using a `callback` function, changes can be accessed from within the target function, + using the `WATCHFILES_CHANGES` environment variable. + + ```py title="Example of run_process accessing changes" + from watchfiles import run_process + + def foobar(a, b, c): + # changes will be an empty list "[]" the first time the function is called + changes = os.getenv('WATCHFILES_CHANGES') + changes = json.loads(changes) + print('foobar called due to changes:', changes) + + if __name__ == '__main__': + run_process('./path/to/dir', target=foobar, args=(1, 2, 3)) + ``` + + Again with the target as `command`, `WATCHFILES_CHANGES` can be used + to access changes. + + ```bash title="example.sh" + echo "changers: ${WATCHFILES_CHANGES}" + ``` + + ```py title="Example of run_process running a command" + from watchfiles import run_process + + if __name__ == '__main__': + run_process('.', target='./example.sh') + ``` + """ + if target_type == 'auto': + target_type = detect_target_type(target) + + logger.debug('running "%s" as %s', target, target_type) + catch_sigterm() + process = start_process(target, target_type, args, kwargs) + reloads = 0 + + if grace_period: + logger.debug('sleeping for %s seconds before watching for changes', grace_period) + sleep(grace_period) + + try: + for changes in watch( + *paths, + watch_filter=watch_filter, + debounce=debounce, + step=step, + debug=debug, + raise_interrupt=False, + recursive=recursive, + ignore_permission_denied=ignore_permission_denied, + ): + callback and callback(changes) + process.stop(sigint_timeout=sigint_timeout, sigkill_timeout=sigkill_timeout) + process = start_process(target, target_type, args, kwargs, changes) + reloads += 1 + finally: + process.stop() + return reloads + + +async def arun_process( + *paths: Union[Path, str], + target: Union[str, Callable[..., Any]], + args: Tuple[Any, ...] = (), + kwargs: Optional[Dict[str, Any]] = None, + target_type: "Literal['function', 'command', 'auto']" = 'auto', + callback: Optional[Callable[[Set[FileChange]], Any]] = None, + watch_filter: Optional[Callable[[Change, str], bool]] = DefaultFilter(), + grace_period: float = 0, + debounce: int = 1_600, + step: int = 50, + debug: bool = False, + recursive: bool = True, + ignore_permission_denied: bool = False, +) -> int: + """ + Async equivalent of [`run_process`][watchfiles.run_process], all arguments match those of `run_process` except + `callback` which can be a coroutine. + + Starting and stopping the process and watching for changes is done in a separate thread. + + As with `run_process`, internally `arun_process` uses [`awatch`][watchfiles.awatch], however `KeyboardInterrupt` + cannot be caught and suppressed in `awatch` so these errors need to be caught separately, see below. + + ```py title="Example of arun_process usage" + import asyncio + from watchfiles import arun_process + + async def callback(changes): + await asyncio.sleep(0.1) + print('changes detected:', changes) + + def foobar(a, b): + print('foobar called with:', a, b) + + async def main(): + await arun_process('.', target=foobar, args=(1, 2), callback=callback) + + if __name__ == '__main__': + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('stopped via KeyboardInterrupt') + ``` + """ + import inspect + + if target_type == 'auto': + target_type = detect_target_type(target) + + logger.debug('running "%s" as %s', target, target_type) + catch_sigterm() + process = await anyio.to_thread.run_sync(start_process, target, target_type, args, kwargs) + reloads = 0 + + if grace_period: + logger.debug('sleeping for %s seconds before watching for changes', grace_period) + await anyio.sleep(grace_period) + + async for changes in awatch( + *paths, + watch_filter=watch_filter, + debounce=debounce, + step=step, + debug=debug, + recursive=recursive, + ignore_permission_denied=ignore_permission_denied, + ): + if callback is not None: + r = callback(changes) + if inspect.isawaitable(r): + await r + + await anyio.to_thread.run_sync(process.stop) + process = await anyio.to_thread.run_sync(start_process, target, target_type, args, kwargs, changes) + reloads += 1 + await anyio.to_thread.run_sync(process.stop) + return reloads + + +# Use spawn context to make sure code run in subprocess +# does not reuse imported modules in main process/context +spawn_context = get_context('spawn') + + +def split_cmd(cmd: str) -> List[str]: + import platform + + posix = platform.uname().system.lower() != 'windows' + return shlex.split(cmd, posix=posix) + + +def start_process( + target: Union[str, Callable[..., Any]], + target_type: "Literal['function', 'command']", + args: Tuple[Any, ...], + kwargs: Optional[Dict[str, Any]], + changes: Optional[Set[FileChange]] = None, +) -> 'CombinedProcess': + if changes is None: + changes_env_var = '[]' + else: + changes_env_var = json.dumps([[c.raw_str(), p] for c, p in changes]) + + os.environ['WATCHFILES_CHANGES'] = changes_env_var + + process: 'Union[SpawnProcess, subprocess.Popen[bytes]]' + if target_type == 'function': + kwargs = kwargs or {} + if isinstance(target, str): + args = target, get_tty_path(), args, kwargs + target_ = run_function + kwargs = {} + else: + target_ = target + + process = spawn_context.Process(target=target_, args=args, kwargs=kwargs) + process.start() + else: + if args or kwargs: + logger.warning('ignoring args and kwargs for "command" target') + + assert isinstance(target, str), 'target must be a string to run as a command' + popen_args = split_cmd(target) + process = subprocess.Popen(popen_args) + return CombinedProcess(process) + + +def detect_target_type(target: Union[str, Callable[..., Any]]) -> "Literal['function', 'command']": + """ + Used by [`run_process`][watchfiles.run_process], [`arun_process`][watchfiles.arun_process] + and indirectly the CLI to determine the target type with `target_type` is `auto`. + + Detects the target type - either `function` or `command`. This method is only called with `target_type='auto'`. + + The following logic is employed: + + * If `target` is not a string, it is assumed to be a function + * If `target` ends with `.py` or `.sh`, it is assumed to be a command + * Otherwise, the target is assumed to be a function if it matches the regex `[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)+` + + If this logic does not work for you, specify the target type explicitly using the `target_type` function argument + or `--target-type` command line argument. + + Args: + target: The target value + + Returns: + either `'function'` or `'command'` + """ + if not isinstance(target, str): + return 'function' + elif target.endswith(('.py', '.sh')): + return 'command' + elif re.fullmatch(r'[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)+', target): + return 'function' + else: + return 'command' + + +class CombinedProcess: + def __init__(self, p: 'Union[SpawnProcess, subprocess.Popen[bytes]]'): + self._p = p + assert self.pid is not None, 'process not yet spawned' + + def stop(self, sigint_timeout: int = 5, sigkill_timeout: int = 1) -> None: + os.environ.pop('WATCHFILES_CHANGES', None) + if self.is_alive(): + logger.debug('stopping process...') + + os.kill(self.pid, signal.SIGINT) + + try: + self.join(sigint_timeout) + except subprocess.TimeoutExpired: + # Capture this exception to allow the self.exitcode to be reached. + # This will allow the SIGKILL to be sent, otherwise it is swallowed up. + logger.warning('SIGINT timed out after %r seconds', sigint_timeout) + pass + + if self.exitcode is None: + logger.warning('process has not terminated, sending SIGKILL') + os.kill(self.pid, signal.SIGKILL) + self.join(sigkill_timeout) + else: + logger.debug('process stopped') + else: + logger.warning('process already dead, exit code: %d', self.exitcode) + + def is_alive(self) -> bool: + if isinstance(self._p, SpawnProcess): + return self._p.is_alive() + else: + return self._p.poll() is None + + @property + def pid(self) -> int: + # we check the process has always been spawned when CombinedProcess is initialised + return self._p.pid # type: ignore[return-value] + + def join(self, timeout: int) -> None: + if isinstance(self._p, SpawnProcess): + self._p.join(timeout) + else: + self._p.wait(timeout) + + @property + def exitcode(self) -> Optional[int]: + if isinstance(self._p, SpawnProcess): + return self._p.exitcode + else: + return self._p.returncode + + +def run_function(function: str, tty_path: Optional[str], args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> None: + with set_tty(tty_path): + func = import_string(function) + func(*args, **kwargs) + + +def import_string(dotted_path: str) -> Any: + """ + Stolen approximately from django. Import a dotted module path and return the attribute/class designated by the + last name in the path. Raise ImportError if the import fails. + """ + try: + module_path, class_name = dotted_path.strip(' ').rsplit('.', 1) + except ValueError as e: + raise ImportError(f'"{dotted_path}" doesn\'t look like a module path') from e + + module = import_module(module_path) + try: + return getattr(module, class_name) + except AttributeError as e: + raise ImportError(f'Module "{module_path}" does not define a "{class_name}" attribute') from e + + +def get_tty_path() -> Optional[str]: # pragma: no cover + """ + Return the path to the current TTY, if any. + + Virtually impossible to test in pytest, hence no cover. + """ + try: + return os.ttyname(sys.stdin.fileno()) + except OSError: + # fileno() always fails with pytest + return '/dev/tty' + except AttributeError: + # on Windows. No idea of a better solution + return None + + +@contextlib.contextmanager +def set_tty(tty_path: Optional[str]) -> Generator[None, None, None]: + if tty_path: + try: + with open(tty_path) as tty: # pragma: no cover + sys.stdin = tty + yield + except OSError: + # eg. "No such device or address: '/dev/tty'", see https://github.com/samuelcolvin/watchfiles/issues/40 + yield + else: + # currently on windows tty_path is None and there's nothing we can do here + yield + + +def raise_keyboard_interrupt(signum: int, _frame: Any) -> None: # pragma: no cover + logger.warning('received signal %s, raising KeyboardInterrupt', signal.Signals(signum)) + raise KeyboardInterrupt + + +def catch_sigterm() -> None: + """ + Catch SIGTERM and raise KeyboardInterrupt instead. This means watchfiles will stop quickly + on `docker compose stop` and other cases where SIGTERM is sent. + + Without this the watchfiles process will be killed while a running process will continue uninterrupted. + """ + logger.debug('registering handler for SIGTERM on watchfiles process %d', os.getpid()) + signal.signal(signal.SIGTERM, raise_keyboard_interrupt) diff --git a/venv/lib/python3.11/site-packages/watchfiles/version.py b/venv/lib/python3.11/site-packages/watchfiles/version.py new file mode 100644 index 0000000..f55721f --- /dev/null +++ b/venv/lib/python3.11/site-packages/watchfiles/version.py @@ -0,0 +1,5 @@ +from ._rust_notify import __version__ + +__all__ = ('VERSION',) + +VERSION = __version__ -- cgit v1.2.3