Skip to content
Closed
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
19 changes: 19 additions & 0 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,25 @@ property:

(note how the drive and local root are regrouped in a single part)

To access the arguments given to the path initializer, use the following
property:

.. attribute:: PurePath.segments

A tuple of giving access to the path's initializer arguments::

>>> p = PurePath('/usr', 'bin/python3')
>>> p.segments
('/usr', 'bin/python3')

>>> p = PurePath('/usr', PurePath('bin', 'python3'))
>>> p.segments
('/usr', 'bin', 'python3')

(note how nested path objects have their segments merged)

.. versionadded:: next


Methods and properties
^^^^^^^^^^^^^^^^^^^^^^
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,11 @@ os
pathlib
-------

* Add :meth:`pathlib.PurePath.segments` attribute that stores the original
arguments given to the path object initializer.

(Contributed by Barney Gale in :gh:`131916`.)

* Add methods to :class:`pathlib.Path` to recursively copy or move files and
directories:

Expand Down
16 changes: 8 additions & 8 deletions Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def __rtruediv__(self, key):
return NotImplemented

def __reduce__(self):
return self.__class__, tuple(self._raw_paths)
return self.__class__, self.segments

def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, self.as_posix())
Expand Down Expand Up @@ -327,20 +327,20 @@ def as_posix(self):
slashes."""
return str(self).replace(self.parser.sep, '/')

@property
def segments(self):
"""Sequence of raw path segments supplied to the path initializer.
"""
return tuple(self._raw_paths)

@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
return self.parser.join(*paths)
else:
paths.append('')
return ''

@property
Expand Down
22 changes: 15 additions & 7 deletions Lib/pathlib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class _PathParser(Protocol):

sep: str
altsep: Optional[str]
def join(self, path: str, *paths: str) -> str: ...
def split(self, path: str) -> tuple[str, str]: ...
def splitext(self, path: str) -> tuple[str, str]: ...
def normcase(self, path: str) -> str: ...
Expand Down Expand Up @@ -76,6 +77,13 @@ def parser(self):
"""
raise NotImplementedError

@property
@abstractmethod
def segments(self):
"""Sequence of raw path segments supplied to the path initializer.
"""
raise NotImplementedError

@abstractmethod
def with_segments(self, *pathsegments):
"""Construct a new path object from any number of path-like objects.
Expand All @@ -84,11 +92,11 @@ def with_segments(self, *pathsegments):
"""
raise NotImplementedError

@abstractmethod
def __str__(self):
"""Return the string representation of the path, suitable for
passing to system calls."""
raise NotImplementedError
"""Return the string representation of the path."""
if not self.segments:
return ''
return self.parser.join(*self.segments)

@property
def anchor(self):
Expand Down Expand Up @@ -178,17 +186,17 @@ def joinpath(self, *pathsegments):
paths) or a totally different path (if one of the arguments is
anchored).
"""
return self.with_segments(str(self), *pathsegments)
return self.with_segments(*self.segments, *pathsegments)

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

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

Expand Down
9 changes: 2 additions & 7 deletions Lib/test/test_pathlib/support/lexical_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@


class LexicalPath(_JoinablePath):
__slots__ = ('_segments',)
__slots__ = ('segments',)
parser = os.path

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

def __hash__(self):
return hash(str(self))
Expand All @@ -29,11 +29,6 @@ def __eq__(self, other):
return NotImplemented
return str(self) == str(other)

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

def __repr__(self):
return f'{type(self).__name__}({str(self)!r})'

Expand Down
18 changes: 4 additions & 14 deletions Lib/test/test_pathlib/support/zip_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,11 @@ class ReadableZipPath(_ReadablePath):
Simple implementation of a ReadablePath class for .zip files.
"""

__slots__ = ('_segments', 'zip_file')
__slots__ = ('segments', 'zip_file')
parser = posixpath

def __init__(self, *pathsegments, zip_file):
self._segments = pathsegments
self.segments = pathsegments
self.zip_file = zip_file
if not isinstance(zip_file.filelist, ZipFileList):
zip_file.filelist = ZipFileList(zip_file)
Expand All @@ -247,11 +247,6 @@ def __eq__(self, other):
return NotImplemented
return str(self) == str(other) and self.zip_file is other.zip_file

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

def __repr__(self):
return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})'

Expand Down Expand Up @@ -293,11 +288,11 @@ class WritableZipPath(_WritablePath):
Simple implementation of a WritablePath class for .zip files.
"""

__slots__ = ('_segments', 'zip_file')
__slots__ = ('segments', 'zip_file')
parser = posixpath

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

def __hash__(self):
Expand All @@ -308,11 +303,6 @@ def __eq__(self, other):
return NotImplemented
return str(self) == str(other) and self.zip_file is other.zip_file

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

def __repr__(self):
return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})'

Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_pathlib/test_join.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ def test_constructor(self):
P('a/b/c')
P('/a/b/c')

def test_segments(self):
P = self.cls
self.assertEqual(P().segments, ())
self.assertEqual(P('a', 'b', 'c').segments, ('a', 'b', 'c'))
self.assertEqual(P('/a', 'b', 'c').segments, ('/a', 'b', 'c'))
self.assertEqual(P('a/b/c').segments, ('a/b/c',))
self.assertEqual(P('/a/b/c').segments, ('/a/b/c',))

def test_with_segments(self):
class P(self.cls):
def __init__(self, *pathsegments, session_id):
Expand Down
20 changes: 9 additions & 11 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@ def test_parse_path_common(self):
check('a/./.', '', '', ['a'])
check('/a/b', '', sep, ['a', 'b'])

def test_segments_nested(self):
P = self.cls
P(FakePath("a/b/c"))
self.assertEqual(P(P('a')).segments, ('a',))
self.assertEqual(P(P('a'), 'b').segments, ('a', 'b'))
self.assertEqual(P(P('a'), P('b')).segments, ('a', 'b'))
self.assertEqual(P(P('a'), P('b'), P('c')).segments, ('a', 'b', 'c'))
self.assertEqual(P(P('./a:b')).segments, ('./a:b',))

def test_empty_path(self):
# The empty path points to '.'
p = self.cls('')
Expand Down Expand Up @@ -1177,17 +1186,6 @@ def tempdir(self):
self.addCleanup(os_helper.rmtree, d)
return d

def test_matches_writablepath_docstrings(self):
path_names = {name for name in dir(pathlib.types._WritablePath) if name[0] != '_'}
for attr_name in path_names:
if attr_name == 'parser':
# On Windows, Path.parser is ntpath, but WritablePath.parser is
# posixpath, and so their docstrings differ.
continue
our_attr = getattr(self.cls, attr_name)
path_attr = getattr(pathlib.types._WritablePath, attr_name)
self.assertEqual(our_attr.__doc__, path_attr.__doc__)

def test_concrete_class(self):
if self.cls is pathlib.Path:
expected = pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :attr:`pathlib.PurePath.segments`, which stores the flattened path
segments given to the path object initializer.
Loading