Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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
27 changes: 7 additions & 20 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 @@ -55,31 +53,20 @@ def filter(names, 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)
for name in names:
if match(os.path.normcase(name)):
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
return [
name for name in names
if not match(os.path.normcase(name))
]


def fnmatchcase(name, pat):
Expand Down
31 changes: 29 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,30 @@ 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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need a test where you mix path objects with bytes and those with strings (add test_mix_bytes_str_pathlike)

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 +356,30 @@ def test_sep(self):


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_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