Skip to content

Commit

Permalink
Support WATCHFILES_FORCE_POLLING env var (#170)
Browse files Browse the repository at this point in the history
* add support for WATCHFILES_FORCE_POLLING env var

* dump tests

* explicit test for _default_force_pulling

* ignore warning on pypy
  • Loading branch information
samuelcolvin authored Jul 20, 2022
1 parent 63e6983 commit 2c7ebb4
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 6 deletions.
6 changes: 6 additions & 0 deletions tests/test_rust_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ def test_polling(test_dir: Path):
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)
r = repr(watcher)
assert r.startswith('RustNotify(Recommended(\n')


def test_polling_repr(test_dir: Path):
watcher = RustNotify([str(test_dir)], True, True, 123)
r = repr(watcher)
Expand Down
52 changes: 50 additions & 2 deletions tests/test_watch.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sys
import threading
from contextlib import contextmanager
Expand All @@ -9,7 +10,7 @@
import pytest

from watchfiles import Change, awatch, watch
from watchfiles.main import _calc_async_timeout
from watchfiles.main import _calc_async_timeout, _default_force_pulling

if TYPE_CHECKING:
from conftest import MockRustType
Expand Down Expand Up @@ -43,6 +44,7 @@ async def test_awatch(tmp_path: Path, write_soon):
break


@pytest.mark.filterwarnings('ignore::DeprecationWarning')
async def test_await_stop_event(tmp_path: Path, write_soon):
sleep(0.05)
write_soon(tmp_path / 'foo.txt')
Expand Down Expand Up @@ -211,7 +213,7 @@ def close(self):
pass


async def test_awatch_interrupt_raise(mocker, caplog):
async def test_awatch_interrupt_raise(mocker):
mocker.patch('watchfiles.main.RustNotify', return_value=MockRustNotifyRaise())

count = 0
Expand All @@ -223,3 +225,49 @@ async def test_awatch_interrupt_raise(mocker, caplog):
# event is set because it's set while handling the KeyboardInterrupt
assert stop_event.is_set()
assert count == 1


class MockRustNotify:
def watch(self, *args):
return 'stop'

def __enter__(self):
return self

def __exit__(self, *args):
pass


def test_watch_polling_not_env(mocker):
m = mocker.patch('watchfiles.main.RustNotify', return_value=MockRustNotify())

for _ in watch('.'):
pass

m.assert_called_once_with(['.'], False, False, 30)


def test_watch_polling_env(mocker):
os.environ['WATCHFILES_FORCE_POLLING'] = '1'
try:
m = mocker.patch('watchfiles.main.RustNotify', return_value=MockRustNotify())

for _ in watch('.'):
pass

m.assert_called_once_with(['.'], False, True, 30)
finally:
del os.environ['WATCHFILES_FORCE_POLLING']


def test_default_force_pulling():
try:
assert _default_force_pulling(True) is True
assert _default_force_pulling(False) is False
assert _default_force_pulling(None) is False
os.environ['WATCHFILES_FORCE_POLLING'] = '1'
assert _default_force_pulling(True) is True
assert _default_force_pulling(False) is False
assert _default_force_pulling(None) is True
finally:
del os.environ['WATCHFILES_FORCE_POLLING']
23 changes: 19 additions & 4 deletions watchfiles/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import os
import sys
import warnings
from enum import IntEnum
Expand Down Expand Up @@ -64,7 +65,7 @@ def watch(
yield_on_timeout: bool = False,
debug: bool = False,
raise_interrupt: bool = True,
force_polling: bool = False,
force_polling: Optional[bool] = None,
poll_delay_ms: int = 30,
) -> Generator[Set[FileChange], None, None]:
"""
Expand All @@ -87,7 +88,8 @@ def watch(
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: if true, always use polling instead of file system notifications.
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`.
Yields:
Expand All @@ -100,6 +102,7 @@ def watch(
print(changes)
```
"""
force_polling = _default_force_pulling(force_polling)
with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms) as watcher:
while True:
raw_changes = watcher.watch(debounce, step, rust_timeout, stop_event)
Expand Down Expand Up @@ -133,7 +136,7 @@ async def awatch( # noqa C901
yield_on_timeout: bool = False,
debug: bool = False,
raise_interrupt: Optional[bool] = None,
force_polling: bool = False,
force_polling: Optional[bool] = None,
poll_delay_ms: int = 30,
) -> AsyncGenerator[Set[FileChange], None]:
"""
Expand All @@ -159,7 +162,8 @@ async def awatch( # noqa C901
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.
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`.
Yields:
Expand Down Expand Up @@ -214,6 +218,7 @@ async def stop_soon():
else:
stop_event_ = stop_event

force_polling = _default_force_pulling(force_polling)
with RustNotify([str(p) for p in paths], debug, force_polling, poll_delay_ms) as watcher:
timeout = _calc_async_timeout(rust_timeout)
CancelledError = anyio.get_cancelled_exc_class()
Expand Down Expand Up @@ -276,3 +281,13 @@ def _calc_async_timeout(timeout: Optional[int]) -> int:
return 5_000
else:
return timeout


def _default_force_pulling(force_polling: Optional[bool]) -> bool:
"""
https://github.com/samuelcolvin/watchfiles/issues/167#issuecomment-1189309354 for rationale.
"""
if force_polling is None:
return 'WATCHFILES_FORCE_POLLING' in os.environ
else:
return force_polling

0 comments on commit 2c7ebb4

Please sign in to comment.