Skip to content

Commit 3fe24c5

Browse files
committed
Fix handling of case-sensitivity for fake posixpath
- relates to hash() (since Python 3.13), match() and full_match() (since Python 3.14)
1 parent 57e5a40 commit 3fe24c5

3 files changed

Lines changed: 73 additions & 3 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ The released versions correspond to PyPI releases.
1010
(see [#1300](https://github.com/pytest-dev/pyfakefs/issues/1300))
1111
* fake file `seek` method did not return the location in the file
1212
(see [#1304](https://github.com/pytest-dev/pyfakefs/issues/1304))
13+
* make sure case sensitivity is correctly set for fake posix paths
14+
in `hash()`, `Path.match` and `Path.full_match`
15+
(see [#1308](https://github.com/pytest-dev/pyfakefs/issues/1308))
1316

1417
## [Version 6.1.6](https://pypi.python.org/pypi/pyfakefs/6.1.6) (2026-03-18)
1518
Follow-up bugfix release for 6.1.5.

pyfakefs/fake_pathlib.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,7 @@ def _normalize_path_sep(cls, path):
798798
)
799799
except AttributeError:
800800
return path
801+
801802
else:
802803

803804
@classmethod
@@ -1037,10 +1038,18 @@ def group(self):
10371038

10381039
return grp.getgrgid(self.stat().st_gid).gr_name
10391040

1041+
if sys.version_info >= (3, 13):
1042+
1043+
def __init__(self, *args):
1044+
super().__init__(*args)
1045+
# case-sensitivity for filling the _str_normcase_cached variable
1046+
# is filled depending on a class check (self.parser is posixpath),
1047+
# which we cannot fake, thus we set it at initialization time
1048+
self._str_normcase_cached = str(self)
1049+
10401050
if sys.version_info >= (3, 14):
1041-
# in Python 3.14, case-sensitivity is checked using an is-check
1042-
# (self.parser is posixpath) if not given, which we cannot fake
1043-
# therefore we already provide the case sensitivity under Posix
1051+
# a similar check is used in the implementations of glob, match and full_match, to check for
1052+
# case sensitivity if not given, so we provide it under Posix as True
10441053
def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
10451054
if case_sensitive is None:
10461055
case_sensitive = True
@@ -1050,6 +1059,20 @@ def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
10501059
recurse_symlinks=recurse_symlinks,
10511060
)
10521061

1062+
def match(self, path_pattern, *, case_sensitive=None):
1063+
if case_sensitive is None:
1064+
case_sensitive = True
1065+
return super().match(
1066+
path_pattern, case_sensitive=case_sensitive
1067+
) # pytype: disable=wrong-keyword-args
1068+
1069+
def full_match(self, pattern, *, case_sensitive=None):
1070+
if case_sensitive is None:
1071+
case_sensitive = True
1072+
return super().full_match( # pytype: disable=attribute-error
1073+
pattern, case_sensitive=case_sensitive
1074+
) # pytype: disable=wrong-keyword-args
1075+
10531076
Path = FakePath
10541077

10551078
def __getattr__(self, name):

pyfakefs/tests/fake_pathlib_test.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,36 @@ def test_match(self):
265265
self.assertTrue(self.path("/a.py").match("/*.py"))
266266
self.assertFalse(self.path("a/b.py").match("/*.py"))
267267

268+
@unittest.skipIf(sys.version_info < (3, 13), "full_match new in Python 3.13")
269+
def test_full_match(self):
270+
self.assertFalse(self.path("a/b.py").full_match("*.py"))
271+
self.assertTrue(self.path("/a/b.py").full_match("/a/*.py"))
272+
self.assertTrue(self.path("/a/b/c.py").full_match("/a/**"))
273+
274+
def test_match_with_differing_case_posix(self):
275+
self.check_posix_only()
276+
self.assertFalse(self.path("a/b.py").match("*.Py"))
277+
self.assertFalse(self.path("/a/b/c.py").match("B/*.py"))
278+
self.assertFalse(self.path("/a.py").match("/*.PY"))
279+
280+
def test_match_with_differing_case_windows(self):
281+
self.check_windows_only()
282+
self.assertTrue(self.path("a/b.py").match("*.Py"))
283+
self.assertTrue(self.path("/a/b/c.py").match("B/*.py"))
284+
self.assertTrue(self.path("/a.py").match("/*.PY"))
285+
286+
@unittest.skipIf(sys.version_info < (3, 13), "full_match new in Python 3.13")
287+
def test_full_match_with_differing_case_posix(self):
288+
self.check_posix_only()
289+
self.assertFalse(self.path("/a/b.py").full_match("/a/*.PY"))
290+
self.assertFalse(self.path("/a/b/c.py").full_match("/A/**"))
291+
292+
@unittest.skipIf(sys.version_info < (3, 13), "full_match new in Python 3.13")
293+
def test_full_match_with_differing_case_windows(self):
294+
self.check_windows_only()
295+
self.assertTrue(self.path("/a/b.py").full_match("/a/*.PY"))
296+
self.assertTrue(self.path("/a/b/c.py").full_match("/A/**"))
297+
268298
def test_relative_to(self):
269299
self.assertEqual(
270300
self.path("/etc/passwd").relative_to("/"), self.path("etc/passwd")
@@ -298,6 +328,20 @@ def test_with_suffix(self):
298328
self.path("README").with_suffix(".txt"), self.path("README.txt")
299329
)
300330

331+
def test_path_comparison_posix(self):
332+
self.check_posix_only()
333+
path1 = self.path("Test")
334+
path2 = self.path("test")
335+
assert path1 != path2
336+
assert hash(path1) != hash(path2)
337+
338+
def test_path_comparison_windows(self):
339+
self.check_windows_only()
340+
path1 = self.path("Test")
341+
path2 = self.path("test")
342+
assert path1 == path2
343+
assert hash(path1) == hash(path2)
344+
301345

302346
class RealPathlibPurePathTest(FakePathlibPurePathTest):
303347
def use_real_fs(self):

0 commit comments

Comments
 (0)