Skip to content

Commit 31fb1d3

Browse files
adamchainzscuml
andauthored
Correct glob matching for ** patterns (#166)
Fixes #91. Supercedes #92 and #134. --------- Co-authored-by: Stephen Mitchell <scum@mac.com>
1 parent 535a7f6 commit 31fb1d3

File tree

3 files changed

+37
-5
lines changed

3 files changed

+37
-5
lines changed

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ Unreleased
77

88
* Support Python 3.14.
99

10+
* Correct glob matching to be equivalent to Django’s ``StatReloader`` for ``**`` patterns.
11+
This fix is limited to Python 3.13+ because it depends the new |Path.full_match()|__ method.
12+
13+
.. |Path.full_match()| replace:: ``Path.full_match()``
14+
__: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.full_match
15+
16+
`PR #166 <https://github.com/adamchainz/django-watchfiles/pull/166>`__.
17+
Thanks to Evgeny Arshinov for the report in `Issue #91 <https://github.com/adamchainz/django-watchfiles/issues/91>`__, and Stephen Mitchell for an initial pull request in `PR #134 <https://github.com/adamchainz/django-watchfiles/pull/134>`__.
18+
1019
1.1.0 (2025-02-06)
1120
------------------
1221

src/django_watchfiles/__init__.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
from __future__ import annotations
22

3+
import sys
34
import threading
45
from collections.abc import Generator, Iterable
5-
from fnmatch import fnmatch
66
from pathlib import Path
77
from typing import Callable
88

99
from django.utils import autoreload
1010
from watchfiles import Change, watch
1111

12+
if sys.version_info >= (3, 13):
13+
path_full_match = Path.full_match
14+
else:
15+
# True backport turned out to be too hard. Instead, fall back to the
16+
# pre-existing incorrect fnmatch implementation
17+
18+
from fnmatch import fnmatch
19+
20+
def path_full_match(path: Path, pattern: str) -> bool:
21+
return fnmatch(str(path), pattern)
22+
1223

1324
class MutableWatcher:
1425
"""
@@ -67,10 +78,8 @@ def file_filter(self, change: Change, filename: str) -> bool:
6778
except ValueError:
6879
pass
6980
else:
70-
relative_path_str = str(relative_path)
71-
for glob in globs:
72-
if fnmatch(relative_path_str, glob):
73-
return True
81+
if any(path_full_match(relative_path, glob) for glob in globs):
82+
return True
7483
return False
7584

7685
def watched_roots(self, watched_files: Iterable[Path]) -> frozenset[Path]:

tests/test_django_watchfiles.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import sys
34
import tempfile
45
import time
56
from pathlib import Path
@@ -119,6 +120,19 @@ def test_file_filter_glob_matched(self):
119120

120121
assert result is True
121122

123+
@pytest.mark.skipif(
124+
sys.version_info < (3, 13),
125+
reason="Path.full_match not available before Python 3.13",
126+
)
127+
def test_file_filter_recursive_glob_matched(self):
128+
self.reloader.watch_dir(self.temp_path, "**/*.txt")
129+
130+
result = self.reloader.file_filter(
131+
Change.modified, str(self.temp_path / "test.txt")
132+
)
133+
134+
assert result is True
135+
122136
def test_file_filter_glob_multiple_globs_unmatched(self):
123137
self.reloader.watch_dir(self.temp_path, "*.css")
124138
self.reloader.watch_dir(self.temp_path, "*.html")

0 commit comments

Comments
 (0)