From f5ba74b81979b621e38be70ec3ddad1e7f1365ce Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sun, 22 Dec 2024 01:41:38 +0000 Subject: [PATCH] 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. --- Lib/pathlib/_abc.py | 86 +++++++++++++++++++------------------------ Lib/pathlib/_local.py | 34 +++++++++++++---- 2 files changed, 64 insertions(+), 56 deletions(-) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 4402efe3a02..b521c757561 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -25,6 +25,23 @@ def _is_case_sensitive(parser): 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 providing shell-style globbing for path objects. @@ -50,7 +67,6 @@ class PurePathBase: __slots__ = () parser = posixpath - _globber = PathGlobber def with_segments(self, *pathsegments): """Construct a new path object from any number of path-like objects. @@ -82,7 +98,7 @@ def root(self): @property def anchor(self): """The concatenation of the drive and root, or ''.""" - return self._stack[0] + return _explode_path(self)[0] @property def name(self): @@ -160,8 +176,8 @@ def relative_to(self, other, *, walk_up=False): """ if not isinstance(other, PurePathBase): other = self.with_segments(other) - anchor0, parts0 = self._stack - anchor1, parts1 = other._stack + anchor0, parts0 = _explode_path(self) + anchor1, parts1 = _explode_path(other) if anchor0 != anchor1: raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors") while parts0 and parts1 and parts0[-1] == parts1[-1]: @@ -183,8 +199,8 @@ def is_relative_to(self, other): """ if not isinstance(other, PurePathBase): other = self.with_segments(other) - anchor0, parts0 = self._stack - anchor1, parts1 = other._stack + anchor0, parts0 = _explode_path(self) + anchor1, parts1 = _explode_path(other) if anchor0 != anchor1: return False while parts0 and parts1 and parts0[-1] == parts1[-1]: @@ -199,7 +215,7 @@ def is_relative_to(self, other): def parts(self): """An object providing sequence-like access to the components in the filesystem path.""" - anchor, parts = self._stack + anchor, parts = _explode_path(self) if anchor: parts.append(anchor) return tuple(reversed(parts)) @@ -224,23 +240,6 @@ def __rtruediv__(self, key): except TypeError: 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 def parent(self): """The logical parent of the path.""" @@ -268,11 +267,6 @@ def is_absolute(self): a drive).""" 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): """ 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 if len(path_parts) > len(pattern_parts) and path_pattern.anchor: return False - globber = self._globber(sep, case_sensitive) + globber = PathGlobber(sep, case_sensitive) for path_part, pattern_part in zip(path_parts, pattern_parts): match = globber.compile(pattern_part) if match(path_part) is None: @@ -309,9 +303,9 @@ def full_match(self, pattern, *, case_sensitive=None): pattern = self.with_segments(pattern) if case_sensitive is None: case_sensitive = _is_case_sensitive(self.parser) - globber = self._globber(pattern.parser.sep, case_sensitive, recursive=True) - match = globber.compile(pattern._pattern_str) - return match(self._pattern_str) is not None + globber = PathGlobber(pattern.parser.sep, case_sensitive, recursive=True) + match = globber.compile(str(pattern)) + return match(str(self)) is not None @@ -463,29 +457,25 @@ def iterdir(self): """ 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): """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern. """ if not isinstance(pattern, PurePathBase): pattern = self.with_segments(pattern) - anchor, parts = pattern._stack + anchor, parts = _explode_path(pattern) if anchor: 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) def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=True): diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 7689c10604d..4897149d7e8 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -5,7 +5,7 @@ import posixpath import sys from errno import EINVAL, EXDEV -from glob import _StringGlobber +from glob import _StringGlobber, _no_recurse_symlinks from itertools import chain from stat import S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO from _collections_abc import Sequence @@ -112,7 +112,6 @@ class PurePath(PurePathBase): '_hash', ) parser = os.path - _globber = _StringGlobber def __new__(cls, *args, **kwargs): """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 return prefix + quote_from_bytes(os.fsencode(path)) - @property - def _pattern_str(self): - """The path expressed as a string, for use in pattern-matching.""" + def full_match(self, pattern, *, case_sensitive=None): + """ + 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 # paths shouldn't match wildcards, so we change it to the empty string. - path_str = str(self) - return '' if path_str == '.' else path_str + path = str(self) if self.parts else '' + 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, # 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. """ 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) - 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) paths = select(root)