From 0078760e8508b9cdcdf49ad5c07d15a2f6101a0d Mon Sep 17 00:00:00 2001 From: Oliver Lambson Date: Mon, 20 Oct 2025 13:02:59 -0400 Subject: [PATCH] add cli --extensions for python filter --- docs/cli.md | 7 +++++++ docs/cli_help.txt | 3 +++ tests/test_cli.py | 29 +++++++++++++++++++++++++++++ watchfiles/cli.py | 26 +++++++++++++++++++++++--- 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 6d82db5c..8cde4caf 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -56,6 +56,13 @@ files: watchfiles --filter python 'pytest --lf' src tests ``` +If you want to watch additional file extensions beyond the default Python files (`.py`, `.pyx`, `.pyd`), you can use +the `--extensions` flag: + +```bash title="Watching Python files and templates" +watchfiles --filter python --extensions '.html,.jinja' 'pytest --lf' src tests +``` + ## Help Run `watchfiles --help` for more options. diff --git a/docs/cli_help.txt b/docs/cli_help.txt index 2568d462..d6fa6f24 100644 --- a/docs/cli_help.txt +++ b/docs/cli_help.txt @@ -1,4 +1,5 @@ usage: watchfiles [-h] [--ignore-paths [IGNORE_PATHS]] + [--extensions [EXTENSIONS]] [--target-type [{command,function,auto}]] [--filter [FILTER]] [--args [ARGS]] [--verbose] [--non-recursive] [--verbosity [{warning,info,debug}]] @@ -28,6 +29,8 @@ options: -h, --help show this help message and exit --ignore-paths [IGNORE_PATHS] Specify directories to ignore, to ignore multiple paths use a comma as separator, e.g. "env" or "env,node_modules" + --extensions [EXTENSIONS] + Specify extra extensions to watch, to specify multiple extensions use a comma as separator, e.g. ".jinja" or ".html,.jinja" --target-type [{command,function,auto}] Whether the target should be intercepted as a shell command or a python function, defaults to "auto" which infers the target type from the target string --filter [FILTER] Which files to watch, defaults to "default" which uses the "DefaultFilter", "python" uses the "PythonFilter", "all" uses no filter, any other value is interpreted as a python function/class path which is imported diff --git a/tests/test_cli.py b/tests/test_cli.py index d22571a7..33a33c31 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -59,6 +59,35 @@ def test_ignore_paths(mocker, tmp_work_path): ) +def test_extensions(mocker, tmp_work_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( + '--extensions', + '.html,.jinja', + '--filter', + 'python', + 'os.getcwd', + '.', + ) + mock_run_process.assert_called_once_with( + Path(str(tmp_work_path)), + target='os.getcwd', + target_type='function', + watch_filter=( + IsInstance(PythonFilter) + & HasAttributes(extensions=('.py', '.pyx', '.pyd', '.html', '.jinja'), _ignore_paths=()) + ), + debug=False, + grace_period=0, + sigint_timeout=5, + sigkill_timeout=1, + recursive=True, + ignore_permission_denied=False, + ) + + class SysError(RuntimeError): pass diff --git a/watchfiles/cli.py b/watchfiles/cli.py index f1e1ddd5..4459e49d 100644 --- a/watchfiles/cli.py +++ b/watchfiles/cli.py @@ -57,6 +57,15 @@ def cli(*args_: str) -> None: 'to ignore multiple paths use a comma as separator, e.g. "env" or "env,node_modules"' ), ) + parser.add_argument( + '--extensions', + nargs='?', + type=str, + help=( + 'Specify extra extensions to watch, ' + 'to specify multiple extensions use a comma as separator, e.g. ".jinja" or ".html,.jinja"' + ), + ) parser.add_argument( '--target-type', nargs='?', @@ -157,7 +166,11 @@ def cli(*args_: str) -> None: print(f'path "{e}" does not exist', file=sys.stderr) sys.exit(1) - watch_filter, watch_filter_str = build_filter(arg_namespace.filter, arg_namespace.ignore_paths) + watch_filter, watch_filter_str = build_filter( + arg_namespace.filter, + arg_namespace.ignore_paths, + arg_namespace.extensions, + ) logger.info( 'watchfiles v%s 👀 path=%s target="%s" (%s) filter=%s...', @@ -195,19 +208,26 @@ def import_exit(function_path: str) -> Any: def build_filter( - filter_name: str, ignore_paths_str: Optional[str] + filter_name: str, ignore_paths_str: Optional[str], extensions_str: Optional[str] = None ) -> Tuple[Union[None, DefaultFilter, Callable[[Change, str], bool]], str]: ignore_paths: List[Path] = [] if ignore_paths_str: ignore_paths = [Path(p).resolve() for p in ignore_paths_str.split(',')] + extensions: List[str] = [] + if extensions_str: + extensions = [p for p in extensions_str.split(',')] if filter_name == 'default': + if extensions: + logger.warning('"--extensions" argument ignored as "all" filter was selected') return DefaultFilter(ignore_paths=ignore_paths), 'DefaultFilter' elif filter_name == 'python': - return PythonFilter(ignore_paths=ignore_paths), 'PythonFilter' + return PythonFilter(ignore_paths=ignore_paths, extra_extensions=extensions), 'PythonFilter' elif filter_name == 'all': if ignore_paths: logger.warning('"--ignore-paths" argument ignored as "all" filter was selected') + if extensions: + logger.warning('"--extensions" argument ignored as "all" filter was selected') return None, '(no filter)' watch_filter_cls = import_exit(filter_name)