Skip to content

Commit

Permalink
Add ignore_permission_denied
Browse files Browse the repository at this point in the history
  • Loading branch information
aminalaee committed Apr 12, 2023
1 parent b1feba1 commit 38f27b6
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 54 deletions.
4 changes: 2 additions & 2 deletions docs/api/rust_backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, True)
r = RustNotify(['first/path', 'second/path'], False, False, 0, True, False)

changes = r.watch(1_600, 50, 100, None)
print(changes)
Expand All @@ -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, 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)
```
Expand Down
33 changes: 21 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,38 +45,47 @@ struct RustNotify {
watcher: WatcherEnum,
}

fn map_watch_error(error: notify::Error) -> PyErr {
fn map_watch_error(error: notify::Error, ignore_permission_denied: bool) -> Option<PyErr> {
let err_string = error.to_string();
match error.kind {
NotifyErrorKind::PathNotFound => return PyFileNotFoundError::new_err(err_string),
NotifyErrorKind::PathNotFound => return Some(PyFileNotFoundError::new_err(err_string)),
NotifyErrorKind::Generic(ref err) => {
// on Windows, we get a Generic with this message when the path does not exist
if err.as_str() == "Input watch path is neither a file nor a directory." {
return PyFileNotFoundError::new_err(err_string);
return Some(PyFileNotFoundError::new_err(err_string));
}
}
NotifyErrorKind::Io(ref io_error) => match io_error.kind() {
IOErrorKind::NotFound => return PyFileNotFoundError::new_err(err_string),
IOErrorKind::PermissionDenied => return PyPermissionError::new_err(err_string),
IOErrorKind::NotFound => return Some(PyFileNotFoundError::new_err(err_string)),
IOErrorKind::PermissionDenied => {
if !ignore_permission_denied {
return Some(PyPermissionError::new_err(err_string));
}
return None;
}
_ => (),
},
_ => (),
};
PyOSError::new_err(format!("{} ({:?})", err_string, error))
Some(PyOSError::new_err(format!("{} ({:?})", err_string, error)))
}

// macro to avoid duplicated code below
macro_rules! watcher_paths {
($watcher:ident, $paths:ident, $debug:ident, $recursive:ident, $strict_errors: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() {
let result = $watcher.watch(Path::new(&watch_path), mode);
if $strict_errors {
(result.map_err(map_watch_error))?
match result {
Err(err) => match map_watch_error(err, $ignore_permission_denied) {
Some(err) => return Err(err),
_ => (),
},
_ => (),
}
}
if $debug {
Expand Down Expand Up @@ -104,7 +113,7 @@ impl RustNotify {
force_polling: bool,
poll_delay_ms: u64,
recursive: bool,
strict_errors: bool,
ignore_permission_denied: bool,
) -> PyResult<Self> {
let changes: Arc<Mutex<HashSet<(u8, String)>>> = Arc::new(Mutex::new(HashSet::<(u8, String)>::new()));
let error: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
Expand Down Expand Up @@ -178,7 +187,7 @@ impl RustNotify {
Ok(watcher) => watcher,
Err(e) => return wf_error!($msg_template, e),
};
watcher_paths!(watcher, watch_paths, debug, recursive, strict_errors);
watcher_paths!(watcher, watch_paths, debug, recursive, ignore_permission_denied);
Ok(WatcherEnum::Poll(watcher))
}};
}
Expand All @@ -189,7 +198,7 @@ impl RustNotify {
match RecommendedWatcher::new(event_handler.clone(), NotifyConfig::default()) {
Ok(watcher) => {
let mut watcher = watcher;
watcher_paths!(watcher, watch_paths, debug, recursive, strict_errors);
watcher_paths!(watcher, watch_paths, debug, recursive, ignore_permission_denied);
Ok(WatcherEnum::Recommended(watcher))
}
Err(error) => {
Expand Down
4 changes: 2 additions & 2 deletions tests/test_force_polling.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_watch_polling_not_env(mocker):
for _ in watch('.'):
pass

m.assert_called_once_with(['.'], False, False, 300, True, True)
m.assert_called_once_with(['.'], False, False, 300, True, False)


def test_watch_polling_env(mocker, env: SetEnv):
Expand All @@ -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, True)
m.assert_called_once_with(['.'], False, True, 300, True, False)


@pytest.mark.parametrize(
Expand Down
58 changes: 29 additions & 29 deletions tests/test_rust_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@


def test_add(test_dir: Path):
watcher = RustNotify([str(test_dir)], True, False, 0, True, 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, True)
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')
Expand All @@ -26,7 +26,7 @@ def test_add_non_recursive(test_dir: Path):


def test_close(test_dir: Path):
watcher = RustNotify([str(test_dir)], True, False, 0, True, True)
watcher = RustNotify([str(test_dir)], True, False, 0, True, False)
assert repr(watcher).startswith('RustNotify(Recommended(\n')

watcher.close()
Expand All @@ -37,15 +37,15 @@ def test_close(test_dir: Path):


def test_modify_write(test_dir: Path):
watcher = RustNotify([str(test_dir)], True, False, 0, True, True)
watcher = RustNotify([str(test_dir)], True, False, 0, True, False)

(test_dir / 'a.txt').write_text('this is new')

assert watcher.watch(200, 50, 500, None) == {(2, str(test_dir / 'a.txt'))}


def test_modify_write_non_recursive(test_dir: Path):
watcher = RustNotify([str(test_dir)], True, False, 0, False, True)
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')
Expand All @@ -57,15 +57,15 @@ 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, True)
watcher = RustNotify([str(test_dir)], True, False, 0, True, False)

(test_dir / 'b.txt').chmod(0o444)

assert watcher.watch(200, 50, 500, None) == {(2, str(test_dir / 'b.txt'))}


def test_delete(test_dir: Path):
watcher = RustNotify([str(test_dir)], False, False, 0, True, True)
watcher = RustNotify([str(test_dir)], False, False, 0, True, False)

(test_dir / 'c.txt').unlink()

Expand All @@ -75,7 +75,7 @@ def test_delete(test_dir: Path):


def test_delete_non_recursive(test_dir: Path):
watcher = RustNotify([str(test_dir)], False, False, 0, False, True)
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()
Expand All @@ -93,7 +93,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, True)
watcher = RustNotify([str(dst)], False, False, 0, True, False)

for f in move_files:
(src / f).rename(dst / f)
Expand All @@ -110,7 +110,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, True)
watcher = RustNotify([str(src)], False, False, 0, True, False)

for f in move_files:
(src / f).rename(dst / f)
Expand All @@ -127,7 +127,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, True)
watcher = RustNotify([str(test_dir)], False, False, 0, True, False)

for f in move_files:
(src / f).rename(dst / f)
Expand All @@ -148,24 +148,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, 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, 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, True)
RustNotify([str(p)], False, True, 0, True, False)


def test_rename(test_dir: Path):
watcher = RustNotify([str(test_dir)], False, False, 0, True, True)
watcher = RustNotify([str(test_dir)], False, False, 0, True, False)

f = test_dir / 'a.txt'
f.rename(f.with_suffix('.new'))
Expand All @@ -181,7 +181,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, 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')
Expand All @@ -195,14 +195,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, 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, 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"):
Expand All @@ -211,7 +211,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, True)
watcher = RustNotify([str(test_dir)], False, False, 0, True, False)

with time_taken(40, 70):
assert watcher.watch(20, 1, 50, None) == 'timeout'
Expand All @@ -227,15 +227,15 @@ 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, 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'


@skip_unless_linux
def test_return_event_unset(test_dir: Path, time_taken):
watcher = RustNotify([str(test_dir)], False, False, 0, True, 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'
Expand All @@ -244,7 +244,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, True)
watcher = RustNotify([str(test_dir)], True, False, 0, True, False)
(test_dir / 'debounce.txt').write_text('foobar')

with time_taken(50, 130):
Expand All @@ -266,7 +266,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, True)
watcher_all = RustNotify([str(tmp_path)], False, False, 0, True, False)

f1.rename(d2 / '1.txt')
f2.rename(d2 / '2.txt')
Expand All @@ -284,26 +284,26 @@ 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, 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)
assert (1, str(test_dir / 'test_polling.txt')) in changes # sometimes has an event modify too


def test_not_polling_repr(test_dir: Path):
watcher = RustNotify([str(test_dir)], True, False, 123, True, 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, 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


def test_non_strict_error(tmp_path: Path):
p = tmp_path / 'missing'
RustNotify([str(p)], False, False, 0, True, False)
@skip_windows
def test_ignore_permission_denied():
RustNotify(['/'], False, False, 0, True, True)
2 changes: 1 addition & 1 deletion watchfiles/_rust_notify.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class RustNotify:
force_polling: bool,
poll_delay_ms: int,
recursive: bool,
strict_errors: bool,
ignore_permission_denied: bool,
) -> None:
"""
Create a new `RustNotify` instance and start a thread to watch for changes.
Expand Down
12 changes: 8 additions & 4 deletions watchfiles/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def watch(
force_polling: Optional[bool] = None,
poll_delay_ms: int = 300,
recursive: bool = True,
strict_errors: bool = True,
ignore_permission_denied: bool = False,
) -> Generator[Set[FileChange], None, None]:
"""
Watch one or more paths and yield a set of changes whenever files change.
Expand Down Expand Up @@ -112,7 +112,9 @@ def watch(
```
"""
force_polling = _default_force_polling(force_polling)
with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms, recursive, strict_errors) as watcher:
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':
Expand Down Expand Up @@ -148,7 +150,7 @@ async def awatch( # noqa C901
force_polling: Optional[bool] = None,
poll_delay_ms: int = 300,
recursive: bool = True,
strict_errors: bool = True,
ignore_permission_denied: bool = False,
) -> AsyncGenerator[Set[FileChange], None]:
"""
Asynchronous equivalent of [`watch`][watchfiles.watch] using threads to wait for changes.
Expand Down Expand Up @@ -232,7 +234,9 @@ 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, strict_errors) as watcher:
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()

Expand Down
Loading

0 comments on commit 38f27b6

Please sign in to comment.