From 538ab0a430119cf64c52bf2fea3e657ac7a1e954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:00:30 +0200 Subject: [PATCH 01/13] add `fnmatch.filterfalse` --- Lib/fnmatch.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index 73acb1fe8d4106..1e1080872f41ef 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -61,6 +61,23 @@ 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.""" + 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: + # using lambda function is usually worse than using explicit 'not' + if not match(name): + result.append(name) + else: + for name in names: + if not match(os.path.normcase(name)): + result.append(name) + return result + def fnmatchcase(name, pat): """Test whether FILENAME matches PATTERN, including case. From ce7720c9c679c2dcae4ad6b00abea8eaf3560503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:00:40 +0200 Subject: [PATCH 02/13] add tests --- Lib/test/test_fnmatch.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_fnmatch.py b/Lib/test/test_fnmatch.py index 10ed496d4e2f37..53eacefcf89a5c 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): @@ -258,6 +258,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'], '*') From 11ca0f3de36ee904a742580770ab36af94f71a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:00:46 +0200 Subject: [PATCH 03/13] add doc --- Doc/library/fnmatch.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Doc/library/fnmatch.rst b/Doc/library/fnmatch.rst index fda44923f204fc..30a91bbb90a0ae 100644 --- a/Doc/library/fnmatch.rst +++ b/Doc/library/fnmatch.rst @@ -79,11 +79,21 @@ cache the compiled regex patterns in the following functions: :func:`fnmatch`, .. function:: filter(names, pat) Construct a list from those elements of the :term:`iterable` *names* - that match pattern *pat*. + that match the pattern *pat*. It is the same as ``[n for n in names if fnmatch(n, pat)]``, but implemented more efficiently. +.. function:: filterfalse(names, pat) + + Construct a list from those elements of the :term:`iterable` *names* + that do not match the pattern *pat*. + It is the same as ``[n for n in names if not fnmatch(n, pat)]``, + but implemented more efficiently. + + .. versionadded:: 3.14 + + .. function:: translate(pat) Return the shell-style pattern *pat* converted to a regular expression for From c1957f2166e7cbb4e12d44ded5d2bcf389cb88c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:00:54 +0200 Subject: [PATCH 04/13] add What's New? --- Doc/whatsnew/3.14.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index ee3001661b3143..642c4c622c2628 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -92,6 +92,13 @@ ast Added :func:`ast.compare` for comparing two ASTs. (Contributed by Batuhan Taskaya and Jeremy Hylton in :issue:`15987`.) +fnmatch +------- + +* Added :func:`fnmatch.filterfalse` for excluding names matching a pattern. + + (Contributed by Bénédikt Tran in :gh:`74598`.) + os -- From cbb7813cd7492db9afd17bcfb3cc5f899e310498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:00:58 +0200 Subject: [PATCH 05/13] blurb --- .../next/Library/2024-06-30-17-00-00.gh-issue-74598.1gVy_8.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-06-30-17-00-00.gh-issue-74598.1gVy_8.rst 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. From 56e6a0ec9122264f7eda09a1678feb5b0e672eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:07:23 +0200 Subject: [PATCH 06/13] use 'is None' instead of 'not' --- Lib/fnmatch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index 1e1080872f41ef..6fc4aa4199715d 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -69,12 +69,11 @@ def filterfalse(names, pat): if os.path is posixpath: # normcase on posix is NOP. Optimize it away from the loop. for name in names: - # using lambda function is usually worse than using explicit 'not' - if not match(name): + if match(name) is None: result.append(name) else: for name in names: - if not match(os.path.normcase(name)): + if match(os.path.normcase(name)) is None: result.append(name) return result From f36b2a8a1fb96de78a8425238dc1841f45674491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:00:07 +0200 Subject: [PATCH 07/13] use `itertools.filterfalse` with POSIX paths --- Lib/fnmatch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index 6fc4aa4199715d..006107a07b64cf 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -9,6 +9,7 @@ The function translate(PATTERN) returns a regular expression corresponding to PATTERN. (It does not compile it.) """ +import itertools import os import posixpath import re @@ -68,9 +69,7 @@ def filterfalse(names, 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) is None: - result.append(name) + return list(itertools.filterfalse(match, names)) else: for name in names: if match(os.path.normcase(name)) is None: From 0030a5e0b7600cb7972aed4c4bce04865fd0fbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:00:33 +0200 Subject: [PATCH 08/13] re-order imports --- Lib/fnmatch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index 006107a07b64cf..b9481cb1a2583b 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -9,11 +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"] From ddeea64d77ed35acc87a3f075963dbe49810b2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:01:11 +0200 Subject: [PATCH 09/13] micro-optimization of variable --- Lib/fnmatch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index b9481cb1a2583b..be3cc7751b972c 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -65,16 +65,16 @@ def filter(names, pat): def filterfalse(names, pat): """Construct a list from those elements of the iterable NAMES that do not 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. return list(itertools.filterfalse(match, names)) - else: - for name in names: - if match(os.path.normcase(name)) is None: - result.append(name) + + result = [] + for name in names: + if match(os.path.normcase(name)) is None: + result.append(name) return result def fnmatchcase(name, pat): From 02b422cfa2159737ddcf20371e3ade6631174018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:20:14 +0100 Subject: [PATCH 10/13] Update Doc/whatsnew/3.14.rst --- Doc/whatsnew/3.14.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 3a8ea8adec4546..4aa9be54dec455 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -300,6 +300,13 @@ dis (Contributed by Bénédikt Tran in :gh:`123165`.) +fnmatch +------- + +* Added :func:`fnmatch.filterfalse` for excluding names matching a pattern. + (Contributed by Bénédikt Tran in :gh:`74598`.) + + fractions --------- @@ -424,13 +431,6 @@ operator (Contributed by Raymond Hettinger and Nico Mexis in :gh:`115808`.) -fnmatch -------- - -* Added :func:`fnmatch.filterfalse` for excluding names matching a pattern. - - (Contributed by Bénédikt Tran in :gh:`74598`.) - os -- From bc1a3c71ed5fc74ce398f9edf30d8bc443b0dda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:21:32 +0100 Subject: [PATCH 11/13] Update Doc/library/fnmatch.rst --- Doc/library/fnmatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/fnmatch.rst b/Doc/library/fnmatch.rst index 30a91bbb90a0ae..b6ea66357f6a48 100644 --- a/Doc/library/fnmatch.rst +++ b/Doc/library/fnmatch.rst @@ -91,7 +91,7 @@ cache the compiled regex patterns in the following functions: :func:`fnmatch`, It is the same as ``[n for n in names if not fnmatch(n, pat)]``, but implemented more efficiently. - .. versionadded:: 3.14 + .. versionadded:: next .. function:: translate(pat) From 4e3ec142dac7bbb194ec1f6d0b8330edecabc21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 7 Apr 2025 09:42:58 +0200 Subject: [PATCH 12/13] a bit of PEP-8 --- Lib/fnmatch.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py index e618df6df71d03..1dee8330f5d9d5 100644 --- a/Lib/fnmatch.py +++ b/Lib/fnmatch.py @@ -16,7 +16,8 @@ import posixpath import re -__all__ = ["filter", "fnmatch", "fnmatchcase", "translate"] +__all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"] + def fnmatch(name, pat): """Test whether FILENAME matches PATTERN. @@ -37,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): @@ -47,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 = [] @@ -63,6 +66,7 @@ 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) @@ -77,6 +81,7 @@ def filterfalse(names, pat): result.append(name) return result + def fnmatchcase(name, pat): """Test whether FILENAME matches PATTERN, including case. @@ -96,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 From ac179c1b8cb1e15d968cf71dcc35c04aac34a4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:47:06 +0200 Subject: [PATCH 13/13] increase test coverage --- Lib/test/test_fnmatch.py | 102 ++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/Lib/test/test_fnmatch.py b/Lib/test/test_fnmatch.py index bd304c925c7ab8..d4163cfe782ce0 100644 --- a/Lib/test/test_fnmatch.py +++ b/Lib/test/test_fnmatch.py @@ -1,12 +1,16 @@ """Test cases for the fnmatch module.""" -import unittest import os import string +import unittest import warnings - from fnmatch import fnmatch, fnmatchcase, translate, filter, filterfalse + +IGNORECASE = os.path.normcase('P') == os.path.normcase('p') +NORMSEP = os.path.normcase('\\') == os.path.normcase('/') + + class FnmatchTestCase(unittest.TestCase): def check_match(self, filename, pattern, should_match=True, fn=fnmatch): @@ -77,23 +81,20 @@ def test_bytes(self): self.check_match(b'foo\nbar', b'foo*') def test_case(self): - ignorecase = os.path.normcase('ABC') == os.path.normcase('abc') check = self.check_match check('abc', 'abc') - check('AbC', 'abc', ignorecase) - check('abc', 'AbC', ignorecase) + check('AbC', 'abc', IGNORECASE) + check('abc', 'AbC', IGNORECASE) check('AbC', 'AbC') def test_sep(self): - normsep = os.path.normcase('\\') == os.path.normcase('/') check = self.check_match check('usr/bin', 'usr/bin') - check('usr\\bin', 'usr/bin', normsep) - check('usr/bin', 'usr\\bin', normsep) + check('usr\\bin', 'usr/bin', NORMSEP) + check('usr/bin', 'usr\\bin', NORMSEP) check('usr\\bin', 'usr\\bin') def test_char_set(self): - ignorecase = os.path.normcase('ABC') == os.path.normcase('abc') check = self.check_match tescases = string.ascii_lowercase + string.digits + string.punctuation for c in tescases: @@ -101,11 +102,11 @@ def test_char_set(self): check(c, '[!az]', c not in 'az') # Case insensitive. for c in tescases: - check(c, '[AZ]', (c in 'az') and ignorecase) - check(c, '[!AZ]', (c not in 'az') or not ignorecase) + check(c, '[AZ]', (c in 'az') and IGNORECASE) + check(c, '[!AZ]', (c not in 'az') or not IGNORECASE) for c in string.ascii_uppercase: - check(c, '[az]', (c in 'AZ') and ignorecase) - check(c, '[!az]', (c not in 'AZ') or not ignorecase) + check(c, '[az]', (c in 'AZ') and IGNORECASE) + check(c, '[!az]', (c not in 'AZ') or not IGNORECASE) # Repeated same character. for c in tescases: check(c, '[aa]', c == 'a') @@ -120,8 +121,6 @@ def test_char_set(self): check('[!]', '[!]') def test_range(self): - ignorecase = os.path.normcase('ABC') == os.path.normcase('abc') - normsep = os.path.normcase('\\') == os.path.normcase('/') check = self.check_match tescases = string.ascii_lowercase + string.digits + string.punctuation for c in tescases: @@ -131,11 +130,11 @@ def test_range(self): check(c, '[!b-dx-z]', c not in 'bcdxyz') # Case insensitive. for c in tescases: - check(c, '[B-D]', (c in 'bcd') and ignorecase) - check(c, '[!B-D]', (c not in 'bcd') or not ignorecase) + check(c, '[B-D]', (c in 'bcd') and IGNORECASE) + check(c, '[!B-D]', (c not in 'bcd') or not IGNORECASE) for c in string.ascii_uppercase: - check(c, '[b-d]', (c in 'BCD') and ignorecase) - check(c, '[!b-d]', (c not in 'BCD') or not ignorecase) + check(c, '[b-d]', (c in 'BCD') and IGNORECASE) + check(c, '[!b-d]', (c not in 'BCD') or not IGNORECASE) # Upper bound == lower bound. for c in tescases: check(c, '[b-b]', c == 'b') @@ -144,7 +143,7 @@ def test_range(self): check(c, '[!-#]', c not in '-#') check(c, '[!--.]', c not in '-.') check(c, '[^-`]', c in '^_`') - if not (normsep and c == '/'): + if not (NORMSEP and c == '/'): check(c, '[[-^]', c in r'[\]^') check(c, r'[\-^]', c in r'\]^') check(c, '[b-]', c in '-b') @@ -160,47 +159,45 @@ def test_range(self): check(c, '[d-bx-z]', c in 'xyz') check(c, '[!d-bx-z]', c not in 'xyz') check(c, '[d-b^-`]', c in '^_`') - if not (normsep and c == '/'): + if not (NORMSEP and c == '/'): check(c, '[d-b[-^]', c in r'[\]^') def test_sep_in_char_set(self): - normsep = os.path.normcase('\\') == os.path.normcase('/') check = self.check_match check('/', r'[/]') check('\\', r'[\]') - check('/', r'[\]', normsep) - check('\\', r'[/]', normsep) + check('/', r'[\]', NORMSEP) + check('\\', r'[/]', NORMSEP) check('[/]', r'[/]', False) check(r'[\\]', r'[/]', False) check('\\', r'[\t]') - check('/', r'[\t]', normsep) + check('/', r'[\t]', NORMSEP) check('t', r'[\t]') check('\t', r'[\t]', False) def test_sep_in_range(self): - normsep = os.path.normcase('\\') == os.path.normcase('/') check = self.check_match - check('a/b', 'a[.-0]b', not normsep) + check('a/b', 'a[.-0]b', not NORMSEP) check('a\\b', 'a[.-0]b', False) - check('a\\b', 'a[Z-^]b', not normsep) + check('a\\b', 'a[Z-^]b', not NORMSEP) check('a/b', 'a[Z-^]b', False) - check('a/b', 'a[/-0]b', not normsep) + check('a/b', 'a[/-0]b', not NORMSEP) check(r'a\b', 'a[/-0]b', False) check('a[/-0]b', 'a[/-0]b', False) check(r'a[\-0]b', 'a[/-0]b', False) check('a/b', 'a[.-/]b') - check(r'a\b', 'a[.-/]b', normsep) + check(r'a\b', 'a[.-/]b', NORMSEP) check('a[.-/]b', 'a[.-/]b', False) check(r'a[.-\]b', 'a[.-/]b', False) check(r'a\b', r'a[\-^]b') - check('a/b', r'a[\-^]b', normsep) + check('a/b', r'a[\-^]b', NORMSEP) check(r'a[\-^]b', r'a[\-^]b', False) check('a[/-^]b', r'a[\-^]b', False) - check(r'a\b', r'a[Z-\]b', not normsep) + check(r'a\b', r'a[Z-\]b', not NORMSEP) check('a/b', r'a[Z-\]b', False) check(r'a[Z-\]b', r'a[Z-\]b', False) check('a[Z-/]b', r'a[Z-\]b', False) @@ -327,29 +324,46 @@ 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'], '*') def test_case(self): - ignorecase = os.path.normcase('P') == os.path.normcase('p') self.assertEqual(filter(['Test.py', 'Test.rb', 'Test.PL'], '*.p*'), - ['Test.py', 'Test.PL'] if ignorecase else ['Test.py']) + ['Test.py', 'Test.PL'] if IGNORECASE else ['Test.py']) self.assertEqual(filter(['Test.py', 'Test.rb', 'Test.PL'], '*.P*'), - ['Test.py', 'Test.PL'] if ignorecase else ['Test.PL']) + ['Test.py', 'Test.PL'] if IGNORECASE else ['Test.PL']) def test_sep(self): - normsep = os.path.normcase('\\') == os.path.normcase('/') self.assertEqual(filter(['usr/bin', 'usr', 'usr\\lib'], 'usr/*'), - ['usr/bin', 'usr\\lib'] if normsep else ['usr/bin']) + ['usr/bin', 'usr\\lib'] if NORMSEP else ['usr/bin']) self.assertEqual(filter(['usr/bin', 'usr', 'usr\\lib'], 'usr\\*'), - ['usr/bin', 'usr\\lib'] if normsep else ['usr\\lib']) + ['usr/bin', 'usr\\lib'] if NORMSEP else ['usr\\lib']) + + +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_mix_bytes_str(self): + self.assertRaises(TypeError, filterfalse, ['test'], b'*') + self.assertRaises(TypeError, filterfalse, [b'test'], '*') + + def test_case(self): + self.assertEqual(filterfalse(['Test.py', 'Test.rb', 'Test.PL'], '*.p*'), + ['Test.rb'] if IGNORECASE else ['Test.rb', 'Test.PL']) + self.assertEqual(filterfalse(['Test.py', 'Test.rb', 'Test.PL'], '*.P*'), + ['Test.rb'] if IGNORECASE else ['Test.py', 'Test.rb',]) + + def test_sep(self): + self.assertEqual(filterfalse(['usr/bin', 'usr', 'usr\\lib'], 'usr/*'), + ['usr'] if NORMSEP else ['usr', 'usr\\lib']) + self.assertEqual(filterfalse(['usr/bin', 'usr', 'usr\\lib'], 'usr\\*'), + ['usr'] if NORMSEP else ['usr/bin', 'usr']) if __name__ == "__main__":