Skip to content

Commit 8100be5

Browse files
barneygalezooba
andauthored
GH-81079: Add case_sensitive argument to pathlib.Path.glob() (GH-102710)
This argument allows case-sensitive matching to be enabled on Windows, and case-insensitive matching to be enabled on Posix. Co-authored-by: Steve Dower <[email protected]>
1 parent 09b7695 commit 8100be5

File tree

4 files changed

+51
-17
lines changed

4 files changed

+51
-17
lines changed

Doc/library/pathlib.rst

+18-2
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,7 @@ call fails (for example because the path doesn't exist).
855855
.. versionadded:: 3.5
856856

857857

858-
.. method:: Path.glob(pattern)
858+
.. method:: Path.glob(pattern, *, case_sensitive=None)
859859

860860
Glob the given relative *pattern* in the directory represented by this path,
861861
yielding all matching files (of any kind)::
@@ -876,6 +876,11 @@ call fails (for example because the path doesn't exist).
876876
PosixPath('setup.py'),
877877
PosixPath('test_pathlib.py')]
878878

879+
By default, or when the *case_sensitive* keyword-only argument is set to
880+
``None``, this method matches paths using platform-specific casing rules:
881+
typically, case-sensitive on POSIX, and case-insensitive on Windows.
882+
Set *case_sensitive* to ``True`` or ``False`` to override this behaviour.
883+
879884
.. note::
880885
Using the "``**``" pattern in large directory trees may consume
881886
an inordinate amount of time.
@@ -886,6 +891,9 @@ call fails (for example because the path doesn't exist).
886891
Return only directories if *pattern* ends with a pathname components
887892
separator (:data:`~os.sep` or :data:`~os.altsep`).
888893

894+
.. versionadded:: 3.12
895+
The *case_sensitive* argument.
896+
889897
.. method:: Path.group()
890898

891899
Return the name of the group owning the file. :exc:`KeyError` is raised
@@ -1271,7 +1279,7 @@ call fails (for example because the path doesn't exist).
12711279
.. versionadded:: 3.6
12721280
The *strict* argument (pre-3.6 behavior is strict).
12731281

1274-
.. method:: Path.rglob(pattern)
1282+
.. method:: Path.rglob(pattern, *, case_sensitive=None)
12751283

12761284
Glob the given relative *pattern* recursively. This is like calling
12771285
:func:`Path.glob` with "``**/``" added in front of the *pattern*, where
@@ -1284,12 +1292,20 @@ call fails (for example because the path doesn't exist).
12841292
PosixPath('setup.py'),
12851293
PosixPath('test_pathlib.py')]
12861294

1295+
By default, or when the *case_sensitive* keyword-only argument is set to
1296+
``None``, this method matches paths using platform-specific casing rules:
1297+
typically, case-sensitive on POSIX, and case-insensitive on Windows.
1298+
Set *case_sensitive* to ``True`` or ``False`` to override this behaviour.
1299+
12871300
.. audit-event:: pathlib.Path.rglob self,pattern pathlib.Path.rglob
12881301

12891302
.. versionchanged:: 3.11
12901303
Return only directories if *pattern* ends with a pathname components
12911304
separator (:data:`~os.sep` or :data:`~os.altsep`).
12921305

1306+
.. versionadded:: 3.12
1307+
The *case_sensitive* argument.
1308+
12931309
.. method:: Path.rmdir()
12941310

12951311
Remove this directory. The directory must be empty.

Lib/pathlib.py

+19-15
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def _is_case_sensitive(flavour):
6262
#
6363

6464
@functools.lru_cache()
65-
def _make_selector(pattern_parts, flavour):
65+
def _make_selector(pattern_parts, flavour, case_sensitive):
6666
pat = pattern_parts[0]
6767
child_parts = pattern_parts[1:]
6868
if not pat:
@@ -75,17 +75,17 @@ def _make_selector(pattern_parts, flavour):
7575
raise ValueError("Invalid pattern: '**' can only be an entire path component")
7676
else:
7777
cls = _WildcardSelector
78-
return cls(pat, child_parts, flavour)
78+
return cls(pat, child_parts, flavour, case_sensitive)
7979

8080

8181
class _Selector:
8282
"""A selector matches a specific glob pattern part against the children
8383
of a given path."""
8484

85-
def __init__(self, child_parts, flavour):
85+
def __init__(self, child_parts, flavour, case_sensitive):
8686
self.child_parts = child_parts
8787
if child_parts:
88-
self.successor = _make_selector(child_parts, flavour)
88+
self.successor = _make_selector(child_parts, flavour, case_sensitive)
8989
self.dironly = True
9090
else:
9191
self.successor = _TerminatingSelector()
@@ -108,8 +108,9 @@ def _select_from(self, parent_path, scandir):
108108

109109

110110
class _ParentSelector(_Selector):
111-
def __init__(self, name, child_parts, flavour):
112-
_Selector.__init__(self, child_parts, flavour)
111+
112+
def __init__(self, name, child_parts, flavour, case_sensitive):
113+
_Selector.__init__(self, child_parts, flavour, case_sensitive)
113114

114115
def _select_from(self, parent_path, scandir):
115116
path = parent_path._make_child_relpath('..')
@@ -119,10 +120,13 @@ def _select_from(self, parent_path, scandir):
119120

120121
class _WildcardSelector(_Selector):
121122

122-
def __init__(self, pat, child_parts, flavour):
123-
flags = re.NOFLAG if _is_case_sensitive(flavour) else re.IGNORECASE
123+
def __init__(self, pat, child_parts, flavour, case_sensitive):
124+
_Selector.__init__(self, child_parts, flavour, case_sensitive)
125+
if case_sensitive is None:
126+
# TODO: evaluate case-sensitivity of each directory in _select_from()
127+
case_sensitive = _is_case_sensitive(flavour)
128+
flags = re.NOFLAG if case_sensitive else re.IGNORECASE
124129
self.match = re.compile(fnmatch.translate(pat), flags=flags).fullmatch
125-
_Selector.__init__(self, child_parts, flavour)
126130

127131
def _select_from(self, parent_path, scandir):
128132
try:
@@ -153,8 +157,8 @@ def _select_from(self, parent_path, scandir):
153157

154158
class _RecursiveWildcardSelector(_Selector):
155159

156-
def __init__(self, pat, child_parts, flavour):
157-
_Selector.__init__(self, child_parts, flavour)
160+
def __init__(self, pat, child_parts, flavour, case_sensitive):
161+
_Selector.__init__(self, child_parts, flavour, case_sensitive)
158162

159163
def _iterate_directories(self, parent_path, scandir):
160164
yield parent_path
@@ -819,7 +823,7 @@ def _scandir(self):
819823
# includes scandir(), which is used to implement glob().
820824
return os.scandir(self)
821825

822-
def glob(self, pattern):
826+
def glob(self, pattern, *, case_sensitive=None):
823827
"""Iterate over this subtree and yield all existing files (of any
824828
kind, including directories) matching the given relative pattern.
825829
"""
@@ -831,11 +835,11 @@ def glob(self, pattern):
831835
raise NotImplementedError("Non-relative patterns are unsupported")
832836
if pattern[-1] in (self._flavour.sep, self._flavour.altsep):
833837
pattern_parts.append('')
834-
selector = _make_selector(tuple(pattern_parts), self._flavour)
838+
selector = _make_selector(tuple(pattern_parts), self._flavour, case_sensitive)
835839
for p in selector.select_from(self):
836840
yield p
837841

838-
def rglob(self, pattern):
842+
def rglob(self, pattern, *, case_sensitive=None):
839843
"""Recursively yield all existing files (of any kind, including
840844
directories) matching the given relative pattern, anywhere in
841845
this subtree.
@@ -846,7 +850,7 @@ def rglob(self, pattern):
846850
raise NotImplementedError("Non-relative patterns are unsupported")
847851
if pattern and pattern[-1] in (self._flavour.sep, self._flavour.altsep):
848852
pattern_parts.append('')
849-
selector = _make_selector(("**",) + tuple(pattern_parts), self._flavour)
853+
selector = _make_selector(("**",) + tuple(pattern_parts), self._flavour, case_sensitive)
850854
for p in selector.select_from(self):
851855
yield p
852856

Lib/test/test_pathlib.py

+12
Original file line numberDiff line numberDiff line change
@@ -1816,6 +1816,18 @@ def _check(glob, expected):
18161816
else:
18171817
_check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE", "linkB"])
18181818

1819+
def test_glob_case_sensitive(self):
1820+
P = self.cls
1821+
def _check(path, pattern, case_sensitive, expected):
1822+
actual = {str(q) for q in path.glob(pattern, case_sensitive=case_sensitive)}
1823+
expected = {str(P(BASE, q)) for q in expected}
1824+
self.assertEqual(actual, expected)
1825+
path = P(BASE)
1826+
_check(path, "DIRB/FILE*", True, [])
1827+
_check(path, "DIRB/FILE*", False, ["dirB/fileB"])
1828+
_check(path, "dirb/file*", True, [])
1829+
_check(path, "dirb/file*", False, ["dirB/fileB"])
1830+
18191831
def test_rglob_common(self):
18201832
def _check(glob, expected):
18211833
self.assertEqual(set(glob), { P(BASE, q) for q in expected })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add *case_sensitive* keyword-only argument to :meth:`pathlib.Path.glob` and
2+
:meth:`~pathlib.Path.rglob`.

0 commit comments

Comments
 (0)