GH-127381: pathlib ABCs: remove case_sensitive argument (#131024)

Remove the *case_sensitive* argument from `_JoinablePath.full_match()` and
`_ReadablePath.glob()`. Using a non-native case sensitivity forces the use
of "case-pedantic" globbing, where we `iterdir()` even for non-wildcard
pattern segments. But it's hard to know when to enable this mode, as
case-sensitivity can vary by directory, so `_PathParser.normcase()` doesn't
always give the full picture. The `Path.glob()` implementation is forced to
make an educated guess, but we can avoid the issue in the ABCs by dropping
the *case_sensitive* argument.

(I probably shouldn't have added these arguments in `PurePath` and `Path`
in the first place!)

Also drop support for `_ReadablePath.glob(recurse_symlinks=False)`, which
makes recursive globbing much slower.
This commit is contained in:
Barney Gale 2025-03-10 17:50:48 +00:00 committed by GitHub
parent c3487c941d
commit 93fc3d34f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 27 additions and 30 deletions

View file

@ -11,7 +11,7 @@ Protocols for supporting classes in pathlib.
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from glob import _PathGlobber, _no_recurse_symlinks from glob import _PathGlobber
from pathlib import PurePath, Path from pathlib import PurePath, Path
from pathlib._os import magic_open, ensure_distinct_paths, copy_file from pathlib._os import magic_open, ensure_distinct_paths, copy_file
from typing import Optional, Protocol, runtime_checkable from typing import Optional, Protocol, runtime_checkable
@ -216,14 +216,13 @@ class _JoinablePath(ABC):
parent = split(path)[0] parent = split(path)[0]
return tuple(parents) return tuple(parents)
def full_match(self, pattern, *, case_sensitive=None): def full_match(self, pattern):
""" """
Return True if this path matches the given glob-style pattern. The Return True if this path matches the given glob-style pattern. The
pattern is matched against the entire path. pattern is matched against the entire path.
""" """
if not hasattr(pattern, 'with_segments'): if not hasattr(pattern, 'with_segments'):
pattern = self.with_segments(pattern) pattern = self.with_segments(pattern)
if case_sensitive is None:
case_sensitive = self.parser.normcase('Aa') == 'Aa' case_sensitive = self.parser.normcase('Aa') == 'Aa'
globber = _PathGlobber(pattern.parser.sep, case_sensitive, recursive=True) globber = _PathGlobber(pattern.parser.sep, case_sensitive, recursive=True)
match = globber.compile(str(pattern), altsep=pattern.parser.altsep) match = globber.compile(str(pattern), altsep=pattern.parser.altsep)
@ -279,7 +278,7 @@ class _ReadablePath(_JoinablePath):
""" """
raise NotImplementedError raise NotImplementedError
def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=True): def glob(self, pattern, *, recurse_symlinks=True):
"""Iterate over this subtree and yield all existing files (of any """Iterate over this subtree and yield all existing files (of any
kind, including directories) matching the given relative pattern. kind, including directories) matching the given relative pattern.
""" """
@ -288,14 +287,10 @@ class _ReadablePath(_JoinablePath):
anchor, parts = _explode_path(pattern) anchor, parts = _explode_path(pattern)
if anchor: if anchor:
raise NotImplementedError("Non-relative patterns are unsupported") raise NotImplementedError("Non-relative patterns are unsupported")
case_sensitive_default = self.parser.normcase('Aa') == 'Aa' elif not recurse_symlinks:
if case_sensitive is None: raise NotImplementedError("recurse_symlinks=False is unsupported")
case_sensitive = case_sensitive_default case_sensitive = self.parser.normcase('Aa') == 'Aa'
case_pedantic = False globber = _PathGlobber(self.parser.sep, case_sensitive, recursive=True)
else:
case_pedantic = case_sensitive_default != case_sensitive
recursive = True if recurse_symlinks else _no_recurse_symlinks
globber = _PathGlobber(self.parser.sep, case_sensitive, case_pedantic, recursive)
select = globber.selector(parts) select = globber.selector(parts)
return select(self.joinpath('')) return select(self.joinpath(''))

View file

