GH-127807: pathlib ABCs: remove a few private attributes (#127851)

From `PurePathBase` delete `_globber`, `_stack` and `_pattern_str`, and
from `PathBase` delete `_glob_selector`. This helps avoid an unpleasant
surprise for a users who try to use these names.
This commit is contained in:
Barney Gale 2024-12-22 01:41:38 +00:00 committed by GitHub
parent a959ea1b0a
commit f5ba74b819
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 56 deletions

View File

@ -25,6 +25,23 @@ def _is_case_sensitive(parser):
return parser.normcase('Aa') == 'Aa' return parser.normcase('Aa') == 'Aa'
def _explode_path(path):
"""
Split the path into a 2-tuple (anchor, parts), where *anchor* is the
uppermost parent of the path (equivalent to path.parents[-1]), and
*parts* is a reversed list of parts following the anchor.
"""
split = path.parser.split
path = str(path)
parent, name = split(path)
names = []
while path != parent:
names.append(name)
path = parent
parent, name = split(path)
return path, names
class PathGlobber(_GlobberBase): class PathGlobber(_GlobberBase):
""" """
Class providing shell-style globbing for path objects. Class providing shell-style globbing for path objects.
@ -50,7 +67,6 @@ class PurePathBase:
__slots__ = () __slots__ = ()
parser = posixpath parser = posixpath
_globber = PathGlobber
def with_segments(self, *pathsegments): def with_segments(self, *pathsegments):
"""Construct a new path object from any number of path-like objects. """Construct a new path object from any number of path-like objects.
@ -82,7 +98,7 @@ def root(self):
@property @property
def anchor(self): def anchor(self):
"""The concatenation of the drive and root, or ''.""" """The concatenation of the drive and root, or ''."""
return self._stack[0] return _explode_path(self)[0]
@property @property
def name(self): def name(self):
@ -160,8 +176,8 @@ def relative_to(self, other, *, walk_up=False):
""" """
if not isinstance(other, PurePathBase): if not isinstance(other, PurePathBase):
other = self.with_segments(other) other = self.with_segments(other)
anchor0, parts0 = self._stack anchor0, parts0 = _explode_path(self)
anchor1, parts1 = other._stack anchor1, parts1 = _explode_path(other)
if anchor0 != anchor1: if anchor0 != anchor1:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors") raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
while parts0 and parts1 and parts0[-1] == parts1[-1]: while parts0 and parts1 and parts0[-1] == parts1[-1]:
@ -183,8 +199,8 @@ def is_relative_to(self, other):
""" """
if not isinstance(other, PurePathBase): if not isinstance(other, PurePathBase):
other = self.with_segments(other) other = self.with_segments(other)
anchor0, parts0 = self._stack anchor0, parts0 = _explode_path(self)
anchor1, parts1 = other._stack anchor1, parts1 = _explode_path(other)
if anchor0 != anchor1: if anchor0 != anchor1:
return False return False
while parts0 and parts1 and parts0[-1] == parts1[-1]: while parts0 and parts1 and parts0[-1] == parts1[-1]:
@ -199,7 +215,7 @@ def is_relative_to(self, other):
def parts(self): def parts(self):
"""An object providing sequence-like access to the """An object providing sequence-like access to the
components in the filesystem path.""" components in the filesystem path."""
anchor, parts = self._stack anchor, parts = _explode_path(self)
if anchor: if anchor:
parts.append(anchor) parts.append(anchor)
return tuple(reversed(parts)) return tuple(reversed(parts))
@ -224,23 +240,6 @@ def __rtruediv__(self, key):
except TypeError: except TypeError:
return NotImplemented return NotImplemented
@property
def _stack(self):
"""
Split the path into a 2-tuple (anchor, parts), where *anchor* is the
uppermost parent of the path (equivalent to path.parents[-1]), and
*parts* is a reversed list of parts following the anchor.
"""
split = self.parser.split
path = str(self)
parent, name = split(path)
names = []
while path != parent:
names.append(name)
path = parent
parent, name = split(path)
return path, names
@property @property
def parent(self): def parent(self):
"""The logical parent of the path.""" """The logical parent of the path."""
@ -268,11 +267,6 @@ def is_absolute(self):
a drive).""" a drive)."""
return self.parser.isabs(str(self)) return self.parser.isabs(str(self))
@property
def _pattern_str(self):
"""The path expressed as a string, for use in pattern-matching."""
return str(self)
def match(self, path_pattern, *, case_sensitive=None): def match(self, path_pattern, *, case_sensitive=None):
""" """
Return True if this path matches the given pattern. If the pattern is Return True if this path matches the given pattern. If the pattern is
@ -293,7 +287,7 @@ def match(self, path_pattern, *, case_sensitive=None):
return False return False
if len(path_parts) > len(pattern_parts) and path_pattern.anchor: if len(path_parts) > len(pattern_parts) and path_pattern.anchor:
return False return False
globber = self._globber(sep, case_sensitive) globber = PathGlobber(sep, case_sensitive)
for path_part, pattern_part in zip(path_parts, pattern_parts): for path_part, pattern_part in zip(path_parts, pattern_parts):
match = globber.compile(pattern_part) match = globber.compile(pattern_part)
if match(path_part) is None: if match(path_part) is None:
@ -309,9 +303,9 @@ def full_match(self, pattern, *, case_sensitive=None):
pattern = self.with_segments(pattern) pattern = self.with_segments(pattern)
if case_sensitive is None: if case_sensitive is None:
case_sensitive = _is_case_sensitive(self.parser) case_sensitive = _is_case_sensitive(self.parser)
globber = self._globber(pattern.parser.sep, case_sensitive, recursive=True) globber = PathGlobber(pattern.parser.sep, case_sensitive, recursive=True)
match = globber.compile(pattern._pattern_str) match = globber.compile(str(pattern))
return match(self._pattern_str) is not None return match(str(self)) is not None
@ -463,29 +457,25 @@ def iterdir(self):
""" """
raise NotImplementedError raise NotImplementedError
def _glob_selector(self, parts, case_sensitive, recurse_symlinks):
if case_sensitive is None:
case_sensitive = _is_case_sensitive(self.parser)
case_pedantic = False
else:
# The user has expressed a case sensitivity choice, but we don't
# know the case sensitivity of the underlying filesystem, so we
# must use scandir() for everything, including non-wildcard parts.
case_pedantic = True
recursive = True if recurse_symlinks else _no_recurse_symlinks
globber = self._globber(self.parser.sep, case_sensitive, case_pedantic, recursive)
return globber.selector(parts)
def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=True): def glob(self, pattern, *, case_sensitive=None, 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.
""" """
if not isinstance(pattern, PurePathBase): if not isinstance(pattern, PurePathBase):
pattern = self.with_segments(pattern) pattern = self.with_segments(pattern)
anchor, parts = pattern._stack anchor, parts = _explode_path(pattern)
if anchor: if anchor:
raise NotImplementedError("Non-relative patterns are unsupported") raise NotImplementedError("Non-relative patterns are unsupported")
select = self._glob_selector(parts, case_sensitive, recurse_symlinks) if case_sensitive is None:
case_sensitive = _is_case_sensitive(self.parser)
case_pedantic = False
elif case_sensitive == _is_case_sensitive(self.parser):
case_pedantic = False
else:
case_pedantic = True
recursive = True if recurse_symlinks else _no_recurse_symlinks
globber = PathGlobber(self.parser.sep, case_sensitive, case_pedantic, recursive)
select = globber.selector(parts)
return select(self) return select(self)
def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=True): def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):

