diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 708a16e6bc8c94..257ac5cda8edc6 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -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 ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index ac5b53ef94bfb1..d413ae180eefd9 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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: diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index cd28f62ce3baf5..b42333ece22cb4 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -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()) @@ -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 diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index d1bb8701b887c8..e8b2954ebb7f3c 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -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: ... @@ -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. @@ -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): @@ -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 diff --git a/Lib/test/test_pathlib/support/lexical_path.py b/Lib/test/test_pathlib/support/lexical_path.py index f29a521af9b013..a3d6f0e1a6ff82 100644 --- a/Lib/test/test_pathlib/support/lexical_path.py +++ b/Lib/test/test_pathlib/support/lexical_path.py @@ -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)) @@ -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})' diff --git a/Lib/test/test_pathlib/support/zip_path.py b/Lib/test/test_pathlib/support/zip_path.py index 242cab1509627b..e065b712b7031f 100644 --- a/Lib/test/test_pathlib/support/zip_path.py +++ b/Lib/test/test_pathlib/support/zip_path.py @@ -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) @@ -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})' @@ -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): @@ -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})' diff --git a/Lib/test/test_pathlib/test_join.py b/Lib/test/test_pathlib/test_join.py index f1a24204b4c30a..3a63eec7ec81d3 100644 --- a/Lib/test/test_pathlib/test_join.py +++ b/Lib/test/test_pathlib/test_join.py @@ -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): diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index b1fcc5f6f0538e..7317dcb7fb1029 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -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('') @@ -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 diff --git a/Misc/NEWS.d/next/Library/2025-03-30-20-25-58.gh-issue-131916.p8bRiF.rst b/Misc/NEWS.d/next/Library/2025-03-30-20-25-58.gh-issue-131916.p8bRiF.rst new file mode 100644 index 00000000000000..0229c13dc9c8c1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-30-20-25-58.gh-issue-131916.p8bRiF.rst @@ -0,0 +1,2 @@ +Add :attr:`pathlib.PurePath.segments`, which stores the flattened path +segments given to the path object initializer.