diff --git a/README.rst b/README.rst index 41342bef..266b0119 100644 --- a/README.rst +++ b/README.rst @@ -29,8 +29,15 @@ To run a function and restart it when code changes: run_process('./path/to/dir', foobar, process_args=(1, 2, 3)) -(``run_process`` uses ``PythonWatcher`` so only changes to python files will prompt a -reload, see *custom watchers* below) +``run_process`` uses ``PythonWatcher`` so only changes to python files will prompt a +reload, see *custom watchers* below. + +If you need notifications about change events as well as to restart a process you can +use the ``callback`` argument to pass a function will will be called on every file change +with one argument: the set of file changes. + +Asynchronous Methods +.................... *watchgod* comes with an asynchronous equivalents of ``watch``: ``awatch`` which uses a ``ThreadPoolExecutor`` to iterate over files. @@ -65,11 +72,14 @@ uses ``awatch``: loop = asyncio.get_event_loop() loop.run_until_complete(main()) -(``arun_process`` uses ``PythonWatcher`` so only changes to python files will prompt a -reload, see *custom watchers* below) +``arun_process`` uses ``PythonWatcher`` so only changes to python files will prompt a +reload, see *custom watchers* below. + +The signature of ``arun_process`` is almost identical to ``run_process`` except that +the ``callback`` argument if provide must be a coroutine, not a function. -Custom watchers ---------------- +Custom Watchers +............... *watchgod* comes with the following watcher classes which can be used via the ``watcher_cls`` keyword argument to any of the methods above. diff --git a/tests/test_run_process.py b/tests/test_run_process.py index 14afcb7e..465b6380 100644 --- a/tests/test_run_process.py +++ b/tests/test_run_process.py @@ -1,3 +1,5 @@ +from asyncio import Future + from watchgod import arun_process, run_process from watchgod.main import _start_process @@ -42,14 +44,17 @@ def test_alive_terminates(mocker): assert mock_kill.call_count == 1 -def test_dead(mocker): +def test_dead_callback(mocker): mock_start_process = mocker.patch('watchgod.main._start_process') mock_start_process.return_value = FakeProcess(is_alive=False) mock_kill = mocker.patch('watchgod.main.os.kill') + c = mocker.MagicMock() - assert run_process('/x/y/z', object(), watcher_cls=FakeWatcher, debounce=5, min_sleep=1) == 1 + assert run_process('/x/y/z', object(), watcher_cls=FakeWatcher, callback=c, debounce=5, min_sleep=1) == 1 assert mock_start_process.call_count == 2 assert mock_kill.call_count == 0 + assert c.call_count == 1 + c.assert_called_with({'x'}) def test_alive_doesnt_terminate(mocker): @@ -74,8 +79,13 @@ async def test_async_alive_terminates(mocker): mock_start_process = mocker.patch('watchgod.main._start_process') mock_start_process.return_value = FakeProcess() mock_kill = mocker.patch('watchgod.main.os.kill') + f = Future() + f.set_result(1) + c = mocker.MagicMock(return_value=f) - reloads = await arun_process('/x/y/async', object(), watcher_cls=FakeWatcher, debounce=5, min_sleep=1) + reloads = await arun_process('/x/y/async', object(), watcher_cls=FakeWatcher, callback=c, debounce=5, min_sleep=1) assert reloads == 1 assert mock_start_process.call_count == 2 assert mock_kill.call_count == 1 + assert c.call_count == 1 + c.assert_called_with({'x'}) diff --git a/watchgod/main.py b/watchgod/main.py index d93e2e41..1a06b1f9 100644 --- a/watchgod/main.py +++ b/watchgod/main.py @@ -7,9 +7,9 @@ from multiprocessing import Process from pathlib import Path from time import sleep, time -from typing import Any, Callable, Dict, Tuple, Type, Union +from typing import Any, Awaitable, Callable, Dict, Set, Tuple, Type, Union -from .watcher import AllWatcher, DefaultWatcher, PythonWatcher +from .watcher import AllWatcher, Change, DefaultWatcher, PythonWatcher __all__ = 'watch', 'awatch', 'run_process', 'arun_process' logger = logging.getLogger('watchgod.main') @@ -116,6 +116,7 @@ def _stop_process(process): def run_process(path: Union[Path, str], target: Callable, *, args: Tuple[Any]=(), kwargs: Dict[str, Any]=None, + callback: Callable[[Set[Tuple[Change, str]]], None]=None, watcher_cls: Type[AllWatcher]=PythonWatcher, debounce=400, min_sleep=100): @@ -126,7 +127,8 @@ def run_process(path: Union[Path, str], target: Callable, *, process = _start_process(target=target, args=args, kwargs=kwargs) reloads = 0 - for _ in watch(path, watcher_cls=watcher_cls, debounce=debounce, min_sleep=min_sleep): + for changes in watch(path, watcher_cls=watcher_cls, debounce=debounce, min_sleep=min_sleep): + callback and callback(changes) _stop_process(process) process = _start_process(target=target, args=args, kwargs=kwargs) reloads += 1 @@ -136,6 +138,7 @@ def run_process(path: Union[Path, str], target: Callable, *, async def arun_process(path: Union[Path, str], target: Callable, *, args: Tuple[Any]=(), kwargs: Dict[str, Any]=None, + callback: Callable[[Set[Tuple[Change, str]]], Awaitable]=None, watcher_cls: Type[AllWatcher]=PythonWatcher, debounce=400, min_sleep=100): @@ -148,7 +151,8 @@ async def arun_process(path: Union[Path, str], target: Callable, *, process = await loop.run_in_executor(None, start_process) reloads = 0 - async for _ in awatch(path, watcher_cls=watcher_cls, debounce=debounce, min_sleep=min_sleep): # noqa: F841 + async for changes in awatch(path, watcher_cls=watcher_cls, debounce=debounce, min_sleep=min_sleep): # noqa: F841 + callback and await callback(changes) await loop.run_in_executor(None, _stop_process, process) process = await loop.run_in_executor(None, start_process) reloads += 1