diff --git a/Doc/library/fnmatch.rst b/Doc/library/fnmatch.rst index 5cb47777ae527d..8ebb09f1f0ff4f 100644 --- a/Doc/library/fnmatch.rst +++ b/Doc/library/fnmatch.rst @@ -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 diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 3c876a193fad32..8e684b767610ed 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -529,6 +529,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 --------- diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index 865baea23467ea..e618df6df71d03 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -9,10 +9,12 @@ 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"] @@ -61,6 +63,20 @@ 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. diff --git a/Lib/test/test_fnmatch.py b/Lib/test/test_fnmatch.py index 9f360e1dc10f47..bd304c925c7ab8 100644 --- a/Lib/test/test_fnmatch.py +++ b/Lib/test/test_fnmatch.py @@ -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): @@ -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'], '*') diff --git a/Misc/NEWS.d/next/Library/2024-06-30-17-00-00.gh-issue-74598.1gVy_8.rst b/Misc/NEWS.d/next/Library/2024-06-30-17-00-00.gh-issue-74598.1gVy_8.rst new file mode 100644 index 00000000000000..3e0d052a58219e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-30-17-00-00.gh-issue-74598.1gVy_8.rst @@ -0,0 +1,2 @@ +Add :func:`fnmatch.filterfalse` for excluding names matching a pattern. +Patch by Bénédikt Tran.