From 0478814c06309cdc4fdcb2c5f4901ee973c98295 Mon Sep 17 00:00:00 2001 From: MonikaDvorackova Date: Fri, 30 Oct 2026 19:00:57 +0100 Subject: [PATCH 1/4] gh-160900: support path-like objects in fnmatch filters --- Lib/fnmatch.py | 27 ++++++++------------------- Lib/test/test_fnmatch.py | 9 +++++++++ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index 10e1c936688d8f8..ae4aa443afbbfdc 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -13,7 +13,6 @@ import functools import itertools import os -import posixpath import re __all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"] @@ -55,15 +54,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 +64,11 @@ 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 list( + itertools.filterfalse( + lambda name: match(os.path.normcase(name)), names + ) + ) def fnmatchcase(name, pat): diff --git a/Lib/test/test_fnmatch.py b/Lib/test/test_fnmatch.py index 5daaf3b3fddb9ea..a5a7496c8702d86 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,6 +318,10 @@ def test_star_indices_locations(self): class FilterTestCase(unittest.TestCase): + def test_pathlike(self): + names = [pathlib.Path("Python"), pathlib.Path("Ruby")] + self.assertEqual(filter(names, "P*"), [names[0]]) + def test_filter(self): self.assertEqual(filter(['Python', 'Ruby', 'Perl', 'Tcl'], 'P*'), @@ -342,6 +347,10 @@ def test_sep(self): class FilterFalseTestCase(unittest.TestCase): + def test_pathlike(self): + names = [pathlib.Path("Python"), pathlib.Path("Ruby")] + self.assertEqual(filterfalse(names, "P*"), [names[1]]) + def test_filterfalse(self): actual = filterfalse(['Python', 'Ruby', 'Perl', 'Tcl'], 'P*') From feb3718b7b285dc587538f4da44cac43d917c767 Mon Sep 17 00:00:00 2001 From: MonikaDvorackova Date: Fri, 30 Oct 2026 19:06:15 +0100 Subject: [PATCH 2/4] gh-160900: add NEWS entry --- .../next/Library/2026-10-30-19-04-37.gh-issue-160900.007288.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-30-19-04-37.gh-issue-160900.007288.rst 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 000000000000000..e28ac3c46be6901 --- /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. From ed77e0a2cc92b7f6b1553fc4f3db417b0b25b998 Mon Sep 17 00:00:00 2001 From: MonikaDvorackova Date: Wed, 4 Nov 2026 15:32:01 +0100 Subject: [PATCH 3/4] gh-160980: Document path-like filtering and optimize filterfalse --- Doc/library/fnmatch.rst | 16 ++++++++++++---- Lib/fnmatch.py | 10 ++++------ Lib/test/test_fnmatch.py | 18 ++++++++---------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Doc/library/fnmatch.rst b/Doc/library/fnmatch.rst index 2b0163bd4ce3535..3091c5dd46bbc42 100644 --- a/Doc/library/fnmatch.rst +++ b/Doc/library/fnmatch.rst @@ -85,19 +85,27 @@ functions: :func:`~fnmatch.fnmatch`, :func:`fnmatchcase`, :func:`.filter`, .. function:: filter(names, pat) - Construct a list from those elements of the :term:`iterable` of filename - strings *names* that match the pattern string *pat*. + Construct a list from those elements of the :term:`iterable` of filenames + *names* that match the pattern string *pat*. It is the same as ``[n for n in names if fnmatch(n, pat)]``, but implemented more efficiently. + The filenames may be strings or path-like objects. Each filename is + normalized using :func:`os.path.normcase` for matching; the original + objects are preserved in the returned list. + .. function:: filterfalse(names, pat) - Construct a list from those elements of the :term:`iterable` of filename - strings *names* that do not match the pattern string *pat*. + Construct a list from those elements of the :term:`iterable` of filenames + *names* that do not match the pattern string *pat*. It is the same as ``[n for n in names if not fnmatch(n, pat)]``, but implemented more efficiently. + The filenames may be strings or path-like objects. Each filename is + normalized using :func:`os.path.normcase` for matching; the original + objects are preserved in the returned list. + .. versionadded:: 3.14 diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index ae4aa443afbbfdc..bc849c20f93b203 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -11,7 +11,6 @@ """ import functools -import itertools import os import re @@ -64,11 +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) - return list( - itertools.filterfalse( - lambda name: match(os.path.normcase(name)), names - ) - ) + 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 a5a7496c8702d86..e9efde56b58427f 100644 --- a/Lib/test/test_fnmatch.py +++ b/Lib/test/test_fnmatch.py @@ -318,17 +318,16 @@ def test_star_indices_locations(self): class FilterTestCase(unittest.TestCase): - def test_pathlike(self): - names = [pathlib.Path("Python"), pathlib.Path("Ruby")] - self.assertEqual(filter(names, "P*"), [names[0]]) - - 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'], '*') @@ -347,17 +346,16 @@ def test_sep(self): class FilterFalseTestCase(unittest.TestCase): - def test_pathlike(self): - names = [pathlib.Path("Python"), pathlib.Path("Ruby")] - self.assertEqual(filterfalse(names, "P*"), [names[1]]) - - 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'], '*') From e32415cd01e95b1c36667a15b0ec603c849eb3fe Mon Sep 17 00:00:00 2001 From: MonikaDvorackova Date: Wed, 4 Nov 2026 17:09:05 +0100 Subject: [PATCH 4/4] gh-160900: document path-like support and test mixed filename types --- Doc/library/fnmatch.rst | 26 +++++++++++++------------- Lib/test/test_fnmatch.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/Doc/library/fnmatch.rst b/Doc/library/fnmatch.rst index 3091c5dd46bbc42..4f1486fefe98a02 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. @@ -85,29 +87,27 @@ functions: :func:`~fnmatch.fnmatch`, :func:`fnmatchcase`, :func:`.filter`, .. function:: filter(names, pat) - Construct a list from those elements of the :term:`iterable` of filenames - *names* that match the pattern string *pat*. + Construct a list from those elements of the :term:`iterable` of filename + strings *names* that match the pattern string *pat*. It is the same as ``[n for n in names if fnmatch(n, pat)]``, but implemented more efficiently. - The filenames may be strings or path-like objects. Each filename is - normalized using :func:`os.path.normcase` for matching; the original - objects are preserved in the returned list. + .. versionchanged:: next + Added support for path-like objects in *names*. .. function:: filterfalse(names, pat) - Construct a list from those elements of the :term:`iterable` of filenames - *names* that do not match the pattern string *pat*. + Construct a list from those elements of the :term:`iterable` of filename + strings *names* that do not match the pattern string *pat*. It is the same as ``[n for n in names if not fnmatch(n, pat)]``, but implemented more efficiently. - The filenames may be strings or path-like objects. Each filename is - normalized using :func:`os.path.normcase` for matching; the original - objects are preserved in the returned list. - .. versionadded:: 3.14 + .. versionchanged:: next + Added support for path-like objects in *names*. + .. function:: translate(pat) diff --git a/Lib/test/test_fnmatch.py b/Lib/test/test_fnmatch.py index e9efde56b58427f..31f7b0525b2d14e 100644 --- a/Lib/test/test_fnmatch.py +++ b/Lib/test/test_fnmatch.py @@ -332,6 +332,16 @@ 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']) @@ -360,6 +370,16 @@ 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'])