Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

GH-127807: pathlib ABCs: remove PurePathBase._raw_paths #127883

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 22, 2024
Merged
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
40 changes: 8 additions & 32 deletions 40 Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,49 +44,25 @@ class PurePathBase:
"""Base class for pure path objects.

This class *does not* provide several magic methods that are defined in
its subclass PurePath. They are: __fspath__, __bytes__, __reduce__,
__hash__, __eq__, __lt__, __le__, __gt__, __ge__. Its initializer and path
joining methods accept only strings, not os.PathLike objects more broadly.
its subclass PurePath. They are: __init__, __fspath__, __bytes__,
__reduce__, __hash__, __eq__, __lt__, __le__, __gt__, __ge__.
"""

__slots__ = (
# The `_raw_paths` slot stores unjoined string paths. This is set in
# the `__init__()` method.
'_raw_paths',
)
__slots__ = ()
parser = posixpath
_globber = PathGlobber

def __init__(self, *args):
for arg in args:
if not isinstance(arg, str):
raise TypeError(
f"argument should be a str, not {type(arg).__name__!r}")
self._raw_paths = list(args)

def with_segments(self, *pathsegments):
"""Construct a new path object from any number of path-like objects.
Subclasses may override this method to customize how new path objects
are created from methods like `iterdir()`.
"""
return type(self)(*pathsegments)
raise NotImplementedError

def __str__(self):
"""Return the string representation of the path, suitable for
passing to system calls."""
paths = self._raw_paths
if len(paths) == 1:
return paths[0]
elif paths:
# Join path segments from the initializer.
path = self.parser.join(*paths)
# Cache the joined path.
paths.clear()
paths.append(path)
return path
else:
paths.append('')
return ''
raise NotImplementedError

def as_posix(self):
"""Return the string representation of the path with forward (/)
Expand Down Expand Up @@ -234,17 +210,17 @@ def joinpath(self, *pathsegments):
paths) or a totally different path (if one of the arguments is
anchored).
"""
return self.with_segments(*self._raw_paths, *pathsegments)
return self.with_segments(str(self), *pathsegments)

def __truediv__(self, key):
try:
return self.with_segments(*self._raw_paths, key)
return self.with_segments(str(self), key)
except TypeError:
return NotImplemented

def __rtruediv__(self, key):
try:
return self.with_segments(key, *self._raw_paths)
return self.with_segments(key, str(self))
except TypeError:
return NotImplemented

