Skip to content
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))
]


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
29 changes: 27 additions & 2 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 @@ -317,17 +318,29 @@ def test_star_indices_locations(self):


class FilterTestCase(unittest.TestCase):

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):
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 @@ -342,17 +355,29 @@ def test_sep(self):


class FilterFalseTestCase(unittest.TestCase):

Comment thread
MonikaDvorackova marked this conversation as resolved.
Resolved
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'], '*')

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 path-like objects on POSIX platforms.
Comment thread
MonikaDvorackova marked this conversation as resolved.
Resolved
Loading