Skip to content
Draft
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
10 changes: 4 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@ jobs:
os: [ubuntu, macos, windows]
rust-version: [stable, "1.77"]
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.13t"
- "3.14"
- "3.14t"
- "pypy3.9"
- "pypy3.10"
- "pypy3.11"
exclude:
Expand Down Expand Up @@ -158,11 +156,11 @@ jobs:
ls: dir
target: i686
python-architecture: x86
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14
interpreter: 3.10 3.11 3.12 3.13 3.14
- os: windows
ls: dir
target: x86_64
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14
interpreter: 3.10 3.11 3.12 3.13 3.14
- os: windows
ls: dir
target: aarch64
Expand Down Expand Up @@ -221,15 +219,15 @@ jobs:
with:
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
args: --release --out dist --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 3.13t 3.14 3.14t' }}
args: --release --out dist --interpreter ${{ matrix.interpreter || '3.10 3.11 3.12 3.13 3.13t 3.14 3.14t' }}

- name: build pypy wheels
if: ${{ matrix.pypy }}
uses: PyO3/maturin-action@v1.49.3
with:
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
args: --release --out dist --interpreter pypy3.9 pypy3.10 pypy3.11
args: --release --out dist --interpreter pypy3.10 pypy3.11

- run: ${{ matrix.ls || 'ls -lh' }} dist/

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ see [the migration guide](https://watchfiles.helpmanual.io/migrating/) for more

## Installation

**watchfiles** requires Python 3.9 - 3.14.
**watchfiles** requires Python 3.10 - 3.14.

```bash
pip install watchfiles
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ See [`arun_process` docs][watchfiles.arun_process] for more details.

## Installation

**watchfiles** requires **Python 3.9** to **Python 3.14**.
**watchfiles** requires **Python 3.10** to **Python 3.14**.

### From PyPI

Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "watchfiles"
requires-python = ">=3.9"
requires-python = ">=3.10"
description = "Simple, modern and high performance file watching and code reload in python."
authors = [{ name = "Samuel Colvin", email = "s@muelcolvin.com" }]
dependencies = ["anyio>=3.0.0"]
Expand All @@ -14,7 +14,6 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -89,7 +88,7 @@ omit = ["*/__main__.py"]

[tool.ruff]
line-length = 120
target-version = "py38"
target-version = "py310"
lint.mccabe = { max-complexity = 14 }
lint.extend-select = ["Q", "RUF100", "C90", "UP", "I"]
lint.flake8-quotes = { inline-quotes = "single", multiline-quotes = "double" }
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path
from threading import Thread
from time import sleep, time
from typing import TYPE_CHECKING, Any, List, Set, Tuple
from typing import TYPE_CHECKING, Any

import pytest

Expand Down Expand Up @@ -61,7 +61,7 @@ def start(path: Path):
t.join()


ChangesType = List[Set[Tuple[int, str]]]
ChangesType = list[set[tuple[int, str]]]


class MockRustNotify:
Expand Down
1,410 changes: 750 additions & 660 deletions uv.lock

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions watchfiles/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import os
import shlex
import sys
from collections.abc import Callable
from pathlib import Path
from textwrap import dedent
from typing import Any, Callable, List, Optional, Tuple, Union, cast
from typing import Any, cast

from . import Change
from .filters import BaseFilter, DefaultFilter, PythonFilter
Expand Down Expand Up @@ -195,9 +196,9 @@ def import_exit(function_path: str) -> Any:


def build_filter(
filter_name: str, ignore_paths_str: Optional[str]
) -> Tuple[Union[None, DefaultFilter, Callable[[Change, str], bool]], str]:
ignore_paths: List[Path] = []
filter_name: str, ignore_paths_str: str | None
) -> tuple[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(',')]

Expand Down
13 changes: 7 additions & 6 deletions watchfiles/filters.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import logging
import os
import re
from collections.abc import Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Sequence, Union
from typing import TYPE_CHECKING

__all__ = 'BaseFilter', 'DefaultFilter', 'PythonFilter'
logger = logging.getLogger('watchfiles.watcher')
Expand Down Expand Up @@ -30,7 +31,7 @@ class BaseFilter:
"entity" here refers to the specific file or directory - basically the result of `path.split(os.sep)[-1]`,
an obvious example would be `r'\\.py[cod]$'`.
"""
ignore_paths: Sequence[Union[str, Path]] = ()
ignore_paths: Sequence[str | Path] = ()
"""
Full paths to ignore, e.g. `/home/users/.cache` or `C:\\Users\\user\\.cache`.
"""
Expand Down Expand Up @@ -101,9 +102,9 @@ class DefaultFilter(BaseFilter):
def __init__(
self,
*,
ignore_dirs: Optional[Sequence[str]] = None,
ignore_entity_patterns: Optional[Sequence[str]] = None,
ignore_paths: Optional[Sequence[Union[str, Path]]] = None,
ignore_dirs: Sequence[str] | None = None,
ignore_entity_patterns: Sequence[str] | None = None,
ignore_paths: Sequence[str | Path] | None = None,
) -> None:
"""
Args:
Expand Down Expand Up @@ -131,7 +132,7 @@ class PythonFilter(DefaultFilter):
def __init__(
self,
*,
ignore_paths: Optional[Sequence[Union[str, Path]]] = None,
ignore_paths: Sequence[str | Path] | None = None,
extra_extensions: Sequence[str] = (),
) -> None:
"""
Expand Down
49 changes: 25 additions & 24 deletions watchfiles/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import os
import sys
import warnings
from collections.abc import AsyncGenerator, Callable, Generator
from enum import IntEnum
from pathlib import Path
from typing import TYPE_CHECKING, AsyncGenerator, Callable, Generator, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, Optional

import anyio

Expand All @@ -31,7 +32,7 @@ def raw_str(self) -> str:
return self.name


FileChange = Tuple[Change, str]
FileChange = tuple[Change, str]
"""
A tuple representing a file change, first element is a [`Change`][watchfiles.Change] member, second is the path
of the file or directory that changed.
Expand All @@ -43,27 +44,27 @@ def raw_str(self) -> str:

import trio

AnyEvent = Union[anyio.Event, asyncio.Event, trio.Event]
AnyEvent = anyio.Event | asyncio.Event | trio.Event

class AbstractEvent(Protocol):
def is_set(self) -> bool: ...


def watch(
*paths: Union[Path, str],
watch_filter: Optional[Callable[['Change', str], bool]] = DefaultFilter(),
*paths: Path | str,
watch_filter: Callable[['Change', str], bool] | None = DefaultFilter(),
debounce: int = 1_600,
step: int = 50,
stop_event: Optional['AbstractEvent'] = None,
rust_timeout: int = 5_000,
yield_on_timeout: bool = False,
debug: Optional[bool] = None,
debug: bool | None = None,
raise_interrupt: bool = True,
force_polling: Optional[bool] = None,
force_polling: bool | None = None,
poll_delay_ms: int = 300,
recursive: bool = True,
ignore_permission_denied: Optional[bool] = None,
) -> Generator[Set[FileChange], None, None]:
ignore_permission_denied: bool | None = None,
) -> Generator[set[FileChange], None, None]:
"""
Watch one or more paths and yield a set of changes whenever files change.

Expand Down Expand Up @@ -151,20 +152,20 @@ def watch(


async def awatch( # C901
*paths: Union[Path, str],
watch_filter: Optional[Callable[[Change, str], bool]] = DefaultFilter(),
*paths: Path | str,
watch_filter: Callable[[Change, str], bool] | None = DefaultFilter(),
debounce: int = 1_600,
step: int = 50,
stop_event: Optional['AnyEvent'] = None,
rust_timeout: Optional[int] = None,
rust_timeout: int | None = None,
yield_on_timeout: bool = False,
debug: Optional[bool] = None,
raise_interrupt: Optional[bool] = None,
force_polling: Optional[bool] = None,
debug: bool | None = None,
raise_interrupt: bool | None = None,
force_polling: bool | None = None,
poll_delay_ms: int = 300,
recursive: bool = True,
ignore_permission_denied: Optional[bool] = None,
) -> AsyncGenerator[Set[FileChange], None]:
ignore_permission_denied: bool | None = None,
) -> AsyncGenerator[set[FileChange], None]:
"""
Asynchronous equivalent of [`watch`][watchfiles.watch] using threads to wait for changes.
Arguments match those of [`watch`][watchfiles.watch] except `stop_event`.
Expand Down Expand Up @@ -289,16 +290,16 @@ async def stop_soon():


def _prep_changes(
raw_changes: Set[Tuple[int, str]], watch_filter: Optional[Callable[[Change, str], bool]]
) -> Set[FileChange]:
raw_changes: set[tuple[int, str]], watch_filter: Callable[[Change, str], bool] | None
) -> set[FileChange]:
# if we wanted to be really snazzy, we could move this into rust
changes = {(Change(change), path) for change, path in raw_changes}
if watch_filter:
changes = {c for c in changes if watch_filter(c[0], c[1])}
return changes


def _log_changes(changes: Set[FileChange]) -> None:
def _log_changes(changes: set[FileChange]) -> None:
if logger.isEnabledFor(logging.INFO): # pragma: no branch
count = len(changes)
plural = '' if count == 1 else 's'
Expand All @@ -308,7 +309,7 @@ def _log_changes(changes: Set[FileChange]) -> None:
logger.info('%d change%s detected', count, plural)


def _calc_async_timeout(timeout: Optional[int]) -> int:
def _calc_async_timeout(timeout: int | None) -> int:
"""
see https://github.com/samuelcolvin/watchfiles/issues/110
"""
Expand All @@ -321,7 +322,7 @@ def _calc_async_timeout(timeout: Optional[int]) -> int:
return timeout


def _default_force_polling(force_polling: Optional[bool]) -> bool:
def _default_force_polling(force_polling: bool | None) -> bool:
"""
See docstring for `watch` above for details.

Expand All @@ -347,7 +348,7 @@ def _default_poll_delay_ms(poll_delay_ms: int) -> int:
return poll_delay_ms


def _default_debug(debug: Optional[bool]) -> bool:
def _default_debug(debug: bool | None) -> bool:
if debug is not None:
return debug
env_var = os.getenv('WATCHFILES_DEBUG')
Expand All @@ -366,7 +367,7 @@ def _auto_force_polling() -> bool:
return 'microsoft-standard' in uname.release.lower() and uname.system.lower() == 'linux'


def _default_ignore_permission_denied(ignore_permission_denied: Optional[bool]) -> bool:
def _default_ignore_permission_denied(ignore_permission_denied: bool | None) -> bool:
if ignore_permission_denied is not None:
return ignore_permission_denied
env_var = os.getenv('WATCHFILES_IGNORE_PERMISSION_DENIED')
Expand Down
Loading
Loading