Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0478814
gh-160900: support path-like objects in fnmatch filters
MonikaDvorackova Oct 30, 2026
feb3718
gh-160900: add NEWS entry
MonikaDvorackova Oct 30, 2026
ed77e0a
gh-160980: Document path-like filtering and optimize filterfalse
MonikaDvorackova Nov 4, 2026
c2f1cb1
Merge branch 'main' into fix/fnmatch-filter-pathlike
MonikaDvorackova Nov 4, 2026
e32415c
gh-160900: document path-like support and test mixed filename types
MonikaDvorackova Nov 4, 2026
5df7949
Merge branch 'main' into fix/fnmatch-filter-pathlike
MonikaDvorackova Nov 4, 2026
e968f2f
Merge branch 'main' into fix/fnmatch-filter-pathlike
MonikaDvorackova Nov 5, 2026
b55db66
Address fnmatch review comments
MonikaDvorackova Nov 5, 2026
fac71d6
Preserve POSIX optimization in fnmatch.filter
MonikaDvorackova Nov 6, 2026
de8ce43
Restore filterfalse POSIX fast path
MonikaDvorackova Nov 6, 2026
99c9daf
Restore fnmatch test and function spacing
MonikaDvorackova Nov 6, 2026
ebbadfc
Restore fnmatch test and function spacing
MonikaDvorackova Nov 6, 2026
9ab64b5
Revert unintended follow-up edits
MonikaDvorackova Nov 6, 2026
53e8cd0
Revert unintended follow-up edits
MonikaDvorackova Nov 6, 2026
ab8c256
gh-160900: support path-like objects in fnmatch filters
MonikaDvorackova Nov 6, 2026
aa6ccbf
Update Misc/NEWS.d/next/Library/2026-10-30-19-04-37.gh-issue-160900.0…
MonikaDvorackova Nov 10, 2026
6fe66e6
gh-160900: normalize path-like names in filterfalse
MonikaDvorackova Nov 10, 2026
584ec30
gh-160900: Remove redundant fspath calls
MonikaDvorackova Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Doc/library/fnmatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path-like object>` 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.

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down
30 changes: 8 additions & 22 deletions Lib/fnmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
"""

import functools
import itertools
import os
import posixpath
import re

__all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"]
Expand Down Expand Up @@ -52,34 +50,22 @@ 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)
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(os.path.normcase(name))
]


Comment thread
picnixz marked this conversation as resolved.
Resolved
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):
Expand Down
27 changes: 27 additions & 0 deletions Lib/test/test_fnmatch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test cases for the fnmatch module."""

import os
import pathlib
import string
import unittest
import warnings
Expand Down Expand Up @@ -324,10 +325,23 @@ def test_filter(self):
self.assertEqual(filter([b'Python', b'Ruby', b'Perl', b'Tcl'], b'P*'),
[b'Python', b'Perl'])

def test_filter_pathlike(self):
Comment thread
MonikaDvorackova marked this conversation as resolved.
Resolved
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'])
Expand All @@ -349,10 +363,23 @@ def test_filterfalse(self):
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'])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow :func:`fnmatch.filter` and :func:`fnmatch.filterfalse` to accept :term:`path-like objects <path-like object>`.
Loading