Expand Down
37 changes: 30 additions & 7 deletions 37 Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ class PurePath(PurePathBase):
"""

__slots__ = (
# The `_raw_paths` slot stores unjoined string paths. This is set in
# the `__init__()` method.
'_raw_paths',

# The `_drv`, `_root` and `_tail_cached` slots store parsed and
# normalized parts of the path. They are set when any of the `drive`,
# `root` or `_tail` properties are accessed for the first time. The
Expand Down Expand Up @@ -140,9 +144,15 @@ def __init__(self, *args):
"object where __fspath__ returns a str, "
f"not {type(path).__name__!r}")
paths.append(path)
# Avoid calling super().__init__, as an optimisation
self._raw_paths = paths

def with_segments(self, *pathsegments):
"""Construct a new path object from any number of path-like objects.
Subclasses may override this method to customize how new path objects
are created from methods like `iterdir()`.
"""
return type(self)(*pathsegments)

def joinpath(self, *pathsegments):
"""Combine this path with one or several arguments, and return a
new path representing either a subpath (if all arguments are relative
Expand Down Expand Up @@ -304,14 +314,29 @@ def _parse_pattern(cls, pattern):
parts.append('')
return parts

@property
def _raw_path(self):
paths = self._raw_paths
if len(paths) == 1:
return paths[0]
elif paths:
# Join path segments from the initializer.
path = self.parser.join(*paths)
# Cache the joined path.
paths.clear()
paths.append(path)
return path
else:
paths.append('')
return ''

@property
def drive(self):
"""The drive prefix (letter or UNC path), if any."""
try:
return self._drv
except AttributeError:
raw_path = PurePathBase.__str__(self)
self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._drv

@property
Expand All @@ -320,17 +345,15 @@ def root(self):
try:
return self._root
except AttributeError:
raw_path = PurePathBase.__str__(self)
self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._root

@property
def _tail(self):
try:
return self._tail_cached
except AttributeError:
raw_path = PurePathBase.__str__(self)
self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._tail_cached

@property
Expand Down
1 change: 0 additions & 1 deletion 1 Lib/pathlib/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class Parser(Protocol):
"""

sep: str
def join(self, path: str, *paths: str) -> str: ...
def split(self, path: str) -> tuple[str, str]: ...
def splitdrive(self, path: str) -> tuple[str, str]: ...
def splitext(self, path: str) -> tuple[str, str]: ...
Expand Down
25 changes: 25 additions & 0 deletions 25 Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,31 @@ def test_fspath_common(self):
self._check_str(p.__fspath__(), ('a/b',))
self._check_str(os.fspath(p), ('a/b',))

def test_bytes(self):
P = self.cls
with self.assertRaises(TypeError):
P(b'a')
with self.assertRaises(TypeError):
P(b'a', 'b')
with self.assertRaises(TypeError):
P('a', b'b')
with self.assertRaises(TypeError):
P('a').joinpath(b'b')
with self.assertRaises(TypeError):
P('a') / b'b'
with self.assertRaises(TypeError):
b'a' / P('b')
with self.assertRaises(TypeError):
P('a').match(b'b')
with self.assertRaises(TypeError):
P('a').relative_to(b'b')
with self.assertRaises(TypeError):
P('a').with_name(b'b')
with self.assertRaises(TypeError):
P('a').with_stem(b'b')
with self.assertRaises(TypeError):
P('a').with_suffix(b'b')

def test_bytes_exc_message(self):
P = self.cls
message = (r"argument should be a str or an os\.PathLike object "
Expand Down
85 changes: 29 additions & 56 deletions 85 Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ def test_parser(self):


class DummyPurePath(PurePathBase):
__slots__ = ()
__slots__ = ('_segments',)

def __init__(self, *segments):
self._segments = segments

def __str__(self):
if self._segments:
return self.parser.join(*self._segments)
return ''

def __eq__(self, other):
if not isinstance(other, DummyPurePath):
Expand All @@ -66,6 +74,9 @@ def __hash__(self):
def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, self.as_posix())

def with_segments(self, *pathsegments):
return type(self)(*pathsegments)


class DummyPurePathTest(unittest.TestCase):
cls = DummyPurePath
Expand Down Expand Up @@ -97,30 +108,11 @@ def test_constructor_common(self):
P('a/b/c')
P('/a/b/c')

def test_bytes(self):
P = self.cls
with self.assertRaises(TypeError):
P(b'a')
with self.assertRaises(TypeError):
P(b'a', 'b')
with self.assertRaises(TypeError):
P('a', b'b')
with self.assertRaises(TypeError):
P('a').joinpath(b'b')
with self.assertRaises(TypeError):
P('a') / b'b'
with self.assertRaises(TypeError):
b'a' / P('b')
with self.assertRaises(TypeError):
P('a').match(b'b')
with self.assertRaises(TypeError):
P('a').relative_to(b'b')
with self.assertRaises(TypeError):
P('a').with_name(b'b')
with self.assertRaises(TypeError):
P('a').with_stem(b'b')
with self.assertRaises(TypeError):
P('a').with_suffix(b'b')
def test_fspath_common(self):
self.assertRaises(TypeError, os.fspath, self.cls(''))

def test_as_bytes_common(self):
self.assertRaises(TypeError, bytes, self.cls(''))

def _check_str_subclass(self, *args):
# Issue #21127: it should be possible to construct a PurePath object
Expand Down Expand Up @@ -1286,36 +1278,6 @@ def test_is_absolute_windows(self):
# Tests for the virtual classes.
#

class PathBaseTest(PurePathBaseTest):
cls = PathBase

def test_not_implemented_error(self):
p = self.cls('')
e = NotImplementedError
self.assertRaises(e, p.stat)
self.assertRaises(e, p.exists)
self.assertRaises(e, p.is_dir)
self.assertRaises(e, p.is_file)
self.assertRaises(e, p.is_symlink)
self.assertRaises(e, p.open)
self.assertRaises(e, p.read_bytes)
self.assertRaises(e, p.read_text)
self.assertRaises(e, p.write_bytes, b'foo')
self.assertRaises(e, p.write_text, 'foo')
self.assertRaises(e, p.iterdir)
self.assertRaises(e, lambda: list(p.glob('*')))
self.assertRaises(e, lambda: list(p.rglob('*')))
self.assertRaises(e, lambda: list(p.walk()))
self.assertRaises(e, p.readlink)
self.assertRaises(e, p.symlink_to, 'foo')
self.assertRaises(e, p.mkdir)

def test_fspath_common(self):
self.assertRaises(TypeError, os.fspath, self.cls(''))

def test_as_bytes_common(self):
self.assertRaises(TypeError, bytes, self.cls(''))


class DummyPathIO(io.BytesIO):
"""
Expand All @@ -1342,11 +1304,19 @@ class DummyPath(PathBase):
Simple implementation of PathBase that keeps files and directories in
memory.
"""
__slots__ = ()
__slots__ = ('_segments')

_files = {}
_directories = {}

def __init__(self, *segments):
self._segments = segments

def __str__(self):
if self._segments:
return self.parser.join(*self._segments)
return ''

def __eq__(self, other):
if not isinstance(other, DummyPath):
return NotImplemented
Expand All @@ -1358,6 +1328,9 @@ def __hash__(self):
def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, self.as_posix())

def with_segments(self, *pathsegments):
return type(self)(*pathsegments)

def stat(self, *, follow_symlinks=True):
path = str(self).rstrip('/')
if path in self._files:
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.