View File

@ -5,7 +5,7 @@
import posixpath import posixpath
import sys import sys
from errno import EINVAL, EXDEV from errno import EINVAL, EXDEV
from glob import _StringGlobber from glob import _StringGlobber, _no_recurse_symlinks
from itertools import chain from itertools import chain
from stat import S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO from stat import S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
from _collections_abc import Sequence from _collections_abc import Sequence
@ -112,7 +112,6 @@ class PurePath(PurePathBase):
'_hash', '_hash',
) )
parser = os.path parser = os.path
_globber = _StringGlobber
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
"""Construct a PurePath from one or several strings and or existing """Construct a PurePath from one or several strings and or existing
@ -513,13 +512,22 @@ def as_uri(self):
from urllib.parse import quote_from_bytes from urllib.parse import quote_from_bytes
return prefix + quote_from_bytes(os.fsencode(path)) return prefix + quote_from_bytes(os.fsencode(path))
@property def full_match(self, pattern, *, case_sensitive=None):
def _pattern_str(self): """
"""The path expressed as a string, for use in pattern-matching.""" Return True if this path matches the given glob-style pattern. The
pattern is matched against the entire path.
"""
if not isinstance(pattern, PurePathBase):
pattern = self.with_segments(pattern)
if case_sensitive is None:
case_sensitive = self.parser is posixpath
# The string representation of an empty path is a single dot ('.'). Empty # The string representation of an empty path is a single dot ('.'). Empty
# paths shouldn't match wildcards, so we change it to the empty string. # paths shouldn't match wildcards, so we change it to the empty string.
path_str = str(self) path = str(self) if self.parts else ''
return '' if path_str == '.' else path_str pattern = str(pattern) if pattern.parts else ''
globber = _StringGlobber(self.parser.sep, case_sensitive, recursive=True)
return globber.compile(pattern)(path) is not None
# Subclassing os.PathLike makes isinstance() checks slower, # Subclassing os.PathLike makes isinstance() checks slower,
# which in turn makes Path construction slower. Register instead! # which in turn makes Path construction slower. Register instead!
@ -749,8 +757,18 @@ def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
kind, including directories) matching the given relative pattern. kind, including directories) matching the given relative pattern.
""" """
sys.audit("pathlib.Path.glob", self, pattern) sys.audit("pathlib.Path.glob", self, pattern)
if case_sensitive is None:
case_sensitive = self.parser is posixpath
case_pedantic = False
else:
# The user has expressed a case sensitivity choice, but we don't
# know the case sensitivity of the underlying filesystem, so we
# must use scandir() for everything, including non-wildcard parts.
case_pedantic = True
parts = self._parse_pattern(pattern) parts = self._parse_pattern(pattern)
select = self._glob_selector(parts[::-1], case_sensitive, recurse_symlinks) recursive = True if recurse_symlinks else _no_recurse_symlinks
globber = _StringGlobber(self.parser.sep, case_sensitive, case_pedantic, recursive)
select = globber.selector(parts[::-1])
root = str(self) root = str(self)
paths = select(root) paths = select(root)