Skip to content

Commit 582d778

Browse files
committed
GH-131916: Add pathlib.PurePath.segments
Add `pathlib.PurePath.segments` attribute, which stores the flattened path segments given to the path object initializer. In the pathlib ABCs, add `JoinablePath.segments` as an abstract attribute, and convert `JoinablePath.__str__()` from an abstract method to a mixin method that joins `self.segments`.
1 parent 2c3e3fe commit 582d778

9 files changed

Lines changed: 77 additions & 47 deletions

File tree

Doc/library/pathlib.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,30 @@ property:
298298

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

301+
To access the arguments given to the path initializer, use the following
302+
property:
303+
304+
.. attribute:: PurePath.segments
305+
306+
A tuple of giving access to the path's initializer arguments::
307+
308+
>>> p = PurePath('/usr', 'bin/python3')
309+
>>> p.segments
310+
('/usr', 'bin/python3')
311+
312+
>>> p = PurePath('/usr', PurePath('bin', 'python3'))
313+
>>> p.segments
314+
('/usr', 'bin', 'python3')
315+
316+
(note how nested path objects have their segments merged)
317+
318+
.. note::
319+
320+
Paths with different segments compare equal if their normalized string
321+
representation is the same.
322+
323+
.. versionadded:: next
324+
301325

302326
Methods and properties
303327
^^^^^^^^^^^^^^^^^^^^^^

Doc/whatsnew/3.14.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,11 @@ os
812812
pathlib
813813
-------
814814

815+
* Add :meth:`pathlib.PurePath.segments` attribute that stores the original
816+
arguments given to the path object initializer.
817+
818+
(Contributed by Barney Gale in :gh:`131916`.)
819+
815820
* Add methods to :class:`pathlib.Path` to recursively copy or move files and
816821
directories:
817822

Lib/pathlib/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def __rtruediv__(self, key):
182182
return NotImplemented
183183

184184
def __reduce__(self):
185-
return self.__class__, tuple(self._raw_paths)
185+
return self.__class__, self.segments
186186

187187
def __repr__(self):
188188
return "{}({!r})".format(self.__class__.__name__, self.as_posix())
@@ -327,20 +327,20 @@ def as_posix(self):
327327
slashes."""
328328
return str(self).replace(self.parser.sep, '/')
329329

330+
@property
331+
def segments(self):
332+
"""Sequence of raw path segments supplied to the path initializer.
333+
"""
334+
return tuple(self._raw_paths)
335+
330336
@property
331337
def _raw_path(self):
332338
paths = self._raw_paths
333339
if len(paths) == 1:
334340
return paths[0]
335341
elif paths:
336-
# Join path segments from the initializer.
337-
path = self.parser.join(*paths)
338-
# Cache the joined path.
339-
paths.clear()
340-
paths.append(path)
341-
return path
342+
return self.parser.join(*paths)
342343
else:
343-
paths.append('')
344344
return ''
345345

346346
@property

Lib/pathlib/types.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class _PathParser(Protocol):
4343

4444
sep: str
4545
altsep: Optional[str]
46+
def join(self, path: str, *paths: str) -> str: ...
4647
def split(self, path: str) -> tuple[str, str]: ...
4748
def splitext(self, path: str) -> tuple[str, str]: ...
4849
def normcase(self, path: str) -> str: ...
@@ -76,6 +77,13 @@ def parser(self):
7677
"""
7778
raise NotImplementedError
7879

80+
@property
81+
@abstractmethod
82+
def segments(self):
83+
"""Sequence of raw path segments supplied to the path initializer.
84+
"""
85+
raise NotImplementedError
86+
7987
@abstractmethod
8088
def with_segments(self, *pathsegments):
8189
"""Construct a new path object from any number of path-like objects.
@@ -84,11 +92,11 @@ def with_segments(self, *pathsegments):
8492
"""
8593
raise NotImplementedError
8694

87-
@abstractmethod
8895
def __str__(self):
89-
"""Return the string representation of the path, suitable for
90-
passing to system calls."""
91-
raise NotImplementedError
96+
"""Return the string representation of the path."""
97+
if not self.segments:
98+
return ''
99+
return self.parser.join(*self.segments)
92100

