From ea31274f4101229744e5fd90df5b74b69e44b53b Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Thu, 24 Aug 2023 14:03:43 +0200 Subject: [PATCH] Add ignore_permission_denied option (#224) * Support ignoring errors * Add ignore_permission_denied * update cli * Make test specific to Linux * apply comments * add environment variable * empty env var equivilent to unset * fix test after change * fix tests and docs --------- Co-authored-by: Samuel Colvin --- docs/api/rust_backend.md | 4 +- docs/cli_help.txt | 5 ++- src/lib.rs | 18 ++++++-- tests/test_cli.py | 30 +++++++++++++ tests/test_force_polling.py | 4 +- tests/test_rust_notify.py | 85 +++++++++++++++++++++++++------------ watchfiles/_rust_notify.pyi | 9 +++- watchfiles/cli.py | 6 +++ watchfiles/main.py | 23 +++++++++- watchfiles/run.py | 11 ++++- 10 files changed, 156 insertions(+), 39 deletions(-) diff --git a/docs/api/rust_backend.md b/docs/api/rust_backend.md index 52444d1..b7512e4 100644 --- a/docs/api/rust_backend.md +++ b/docs/api/rust_backend.md @@ -12,7 +12,7 @@ The rust backend can be accessed directly as follows: title="Rust backend example" from watchfiles._rust_notify import RustNotify -r = RustNotify(['first/path', 'second/path'], False, False, 0, True) +r = RustNotify(['first/path', 'second/path'], False, False, 0, True, False) changes = r.watch(1_600, 50, 100, None) print(changes) @@ -26,7 +26,7 @@ Or using `RustNotify` as a context manager: title="Rust backend context manager example" from watchfiles._rust_notify import RustNotify -with RustNotify(['first/path', 'second/path'], False, False, 0, True) as r: +with RustNotify(['first/path', 'second/path'], False, False, 0, True, False) as r: changes = r.watch(1_600, 50, 100, None) print(changes) ``` diff --git a/docs/cli_help.txt b/docs/cli_help.txt index 0912b33..2568d46 100644 --- a/docs/cli_help.txt +++ b/docs/cli_help.txt @@ -4,7 +4,8 @@ usage: watchfiles [-h] [--ignore-paths [IGNORE_PATHS]] [--non-recursive] [--verbosity [{warning,info,debug}]] [--sigint-timeout [SIGINT_TIMEOUT]] [--grace-period [GRACE_PERIOD]] - [--sigkill-timeout [SIGKILL_TIMEOUT]] [--version] + [--sigkill-timeout [SIGKILL_TIMEOUT]] + [--ignore-permission-denied] [--version] target [paths ...] Watch one or more directories and execute either a shell command or a python function on file changes. @@ -41,4 +42,6 @@ options: Number of seconds after the process is started before watching for changes. --sigkill-timeout [SIGKILL_TIMEOUT] How long to wait for the sigkill timeout before issuing a timeout exception. + --ignore-permission-denied + Ignore permission denied errors while watching files and directories. --version, -V show program's version number and exit diff --git a/src/lib.rs b/src/lib.rs index 5b19ab4..b8fa6d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,14 +67,23 @@ fn map_watch_error(error: notify::Error) -> PyErr { // macro to avoid duplicated code below macro_rules! watcher_paths { - ($watcher:ident, $paths:ident, $debug:ident, $recursive:ident) => { + ($watcher:ident, $paths:ident, $debug:ident, $recursive:ident, $ignore_permission_denied:ident) => { let mode = if $recursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive }; for watch_path in $paths.into_iter() { - $watcher.watch(Path::new(&watch_path), mode).map_err(map_watch_error)?; + let result = $watcher.watch(Path::new(&watch_path), mode); + match result { + Err(err) => { + let err = map_watch_error(err); + if !$ignore_permission_denied { + return Err(err); + } + } + _ => (), + } } if $debug { eprintln!("watcher: {:?}", $watcher); @@ -101,6 +110,7 @@ impl RustNotify { force_polling: bool, poll_delay_ms: u64, recursive: bool, + ignore_permission_denied: bool, ) -> PyResult { let changes: Arc>> = Arc::new(Mutex::new(HashSet::<(u8, String)>::new())); let error: Arc>> = Arc::new(Mutex::new(None)); @@ -184,7 +194,7 @@ impl RustNotify { Ok(watcher) => watcher, Err(e) => return wf_error!($msg_template, e), }; - watcher_paths!(watcher, watch_paths, debug, recursive); + watcher_paths!(watcher, watch_paths, debug, recursive, ignore_permission_denied); Ok(WatcherEnum::Poll(watcher)) }}; } @@ -195,7 +205,7 @@ impl RustNotify { match RecommendedWatcher::new(event_handler.clone(), NotifyConfig::default()) { Ok(watcher) => { let mut watcher = watcher; - watcher_paths!(watcher, watch_paths, debug, recursive); + watcher_paths!(watcher, watch_paths, debug, recursive, ignore_permission_denied); Ok(WatcherEnum::Recommended(watcher)) } Err(error) => { diff --git a/tests/test_cli.py b/tests/test_cli.py index 983b2ab..d22571a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -26,6 +26,7 @@ def test_function(mocker, tmp_path): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) @@ -54,6 +55,7 @@ def test_ignore_paths(mocker, tmp_work_path): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) @@ -108,6 +110,7 @@ def test_command(mocker, tmp_work_path): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) @@ -126,6 +129,7 @@ def test_verbosity(mocker, tmp_path): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) @@ -144,6 +148,7 @@ def test_verbose(mocker, tmp_path): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) @@ -162,6 +167,7 @@ def test_non_recursive(mocker, tmp_path): sigint_timeout=5, sigkill_timeout=1, recursive=False, + ignore_permission_denied=False, ) @@ -180,6 +186,7 @@ def test_filter_all(mocker, tmp_path, capsys): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) out, err = capsys.readouterr() assert out == '' @@ -201,6 +208,7 @@ def test_filter_default(mocker, tmp_path): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) @@ -219,6 +227,7 @@ def test_set_type(mocker, tmp_path): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) @@ -275,6 +284,7 @@ def test_args(mocker, tmp_path, reset_argv, caplog): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) assert sys.argv == ['os.getcwd', '--version'] assert 'WARNING: --args' not in caplog.text @@ -297,5 +307,25 @@ def test_args_command(mocker, tmp_path, caplog): sigint_timeout=5, sigkill_timeout=1, recursive=True, + ignore_permission_denied=False, ) assert 'WARNING: --args is only used when the target is a function\n' in caplog.text + + +def test_ignore_permission_denied(mocker, tmp_path): + mocker.patch('watchfiles.cli.sys.stdin.fileno') + mocker.patch('os.ttyname', return_value='/path/to/tty') + mock_run_process = mocker.patch('watchfiles.cli.run_process') + cli('--ignore-permission-denied', 'os.getcwd', str(tmp_path)) + mock_run_process.assert_called_once_with( + tmp_path, + target='os.getcwd', + target_type='function', + watch_filter=IsInstance(DefaultFilter, only_direct_instance=True), + debug=False, + grace_period=0, + sigint_timeout=5, + sigkill_timeout=1, + recursive=True, + ignore_permission_denied=True, + ) diff --git a/tests/test_force_polling.py b/tests/test_force_polling.py index e3805ba..25af91d 100644 --- a/tests/test_force_polling.py +++ b/tests/test_force_polling.py @@ -29,7 +29,7 @@ def test_watch_polling_not_env(mocker): for _ in watch('.'): pass - m.assert_called_once_with(['.'], False, False, 300, True) + m.assert_called_once_with(['.'], False, False, 300, True, False) def test_watch_polling_env(mocker, env: SetEnv): @@ -39,7 +39,7 @@ def test_watch_polling_env(mocker, env: SetEnv): for _ in watch('.'): pass - m.assert_called_once_with(['.'], False, True, 300, True) + m.assert_called_once_with(['.'], False, True, 300, True, False) @pytest.mark.parametrize( diff --git a/tests/test_rust_notify.py b/tests/test_rust_notify.py index 17dfa09..4366cf8 100644 --- a/tests/test_rust_notify.py +++ b/tests/test_rust_notify.py @@ -1,23 +1,28 @@ import sys from pathlib import Path +from typing import TYPE_CHECKING import pytest from watchfiles._rust_notify import RustNotify +from watchfiles.main import _default_ignore_permission_denied + +if TYPE_CHECKING: + from .conftest import SetEnv skip_unless_linux = pytest.mark.skipif('linux' not in sys.platform, reason='avoid differences on other systems') skip_windows = pytest.mark.skipif(sys.platform == 'win32', reason='fails on Windows') def test_add(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, False, 0, True) + watcher = RustNotify([str(test_dir)], True, False, 0, True, False) (test_dir / 'new_file.txt').write_text('foobar') assert watcher.watch(200, 50, 500, None) == {(1, str(test_dir / 'new_file.txt'))} def test_add_non_recursive(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, False, 0, False) + watcher = RustNotify([str(test_dir)], True, False, 0, False, False) (test_dir / 'new_file_non_recursive.txt').write_text('foobar') (test_dir / 'dir_a' / 'new_file_non_recursive.txt').write_text('foobar') @@ -26,7 +31,7 @@ def test_add_non_recursive(test_dir: Path): def test_close(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, False, 0, True) + watcher = RustNotify([str(test_dir)], True, False, 0, True, False) assert repr(watcher).startswith('RustNotify(Recommended(\n') watcher.close() @@ -37,7 +42,7 @@ def test_close(test_dir: Path): def test_modify_write(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, False, 0, True) + watcher = RustNotify([str(test_dir)], True, False, 0, True, False) (test_dir / 'a.txt').write_text('this is new') @@ -45,7 +50,7 @@ def test_modify_write(test_dir: Path): def test_modify_write_non_recursive(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, False, 0, False) + watcher = RustNotify([str(test_dir)], True, False, 0, False, False) (test_dir / 'a_non_recursive.txt').write_text('this is new') (test_dir / 'dir_a' / 'a_non_recursive.txt').write_text('this is new') @@ -57,7 +62,7 @@ def test_modify_write_non_recursive(test_dir: Path): @skip_windows def test_modify_chmod(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, False, 0, True) + watcher = RustNotify([str(test_dir)], True, False, 0, True, False) (test_dir / 'b.txt').chmod(0o444) @@ -65,7 +70,7 @@ def test_modify_chmod(test_dir: Path): def test_delete(test_dir: Path): - watcher = RustNotify([str(test_dir)], False, False, 0, True) + watcher = RustNotify([str(test_dir)], False, False, 0, True, False) (test_dir / 'c.txt').unlink() @@ -75,7 +80,7 @@ def test_delete(test_dir: Path): def test_delete_non_recursive(test_dir: Path): - watcher = RustNotify([str(test_dir)], False, False, 0, False) + watcher = RustNotify([str(test_dir)], False, False, 0, False, False) (test_dir / 'c_non_recursive.txt').unlink() (test_dir / 'dir_a' / 'c_non_recursive.txt').unlink() @@ -93,7 +98,7 @@ def test_move_in(test_dir: Path): assert dst.is_dir() move_files = 'a.txt', 'b.txt' - watcher = RustNotify([str(dst)], False, False, 0, True) + watcher = RustNotify([str(dst)], False, False, 0, True, False) for f in move_files: (src / f).rename(dst / f) @@ -110,7 +115,7 @@ def test_move_out(test_dir: Path): dst = test_dir / 'dir_b' move_files = 'c.txt', 'd.txt' - watcher = RustNotify([str(src)], False, False, 0, True) + watcher = RustNotify([str(src)], False, False, 0, True, False) for f in move_files: (src / f).rename(dst / f) @@ -127,7 +132,7 @@ def test_move_internal(test_dir: Path): dst = test_dir / 'dir_b' move_files = 'e.txt', 'f.txt' - watcher = RustNotify([str(test_dir)], False, False, 0, True) + watcher = RustNotify([str(test_dir)], False, False, 0, True, False) for f in move_files: (src / f).rename(dst / f) @@ -148,24 +153,24 @@ def test_move_internal(test_dir: Path): def test_does_not_exist(tmp_path: Path): p = tmp_path / 'missing' with pytest.raises(FileNotFoundError): - RustNotify([str(p)], False, False, 0, True) + RustNotify([str(p)], False, False, 0, True, False) @skip_unless_linux def test_does_not_exist_message(tmp_path: Path): p = tmp_path / 'missing' with pytest.raises(FileNotFoundError, match='No such file or directory'): - RustNotify([str(p)], False, False, 0, True) + RustNotify([str(p)], False, False, 0, True, False) def test_does_not_exist_polling(tmp_path: Path): p = tmp_path / 'missing' with pytest.raises(FileNotFoundError, match='No such file or directory'): - RustNotify([str(p)], False, True, 0, True) + RustNotify([str(p)], False, True, 0, True, False) def test_rename(test_dir: Path): - watcher = RustNotify([str(test_dir)], False, False, 0, True) + watcher = RustNotify([str(test_dir)], False, False, 0, True, False) f = test_dir / 'a.txt' f.rename(f.with_suffix('.new')) @@ -181,7 +186,7 @@ def test_watch_multiple(tmp_path: Path): foo.mkdir() bar = tmp_path / 'bar' bar.mkdir() - watcher = RustNotify([str(foo), str(bar)], False, False, 0, True) + watcher = RustNotify([str(foo), str(bar)], False, False, 0, True, False) (tmp_path / 'not_included.txt').write_text('foobar') (foo / 'foo.txt').write_text('foobar') @@ -195,14 +200,14 @@ def test_watch_multiple(tmp_path: Path): def test_wrong_type_event(test_dir: Path, time_taken): - watcher = RustNotify([str(test_dir)], False, False, 0, True) + watcher = RustNotify([str(test_dir)], False, False, 0, True, False) with pytest.raises(AttributeError, match="'object' object has no attribute 'is_set'"): watcher.watch(100, 1, 500, object()) def test_wrong_type_event_is_set(test_dir: Path, time_taken): - watcher = RustNotify([str(test_dir)], False, False, 0, True) + watcher = RustNotify([str(test_dir)], False, False, 0, True, False) event = type('BadEvent', (), {'is_set': 123})() with pytest.raises(TypeError, match="'stop_event.is_set' must be callable"): @@ -211,7 +216,7 @@ def test_wrong_type_event_is_set(test_dir: Path, time_taken): @skip_unless_linux def test_return_timeout(test_dir: Path, time_taken): - watcher = RustNotify([str(test_dir)], False, False, 0, True) + watcher = RustNotify([str(test_dir)], False, False, 0, True, False) with time_taken(40, 70): assert watcher.watch(20, 1, 50, None) == 'timeout' @@ -227,7 +232,7 @@ def is_set(self) -> bool: @skip_unless_linux def test_return_event_set(test_dir: Path, time_taken): - watcher = RustNotify([str(test_dir)], False, False, 0, True) + watcher = RustNotify([str(test_dir)], False, False, 0, True, False) with time_taken(0, 20): assert watcher.watch(100, 1, 500, AbstractEvent(True)) == 'stop' @@ -235,7 +240,7 @@ def test_return_event_set(test_dir: Path, time_taken): @skip_unless_linux def test_return_event_unset(test_dir: Path, time_taken): - watcher = RustNotify([str(test_dir)], False, False, 0, True) + watcher = RustNotify([str(test_dir)], False, False, 0, True, False) with time_taken(40, 70): assert watcher.watch(20, 1, 50, AbstractEvent(False)) == 'timeout' @@ -244,7 +249,7 @@ def test_return_event_unset(test_dir: Path, time_taken): @skip_unless_linux def test_return_debounce_no_timeout(test_dir: Path, time_taken): # would return sooner if the timeout logic wasn't in an else clause - watcher = RustNotify([str(test_dir)], True, False, 0, True) + watcher = RustNotify([str(test_dir)], True, False, 0, True, False) (test_dir / 'debounce.txt').write_text('foobar') with time_taken(50, 130): @@ -266,7 +271,7 @@ def test_rename_multiple_inside(tmp_path: Path): d2 = tmp_path / 'd2' d2.mkdir() - watcher_all = RustNotify([str(tmp_path)], False, False, 0, True) + watcher_all = RustNotify([str(tmp_path)], False, False, 0, True, False) f1.rename(d2 / '1.txt') f2.rename(d2 / '2.txt') @@ -284,7 +289,7 @@ def test_rename_multiple_inside(tmp_path: Path): @skip_windows def test_polling(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, True, 100, True) + watcher = RustNotify([str(test_dir)], True, True, 100, True, False) (test_dir / 'test_polling.txt').write_text('foobar') changes = watcher.watch(200, 50, 500, None) @@ -292,13 +297,41 @@ def test_polling(test_dir: Path): def test_not_polling_repr(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, False, 123, True) + watcher = RustNotify([str(test_dir)], True, False, 123, True, False) r = repr(watcher) assert r.startswith('RustNotify(Recommended(\n') def test_polling_repr(test_dir: Path): - watcher = RustNotify([str(test_dir)], True, True, 123, True) + watcher = RustNotify([str(test_dir)], True, True, 123, True, False) r = repr(watcher) assert r.startswith('RustNotify(Poll(\n PollWatcher {\n') assert 'delay: 123ms' in r + + +@skip_unless_linux +def test_ignore_permission_denied(): + RustNotify(['/'], False, False, 0, True, True) + + with pytest.raises(PermissionError): + RustNotify(['/'], False, False, 0, True, False) + + +@pytest.mark.parametrize( + 'env_var,arg,expected', + [ + (None, True, True), + (None, False, False), + (None, None, False), + ('', True, True), + ('', False, False), + ('', None, False), + ('1', True, True), + ('1', False, False), + ('1', None, True), + ], +) +def test_default_ignore_permission_denied(env: 'SetEnv', env_var, arg, expected): + if env_var is not None: + env('WATCHFILES_IGNORE_PERMISSION_DENIED', env_var) + assert _default_ignore_permission_denied(arg) == expected diff --git a/watchfiles/_rust_notify.pyi b/watchfiles/_rust_notify.pyi index 7b68bfe..63eacda 100644 --- a/watchfiles/_rust_notify.pyi +++ b/watchfiles/_rust_notify.pyi @@ -15,7 +15,13 @@ class RustNotify: """ def __init__( - self, watch_paths: List[str], debug: bool, force_polling: bool, poll_delay_ms: int, recursive: bool + 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. @@ -29,6 +35,7 @@ class RustNotify: 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, diff --git a/watchfiles/cli.py b/watchfiles/cli.py index 945ded0..f1e1ddd 100644 --- a/watchfiles/cli.py +++ b/watchfiles/cli.py @@ -118,6 +118,11 @@ def cli(*args_: str) -> None: 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) @@ -172,6 +177,7 @@ def cli(*args_: str) -> None: 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, ) diff --git a/watchfiles/main.py b/watchfiles/main.py index 2e116e8..1df7e54 100644 --- a/watchfiles/main.py +++ b/watchfiles/main.py @@ -63,6 +63,7 @@ def watch( 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. @@ -99,6 +100,8 @@ def watch( 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. @@ -111,7 +114,10 @@ def watch( ``` """ force_polling = _default_force_polling(force_polling) - with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher: + 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': @@ -149,6 +155,7 @@ async def awatch( # noqa C901 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. @@ -178,6 +185,8 @@ async def awatch( # noqa C901 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. @@ -232,7 +241,10 @@ async def stop_soon(): stop_event_ = stop_event force_polling = _default_force_polling(force_polling) - with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive) as watcher: + 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() @@ -323,3 +335,10 @@ def _auto_force_polling() -> bool: 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/watchfiles/run.py b/watchfiles/run.py index a59aa13..71e9b3a 100644 --- a/watchfiles/run.py +++ b/watchfiles/run.py @@ -44,6 +44,7 @@ def run_process( 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. @@ -144,6 +145,7 @@ def foobar(a, b, c): 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) @@ -167,6 +169,7 @@ async def arun_process( 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 @@ -212,7 +215,13 @@ async def main(): await anyio.sleep(grace_period) async for changes in awatch( - *paths, watch_filter=watch_filter, debounce=debounce, step=step, debug=debug, recursive=recursive + *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)