Skip to content

gh-74598: add fnmatch.filterfalse for excluding names #121185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
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
10 changes: 10 additions & 0 deletions Doc/library/fnmatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ functions: :func:`fnmatch`, :func:`fnmatchcase`, :func:`.filter`.
but implemented more efficiently.


.. 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*.
It is the same as ``[n for n in names if not fnmatch(n, pat)]``,
but implemented more efficiently.

.. versionadded:: next


.. function:: translate(pat)

Return the shell-style pattern *pat* converted to a regular expression for
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,13 @@ errno
(Contributed by James Roy in :gh:`126585`.)


fnmatch
-------

* Added :func:`fnmatch.filterfalse` for excluding names matching a pattern.
(Contributed by Bénédikt Tran in :gh:`74598`.)


fractions
---------

Expand Down
27 changes: 25 additions & 2 deletions Lib/fnmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
The function translate(PATTERN) returns a regular expression
corresponding to PATTERN. (It does not compile it.)
"""

import functools
import itertools
import os
import posixpath
import re
import functools

__all__ = ["filter", "fnmatch", "fnmatchcase", "translate"]
__all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"]


def fnmatch(name, pat):
"""Test whether FILENAME matches PATTERN.
Expand All @@ -35,6 +38,7 @@ def fnmatch(name, pat):
pat = os.path.normcase(pat)
return fnmatchcase(name, pat)


@functools.lru_cache(maxsize=32768, typed=True)
def _compile_pattern(pat):
if isinstance(pat, bytes):
Expand All @@ -45,6 +49,7 @@ def _compile_pattern(pat):
res = translate(pat)
return re.compile(res).match


def filter(names, pat):
"""Construct a list from those elements of the iterable NAMES that match PAT."""
result = []
Expand All @@ -61,6 +66,22 @@ def filter(names, pat):
result.append(name)
return result


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


def fnmatchcase(name, pat):
"""Test whether FILENAME matches PATTERN, including case.

Expand All @@ -80,9 +101,11 @@ def translate(pat):
parts, star_indices = _translate(pat, '*', '.')
return _join_translated_parts(parts, star_indices)


_re_setops_sub = re.compile(r'([&~|])').sub
_re_escape = functools.lru_cache(maxsize=512)(re.escape)


def _translate(pat, star, question_mark):
res = []
add = res.append
Expand Down
8 changes: 7 additions & 1 deletion Lib/test/test_fnmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import string
import warnings

from fnmatch import fnmatch, fnmatchcase, translate, filter
from fnmatch import fnmatch, fnmatchcase, translate, filter, filterfalse

class FnmatchTestCase(unittest.TestCase):

Expand Down Expand Up @@ -327,6 +327,12 @@ def test_filter(self):
self.assertEqual(filter([b'Python', b'Ruby', b'Perl', b'Tcl'], b'P*'),
[b'Python', b'Perl'])

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_mix_bytes_str(self):
self.assertRaises(TypeError, filter, ['test'], b'*')
self.assertRaises(TypeError, filter, [b'test'], '*')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`fnmatch.filterfalse` for excluding names matching a pattern.
Patch by Bénédikt Tran.
Loading