93101
@property
94102
def anchor(self):
@@ -178,17 +186,17 @@ def joinpath(self, *pathsegments):
178186
paths) or a totally different path (if one of the arguments is
179187
anchored).
180188
"""
181-
return self.with_segments(str(self), *pathsegments)
189+
return self.with_segments(*self.segments, *pathsegments)
182190

183191
def __truediv__(self, key):
184192
try:
185-
return self.with_segments(str(self), key)
193+
return self.with_segments(*self.segments, key)
186194
except TypeError:
187195
return NotImplemented
188196

189197
def __rtruediv__(self, key):
190198
try:
191-
return self.with_segments(key, str(self))
199+
return self.with_segments(key, *self.segments)
192200
except TypeError:
193201
return NotImplemented
194202

Lib/test/test_pathlib/support/lexical_path.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515

1616

1717
class LexicalPath(_JoinablePath):
18-
__slots__ = ('_segments',)
18+
__slots__ = ('segments',)
1919
parser = os.path
2020

2121
def __init__(self, *pathsegments):
22-
self._segments = pathsegments
22+
self.segments = pathsegments
2323

2424
def __hash__(self):
2525
return hash(str(self))
@@ -29,11 +29,6 @@ def __eq__(self, other):
2929
return NotImplemented
3030
return str(self) == str(other)
3131

32-
def __str__(self):
33-
if not self._segments:
34-
return ''
35-
return self.parser.join(*self._segments)
36-
3732
def __repr__(self):
3833
return f'{type(self).__name__}({str(self)!r})'
3934

Lib/test/test_pathlib/support/zip_path.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,11 @@ class ReadableZipPath(_ReadablePath):
230230
Simple implementation of a ReadablePath class for .zip files.
231231
"""
232232

233-
__slots__ = ('_segments', 'zip_file')
233+
__slots__ = ('segments', 'zip_file')
234234
parser = posixpath
235235

236236
def __init__(self, *pathsegments, zip_file):
237-
self._segments = pathsegments
237+
self.segments = pathsegments
238238
self.zip_file = zip_file
239239
if not isinstance(zip_file.filelist, ZipFileList):
240240
zip_file.filelist = ZipFileList(zip_file)
@@ -247,11 +247,6 @@ def __eq__(self, other):
247247
return NotImplemented
248248
return str(self) == str(other) and self.zip_file is other.zip_file
249249

250-
def __str__(self):
251-
if not self._segments:
252-
return ''
253-
return self.parser.join(*self._segments)
254-
255250
def __repr__(self):
256251
return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})'
257252

@@ -293,11 +288,11 @@ class WritableZipPath(_WritablePath):
293288
Simple implementation of a WritablePath class for .zip files.
294289
"""
295290

296-
__slots__ = ('_segments', 'zip_file')
291+
__slots__ = ('segments', 'zip_file')
297292
parser = posixpath
298293

299294
def __init__(self, *pathsegments, zip_file):
300-
self._segments = pathsegments
295+
self.segments = pathsegments
301296
self.zip_file = zip_file
302297

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

311-
def __str__(self):
312-
if not self._segments:
313-
return ''
314-
return self.parser.join(*self._segments)
315-
316306
def __repr__(self):
317307
return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})'
318308

Lib/test/test_pathlib/test_join.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ def test_constructor(self):
3131
P('a/b/c')
3232
P('/a/b/c')
3333

34+
def test_segments(self):
35+
P = self.cls
36+
self.assertEqual(P().segments, ())
37+
self.assertEqual(P('a', 'b', 'c').segments, ('a', 'b', 'c'))
38+
self.assertEqual(P('/a', 'b', 'c').segments, ('/a', 'b', 'c'))
39+
self.assertEqual(P('a/b/c').segments, ('a/b/c',))
40+
self.assertEqual(P('/a/b/c').segments, ('/a/b/c',))
41+
3442
def test_with_segments(self):
3543
class P(self.cls):
3644
def __init__(self, *pathsegments, session_id):

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,15 @@ def test_parse_path_common(self):
256256
check('a/./.', '', '', ['a'])
257257
check('/a/b', '', sep, ['a', 'b'])
258258

259+
def test_segments_nested(self):
260+
P = self.cls
261+
P(FakePath("a/b/c"))
262+
self.assertEqual(P(P('a')).segments, ('a',))
263+
self.assertEqual(P(P('a'), 'b').segments, ('a', 'b'))
264+
self.assertEqual(P(P('a'), P('b')).segments, ('a', 'b'))
265+
self.assertEqual(P(P('a'), P('b'), P('c')).segments, ('a', 'b', 'c'))
266+
self.assertEqual(P(P('./a:b')).segments, ('./a:b',))
267+
259268
def test_empty_path(self):
260269
# The empty path points to '.'
261270
p = self.cls('')
@@ -1177,17 +1186,6 @@ def tempdir(self):
11771186
self.addCleanup(os_helper.rmtree, d)
11781187
return d
11791188

1180-
def test_matches_writablepath_docstrings(self):
1181-
path_names = {name for name in dir(pathlib.types._WritablePath) if name[0] != '_'}
1182-
for attr_name in path_names:
1183-
if attr_name == 'parser':
1184-
# On Windows, Path.parser is ntpath, but WritablePath.parser is
1185-
# posixpath, and so their docstrings differ.
1186-
continue
1187-
our_attr = getattr(self.cls, attr_name)
1188-
path_attr = getattr(pathlib.types._WritablePath, attr_name)
1189-
self.assertEqual(our_attr.__doc__, path_attr.__doc__)
1190-
11911189
def test_concrete_class(self):
11921190
if self.cls is pathlib.Path:
11931191
expected = pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :attr:`pathlib.PurePath.segments`, which stores the flattened path
2+
segments given to the path object initializer.

0 commit comments

Comments
 (0)