diff --git a/Doc/library/fnmatch.rst b/Doc/library/fnmatch.rst index 2b0163bd4ce353..4f1486fefe98a0 100644 --- a/Doc/library/fnmatch.rst +++ b/Doc/library/fnmatch.rst @@ -46,8 +46,10 @@ module. See module :mod:`glob` for pathname expansion (:mod:`glob` uses a period are not special for this module, and are matched by the ``*`` and ``?`` patterns. -Unless stated otherwise, "filename string" and "pattern string" either refer to -:class:`str` or ``ISO-8859-1`` encoded :class:`bytes` objects. Note that the +Unless stated otherwise, "filename string" and "pattern string" refer to +:class:`str` or ``ISO-8859-1`` encoded :class:`bytes` objects. +In addition to filename strings, :func:`.filter` and :func:`.filterfalse` +accept :term:`path-like objects ` in *names*. Note that the functions documented below do not allow to mix a :class:`!bytes` pattern with a :class:`!str` filename, and vice-versa. @@ -90,6 +92,9 @@ functions: :func:`~fnmatch.fnmatch`, :func:`fnmatchcase`, :func:`.filter`, It is the same as ``[n for n in names if fnmatch(n, pat)]``, but implemented more efficiently. + .. versionchanged:: next + Added support for path-like objects in *names*. + .. function:: filterfalse(names, pat) @@ -100,6 +105,9 @@ functions: :func:`~fnmatch.fnmatch`, :func:`fnmatchcase`, :func:`.filter`, .. versionadded:: 3.14 + .. versionchanged:: next + Added support for path-like objects in *names*. + .. function:: translate(pat) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index 10e1c936688d8f..bc849c20f93b20 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -11,9 +11,7 @@ """ import functools -import itertools import os -import posixpath import re __all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"] @@ -55,15 +53,9 @@ def filter(names, pat): result = [] pat = os.path.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) + for name in names: + if match(os.path.normcase(name)): + result.append(name) return result @@ -71,15 +63,10 @@ def filterfalse(names, pat): """Construct a list from those elements of the iterable NAMES that do not match PAT.""" pat = os.path.normcase(pat) match = _compile_pattern(pat) - if os.path is posixpath: - # normcase on posix is NOP. Optimize it away from the loop. - return list(itertools.filterfalse(match, names)) - - result = [] - for name in names: - if match(os.path.normcase(name)) is None: - result.append(name) - return result + return [ + name for name in names + if not match(os.path.normcase(name)) + ] def fnmatchcase(name, pat): diff --git a/Lib/test/test_fnmatch.py b/Lib/test/test_fnmatch.py index 5daaf3b3fddb9e..31f7b0525b2d14 100644 --- a/Lib/test/test_fnmatch.py +++ b/Lib/test/test_fnmatch.py @@ -1,6 +1,7 @@ """Test cases for the fnmatch module.""" import os +import pathlib import string import unittest import warnings @@ -317,17 +318,30 @@ def test_star_indices_locations(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']) + def test_filter_pathlike(self): + names = [pathlib.Path("Python"), pathlib.Path("Ruby")] + self.assertEqual(filter(names, "P*"), [names[0]]) + def test_mix_bytes_str(self): self.assertRaises(TypeError, filter, ['test'], b'*') self.assertRaises(TypeError, filter, [b'test'], '*') + + def test_mix_bytes_str_pathlike(self): + path = pathlib.Path("test") + self.assertRaises(TypeError, filter, [path], b'*') + self.assertRaises(TypeError, filter, [path, b'test'], '*') + self.assertRaises(TypeError, filter, [b'test', path], '*') + self.assertRaises(TypeError, filter, [path, 'test'], b'*') + self.assertRaises(TypeError, filter, ['test', path], b'*') + self.assertEqual(filter([path, 'other'], 'te*'), [path]) + def test_case(self): self.assertEqual(filter(['Test.py', 'Test.rb', 'Test.PL'], '*.p*'), ['Test.py', 'Test.PL'] if IGNORECASE else ['Test.py']) @@ -342,17 +356,30 @@ def test_sep(self): class FilterFalseTestCase(unittest.TestCase): - def test_filterfalse(self): actual = filterfalse(['Python', 'Ruby', 'Perl', 'Tcl'], 'P*') self.assertListEqual(actual, ['Ruby', 'Tcl']) actual = filterfalse([b'Python', b'Ruby', b'Perl', b'Tcl'], b'P*') self.assertListEqual(actual, [b'Ruby', b'Tcl']) + def test_filterfalse_pathlike(self): + names = [pathlib.Path("Python"), pathlib.Path("Ruby")] + self.assertEqual(filterfalse(names, "P*"), [names[1]]) + def test_mix_bytes_str(self): self.assertRaises(TypeError, filterfalse, ['test'], b'*') self.assertRaises(TypeError, filterfalse, [b'test'], '*') + + def test_mix_bytes_str_pathlike(self): + path = pathlib.Path("test") + self.assertRaises(TypeError, filterfalse, [path], b'*') + self.assertRaises(TypeError, filterfalse, [path, b'test'], '*') + self.assertRaises(TypeError, filterfalse, [b'test', path], '*') + self.assertRaises(TypeError, filterfalse, [path, 'test'], b'*') + self.assertRaises(TypeError, filterfalse, ['test', path], b'*') + self.assertEqual(filterfalse([path, 'other'], 'te*'), ['other']) + def test_case(self): self.assertEqual(filterfalse(['Test.py', 'Test.rb', 'Test.PL'], '*.p*'), ['Test.rb'] if IGNORECASE else ['Test.rb', 'Test.PL']) diff --git a/Misc/NEWS.d/next/Library/2026-10-30-19-04-37.gh-issue-160900.007288.rst b/Misc/NEWS.d/next/Library/2026-10-30-19-04-37.gh-issue-160900.007288.rst new file mode 100644 index 00000000000000..e28ac3c46be690 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-30-19-04-37.gh-issue-160900.007288.rst @@ -0,0 +1 @@ +Allow :func:`fnmatch.filter` and :func:`fnmatch.filterfalse` to accept path-like objects on POSIX platforms.