@ -130,11 +130,6 @@ class JoinTestBase:
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c./**')) self.assertFalse(P('a/b/c.py').full_match('**/a/b/c./**'))
self.assertFalse(P('a/b/c.py').full_match('/a/b/c.py/**')) self.assertFalse(P('a/b/c.py').full_match('/a/b/c.py/**'))
self.assertFalse(P('a/b/c.py').full_match('/**/a/b/c.py')) self.assertFalse(P('a/b/c.py').full_match('/**/a/b/c.py'))
# Case-sensitive flag
self.assertFalse(P('A.py').full_match('a.PY', case_sensitive=True))
self.assertTrue(P('A.py').full_match('a.PY', case_sensitive=False))
self.assertFalse(P('c:/a/B.Py').full_match('C:/A/*.pY', case_sensitive=True))
self.assertTrue(P('/a/b/c.py').full_match('/A/*/*.Py', case_sensitive=False))
# Matching against empty path # Matching against empty path
self.assertFalse(P('').full_match('*')) self.assertFalse(P('').full_match('*'))
self.assertTrue(P('').full_match('**')) self.assertTrue(P('').full_match('**'))

View file

@ -433,6 +433,13 @@ class PurePathTest(test_pathlib_abc.JoinablePathTest):
with self.assertWarns(DeprecationWarning): with self.assertWarns(DeprecationWarning):
p.is_reserved() p.is_reserved()
def test_full_match_case_sensitive(self):
P = self.cls
self.assertFalse(P('A.py').full_match('a.PY', case_sensitive=True))
self.assertTrue(P('A.py').full_match('a.PY', case_sensitive=False))
self.assertFalse(P('c:/a/B.Py').full_match('C:/A/*.pY', case_sensitive=True))
self.assertTrue(P('/a/b/c.py').full_match('/A/*/*.Py', case_sensitive=False))
def test_match_empty(self): def test_match_empty(self):
P = self.cls P = self.cls
self.assertRaises(ValueError, P('a').match, '') self.assertRaises(ValueError, P('a').match, '')
@ -2737,6 +2744,18 @@ class PathTest(test_pathlib_abc.RWPathTest, PurePathTest):
self.assertEqual(expect, set(p.glob(P(pattern)))) self.assertEqual(expect, set(p.glob(P(pattern))))
self.assertEqual(expect, set(p.glob(FakePath(pattern)))) self.assertEqual(expect, set(p.glob(FakePath(pattern))))
def test_glob_case_sensitive(self):
P = self.cls
def _check(path, pattern, case_sensitive, expected):
actual = {str(q) for q in path.glob(pattern, case_sensitive=case_sensitive)}
expected = {str(P(self.base, q)) for q in expected}
self.assertEqual(actual, expected)
path = P(self.base)
_check(path, "DIRB/FILE*", True, [])
_check(path, "DIRB/FILE*", False, ["dirB/fileB"])
_check(path, "dirb/file*", True, [])
_check(path, "dirb/file*", False, ["dirB/fileB"])
@needs_symlinks @needs_symlinks
def test_glob_dot(self): def test_glob_dot(self):
P = self.cls P = self.cls

View file

@ -709,18 +709,6 @@ class ReadablePathTest(JoinablePathTest):
p = P(self.base) p = P(self.base)
self.assertEqual(list(p.glob("")), [p.joinpath("")]) self.assertEqual(list(p.glob("")), [p.joinpath("")])
def test_glob_case_sensitive(self):
P = self.cls
def _check(path, pattern, case_sensitive, expected):
actual = {str(q) for q in path.glob(pattern, case_sensitive=case_sensitive)}
expected = {str(P(self.base, q)) for q in expected}
self.assertEqual(actual, expected)
path = P(self.base)
_check(path, "DIRB/FILE*", True, [])
_check(path, "DIRB/FILE*", False, ["dirB/fileB"])
_check(path, "dirb/file*", True, [])
_check(path, "dirb/file*", False, ["dirB/fileB"])
def test_info_exists(self): def test_info_exists(self):
p = self.cls(self.base) p = self.cls(self.base)
self.assertTrue(p.info.exists()) self.assertTrue(p.info.exists())