Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions docs/cli_help.txt
Original file line number Diff line number Diff line change
@@ -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}]]
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 23 additions & 3 deletions watchfiles/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='?',
Expand Down Expand Up @@ -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...',
Expand Down Expand Up @@ -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
Copy link
Author

@oliverlambson oliverlambson Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a default to reduce blast radius of change, but i think no default would be more in-line with the rest of the repo. lmk if you would prefer i remove it and add the arg to all calls to build_filter

) -> 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)
Expand Down
Loading