Skip to content

Commit

Permalink
pythonGH-127807: pathlib ABCs: remove PurePathBase._raw_paths
Browse files Browse the repository at this point in the history
Remove the `PurePathBase` initializer, and make `with_segments()` and
`__str__()` abstract. This allows us to drop the `_raw_paths` attribute,
and also the `Parser.join()` protocol method.
  • Loading branch information
barneygale committed Dec 12, 2024
1 parent 7146f18 commit 2e149df
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 93 deletions.
35 changes: 6 additions & 29 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,44 +49,21 @@ class PurePathBase:
joining methods accept only strings, not os.PathLike objects more broadly.
"""

__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 +211,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 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 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 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 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

0 comments on commit 2e149df

Please sign in to comment.