Skip to content

Commit

Permalink
add grace period during which file changes are ignored (samuelcolvin#243
Browse files Browse the repository at this point in the history
)

* add grace period during which file changes are ignored

* fix cli option

* fix tests
  • Loading branch information
samuelcolvin authored Aug 24, 2023
1 parent 79dd647 commit ea789d3
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ build-dev:

.PHONY: format
format:
$(ruff) --fix
$(ruff) --fix-only
$(isort)
$(black)
@echo 'max_width = 120' > .rustfmt.toml
Expand Down
3 changes: 3 additions & 0 deletions docs/cli_help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ usage: watchfiles [-h] [--ignore-paths [IGNORE_PATHS]]
[--filter [FILTER]] [--args [ARGS]] [--verbose]
[--non-recursive] [--verbosity [{warning,info,debug}]]
[--sigint-timeout [SIGINT_TIMEOUT]]
[--grace-period [GRACE_PERIOD]]
[--sigkill-timeout [SIGKILL_TIMEOUT]] [--version]
target [paths ...]

Expand Down Expand Up @@ -36,6 +37,8 @@ options:
Log level, defaults to "info"
--sigint-timeout [SIGINT_TIMEOUT]
How long to wait for the sigint timeout before sending sigkill.
--grace-period [GRACE_PERIOD]
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.
--version, -V show program's version number and exit
11 changes: 11 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def test_function(mocker, tmp_path):
target_type='function',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand Down Expand Up @@ -49,6 +50,7 @@ def test_ignore_paths(mocker, tmp_work_path):
& HasAttributes(extensions=('.py', '.pyx', '.pyd'), _ignore_paths=('/foo/bar', '/apple/banana'))
),
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand Down Expand Up @@ -102,6 +104,7 @@ def test_command(mocker, tmp_work_path):
target_type='command',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand All @@ -119,6 +122,7 @@ def test_verbosity(mocker, tmp_path):
target_type='function',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=True,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand All @@ -136,6 +140,7 @@ def test_verbose(mocker, tmp_path):
target_type='function',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=True,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand All @@ -153,6 +158,7 @@ def test_non_recursive(mocker, tmp_path):
target_type='function',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=False,
Expand All @@ -170,6 +176,7 @@ def test_filter_all(mocker, tmp_path, capsys):
target_type='function',
watch_filter=None,
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand All @@ -190,6 +197,7 @@ def test_filter_default(mocker, tmp_path):
target_type='function',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand All @@ -207,6 +215,7 @@ def test_set_type(mocker, tmp_path):
target_type='command',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand Down Expand Up @@ -262,6 +271,7 @@ def test_args(mocker, tmp_path, reset_argv, caplog):
target_type='function',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand All @@ -283,6 +293,7 @@ def test_args_command(mocker, tmp_path, caplog):
target_type='command',
watch_filter=IsInstance(DefaultFilter, only_direct_instance=True),
debug=False,
grace_period=0,
sigint_timeout=5,
sigkill_timeout=1,
recursive=True,
Expand Down
17 changes: 11 additions & 6 deletions tests/test_run_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ def test_alive_terminates(mocker, mock_rust_notify: 'MockRustType', caplog):
mock_kill = mocker.patch('watchfiles.run.os.kill')
mock_rust_notify([{(1, '/path/to/foobar.py')}])

assert run_process('/x/y/z', target=os.getcwd, debounce=5, step=1) == 1
assert run_process('/x/y/z', target=os.getcwd, debounce=5, grace_period=0.01, step=1) == 1
assert mock_spawn_process.call_count == 2
assert mock_popen.call_count == 0
assert mock_kill.call_count == 2 # kill in loop + final kill
assert 'watchfiles.main DEBUG: running "<built-in function getcwd>" as function\n' in caplog.text
assert 'sleeping for 0.01 seconds before watching for changes' in caplog.text


def test_dead_callback(mocker, mock_rust_notify: 'MockRustType'):
Expand Down Expand Up @@ -180,12 +181,16 @@ async def test_async_sync_callback(mocker, mock_rust_notify: 'MockRustType'):

callback_calls = []

assert (
await arun_process(
'/x/y/async', target='os.getcwd', target_type='function', callback=callback_calls.append, debounce=5, step=1
)
== 2
v = await arun_process(
'/x/y/async',
target='os.getcwd',
target_type='function',
callback=callback_calls.append,
grace_period=0.01,
debounce=5,
step=1,
)
assert v == 2
assert mock_spawn_process.call_count == 3
assert mock_kill.call_count == 3
assert callback_calls == [{(Change.added, '/path/to/foo.py')}, {(Change.modified, '/path/to/bar.py')}]
Expand Down
8 changes: 8 additions & 0 deletions watchfiles/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ def cli(*args_: str) -> None:
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='?',
Expand Down Expand Up @@ -165,6 +172,7 @@ def cli(*args_: str) -> None:
sigint_timeout=arg_namespace.sigint_timeout,
sigkill_timeout=arg_namespace.sigkill_timeout,
recursive=not arg_namespace.non_recursive,
grace_period=arg_namespace.grace_period,
)


Expand Down
12 changes: 12 additions & 0 deletions watchfiles/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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
Expand All @@ -36,6 +37,7 @@ def run_process(
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,
Expand Down Expand Up @@ -68,6 +70,7 @@ def run_process(
[`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]
Expand Down Expand Up @@ -128,6 +131,10 @@ def foobar(a, b, c):
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,
Expand Down Expand Up @@ -155,6 +162,7 @@ async def arun_process(
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,
Expand Down Expand Up @@ -199,6 +207,10 @@ async def main():
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
):
Expand Down

0 comments on commit ea789d3

Please sign in to comment.