diff --git a/Doc/library/fnmatch.rst b/Doc/library/fnmatch.rst index fda44923f204fc..f3458cb5b8b7f1 100644 --- a/Doc/library/fnmatch.rst +++ b/Doc/library/fnmatch.rst @@ -83,6 +83,10 @@ cache the compiled regex patterns in the following functions: :func:`fnmatch`, It is the same as ``[n for n in names if fnmatch(n, pat)]``, but implemented more efficiently. + .. versionchanged:: 3.14 + Added support for :term:`path-like objects ` for + the *names* parameter. + .. function:: translate(pat) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index 73acb1fe8d4106..6b798b906e1c95 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -10,7 +10,6 @@ corresponding to PATTERN. (It does not compile it.) """ import os -import posixpath import re import functools @@ -47,19 +46,10 @@ def _compile_pattern(pat): def filter(names, pat): """Construct a list from those elements of the iterable NAMES that match PAT.""" - result = [] - pat = os.path.normcase(pat) + normcase = os.path.normcase + pat = normcase(pat) match = _compile_pattern(pat) - if os.path is posixpath: - # normcase on posix is NOP. Optimize it away from the loop. - for name in names: - if match(name): - result.append(name) - else: - for name in names: - if match(os.path.normcase(name)): - result.append(name) - return result + return [name for name in names if match(normcase(name))] def fnmatchcase(name, pat): """Test whether FILENAME matches PATTERN, including case. diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py index 891405943b78c5..726d2ad1ef461b 100644 --- a/Lib/test/support/os_helper.py +++ b/Lib/test/support/os_helper.py @@ -600,6 +600,9 @@ def __init__(self, path): def __repr__(self): return f'' + def __eq__(self, other): + return isinstance(other, type(self)) and self.path == other.path + def __fspath__(self): if (isinstance(self.path, BaseException) or isinstance(self.path, type) and diff --git a/Lib/test/test_fnmatch.py b/Lib/test/test_fnmatch.py index 10ed496d4e2f37..7ede2d6b622cc1 100644 --- a/Lib/test/test_fnmatch.py +++ b/Lib/test/test_fnmatch.py @@ -4,6 +4,8 @@ import os import string import warnings +from pathlib import Path +from test.support.os_helper import FakePath from fnmatch import fnmatch, fnmatchcase, translate, filter @@ -253,14 +255,26 @@ def test_translate(self): class FilterTestCase(unittest.TestCase): def test_filter(self): - self.assertEqual(filter(['Python', 'Ruby', 'Perl', 'Tcl'], 'P*'), - ['Python', 'Perl']) - self.assertEqual(filter([b'Python', b'Ruby', b'Perl', b'Tcl'], b'P*'), - [b'Python', b'Perl']) + for cls in (str, Path, FakePath): + names = list(map(cls, ['Python', 'Ruby', 'Perl', 'Tcl'])) + filtered = list(map(cls, ['Python', 'Perl'])) + with self.subTest('string pattern', cls=cls, names=names): + self.assertListEqual(filter(names, 'P*'), filtered) + # We want to test the case when os.path.normcase() returns bytes but + # we cannot use pathlib.Path since they only accept string objects. + for cls in (bytes, FakePath): + names = list(map(cls, [b'Python', b'Ruby', b'Perl', b'Tcl'])) + filtered = list(map(cls, [b'Python', b'Perl'])) + with self.subTest('bytes pattern', cls=cls, names=names): + self.assertListEqual(filter(names, b'P*'), filtered) def test_mix_bytes_str(self): self.assertRaises(TypeError, filter, ['test'], b'*') self.assertRaises(TypeError, filter, [b'test'], '*') + # We want to test the case when os.path.normcase() returns bytes but + # we cannot use pathlib.Path since they only accept string objects. + self.assertRaises(TypeError, filter, [FakePath('test')], b'*') + self.assertRaises(TypeError, filter, [FakePath(b'test')], '*') def test_case(self): ignorecase = os.path.normcase('P') == os.path.normcase('p') diff --git a/Misc/NEWS.d/next/Library/2024-08-19-09-36-50.gh-issue-123135.w_ZNAs.rst b/Misc/NEWS.d/next/Library/2024-08-19-09-36-50.gh-issue-123135.w_ZNAs.rst new file mode 100644 index 00000000000000..1bf53c1bc4e9fe --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-08-19-09-36-50.gh-issue-123135.w_ZNAs.rst @@ -0,0 +1,3 @@ +Added support for supplying :term:`path-like objects ` +to the *names* parameter of :func:`fnmatch.filter`. Previously, such +objects were only accepted on non-POSIX platforms. Patch by Bénédikt Tran.