GH-100479: Add pathlib.PurePath.with_segments()
(GH-103975)
Add `pathlib.PurePath.with_segments()`, which creates a path object from arguments. This method is called whenever a derivative path is created, such as from `pathlib.PurePath.parent`. Subclasses may override this method to share information between path objects. Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
1afe0e0320
commit
d00d942149
@ -530,10 +530,10 @@ Pure paths provide the following methods and properties:
|
|||||||
unintended effects.
|
unintended effects.
|
||||||
|
|
||||||
|
|
||||||
.. method:: PurePath.joinpath(*other)
|
.. method:: PurePath.joinpath(*pathsegments)
|
||||||
|
|
||||||
Calling this method is equivalent to combining the path with each of
|
Calling this method is equivalent to combining the path with each of
|
||||||
the *other* arguments in turn::
|
the given *pathsegments* in turn::
|
||||||
|
|
||||||
>>> PurePosixPath('/etc').joinpath('passwd')
|
>>> PurePosixPath('/etc').joinpath('passwd')
|
||||||
PurePosixPath('/etc/passwd')
|
PurePosixPath('/etc/passwd')
|
||||||
@ -680,6 +680,30 @@ Pure paths provide the following methods and properties:
|
|||||||
PureWindowsPath('README')
|
PureWindowsPath('README')
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: PurePath.with_segments(*pathsegments)
|
||||||
|
|
||||||
|
Create a new path object of the same type by combining the given
|
||||||
|
*pathsegments*. This method is called whenever a derivative path is created,
|
||||||
|
such as from :attr:`parent` and :meth:`relative_to`. Subclasses may
|
||||||
|
override this method to pass information to derivative paths, for example::
|
||||||
|
|
||||||
|
from pathlib import PurePosixPath
|
||||||
|
|
||||||
|
class MyPath(PurePosixPath):
|
||||||
|
def __init__(self, *pathsegments, session_id):
|
||||||
|
super().__init__(*pathsegments)
|
||||||
|
self.session_id = session_id
|
||||||
|
|
||||||
|
def with_segments(self, *pathsegments):
|
||||||
|
return type(self)(*pathsegments, session_id=self.session_id)
|
||||||
|
|
||||||
|
etc = MyPath('/etc', session_id=42)
|
||||||
|
hosts = etc / 'hosts'
|
||||||
|
print(hosts.session_id) # 42
|
||||||
|
|
||||||
|
.. versionadded:: 3.12
|
||||||
|
|
||||||
|
|
||||||
.. _concrete-paths:
|
.. _concrete-paths:
|
||||||
|
|
||||||
|
|
||||||
|
@ -348,6 +348,11 @@ inspect
|
|||||||
pathlib
|
pathlib
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
* Add support for subclassing :class:`pathlib.PurePath` and
|
||||||
|
:class:`~pathlib.Path`, plus their Posix- and Windows-specific variants.
|
||||||
|
Subclasses may override the :meth:`~pathlib.PurePath.with_segments` method
|
||||||
|
to pass information between path instances.
|
||||||
|
|
||||||
* Add :meth:`~pathlib.Path.walk` for walking the directory trees and generating
|
* Add :meth:`~pathlib.Path.walk` for walking the directory trees and generating
|
||||||
all file or directory names within them, similar to :func:`os.walk`.
|
all file or directory names within them, similar to :func:`os.walk`.
|
||||||
(Contributed by Stanislav Zmiev in :gh:`90385`.)
|
(Contributed by Stanislav Zmiev in :gh:`90385`.)
|
||||||
|
@ -204,11 +204,10 @@ class _RecursiveWildcardSelector(_Selector):
|
|||||||
class _PathParents(Sequence):
|
class _PathParents(Sequence):
|
||||||
"""This object provides sequence-like access to the logical ancestors
|
"""This object provides sequence-like access to the logical ancestors
|
||||||
of a path. Don't try to construct it yourself."""
|
of a path. Don't try to construct it yourself."""
|
||||||
__slots__ = ('_pathcls', '_drv', '_root', '_tail')
|
__slots__ = ('_path', '_drv', '_root', '_tail')
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
# We don't store the instance to avoid reference cycles
|
self._path = path
|
||||||
self._pathcls = type(path)
|
|
||||||
self._drv = path.drive
|
self._drv = path.drive
|
||||||
self._root = path.root
|
self._root = path.root
|
||||||
self._tail = path._tail
|
self._tail = path._tail
|
||||||
@ -224,11 +223,11 @@ class _PathParents(Sequence):
|
|||||||
raise IndexError(idx)
|
raise IndexError(idx)
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
idx += len(self)
|
idx += len(self)
|
||||||
return self._pathcls._from_parsed_parts(self._drv, self._root,
|
return self._path._from_parsed_parts(self._drv, self._root,
|
||||||
self._tail[:-idx - 1])
|
self._tail[:-idx - 1])
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{}.parents>".format(self._pathcls.__name__)
|
return "<{}.parents>".format(type(self._path).__name__)
|
||||||
|
|
||||||
|
|
||||||
class PurePath(object):
|
class PurePath(object):
|
||||||
@ -316,6 +315,13 @@ class PurePath(object):
|
|||||||
else:
|
else:
|
||||||
self._raw_path = self._flavour.join(*paths)
|
self._raw_path = self._flavour.join(*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)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse_path(cls, path):
|
def _parse_path(cls, path):
|
||||||
if not path:
|
if not path:
|
||||||
@ -342,15 +348,14 @@ class PurePath(object):
|
|||||||
self._root = root
|
self._root = root
|
||||||
self._tail_cached = tail
|
self._tail_cached = tail
|
||||||
|
|
||||||
@classmethod
|
def _from_parsed_parts(self, drv, root, tail):
|
||||||
def _from_parsed_parts(cls, drv, root, tail):
|
path_str = self._format_parsed_parts(drv, root, tail)
|
||||||
path = cls._format_parsed_parts(drv, root, tail)
|
path = self.with_segments(path_str)
|
||||||
self = cls(path)
|
path._str = path_str or '.'
|
||||||
self._str = path or '.'
|
path._drv = drv
|
||||||
self._drv = drv
|
path._root = root
|
||||||
self._root = root
|
path._tail_cached = tail
|
||||||
self._tail_cached = tail
|
return path
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_parsed_parts(cls, drv, root, tail):
|
def _format_parsed_parts(cls, drv, root, tail):
|
||||||
@ -584,8 +589,7 @@ class PurePath(object):
|
|||||||
"scheduled for removal in Python {remove}")
|
"scheduled for removal in Python {remove}")
|
||||||
warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg,
|
warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg,
|
||||||
remove=(3, 14))
|
remove=(3, 14))
|
||||||
path_cls = type(self)
|
other = self.with_segments(other, *_deprecated)
|
||||||
other = path_cls(other, *_deprecated)
|
|
||||||
for step, path in enumerate([other] + list(other.parents)):
|
for step, path in enumerate([other] + list(other.parents)):
|
||||||
if self.is_relative_to(path):
|
if self.is_relative_to(path):
|
||||||
break
|
break
|
||||||
@ -594,7 +598,7 @@ class PurePath(object):
|
|||||||
if step and not walk_up:
|
if step and not walk_up:
|
||||||
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
|
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
|
||||||
parts = ['..'] * step + self._tail[len(path._tail):]
|
parts = ['..'] * step + self._tail[len(path._tail):]
|
||||||
return path_cls(*parts)
|
return self.with_segments(*parts)
|
||||||
|
|
||||||
def is_relative_to(self, other, /, *_deprecated):
|
def is_relative_to(self, other, /, *_deprecated):
|
||||||
"""Return True if the path is relative to another path or False.
|
"""Return True if the path is relative to another path or False.
|
||||||
@ -605,7 +609,7 @@ class PurePath(object):
|
|||||||
"scheduled for removal in Python {remove}")
|
"scheduled for removal in Python {remove}")
|
||||||
warnings._deprecated("pathlib.PurePath.is_relative_to(*args)",
|
warnings._deprecated("pathlib.PurePath.is_relative_to(*args)",
|
||||||
msg, remove=(3, 14))
|
msg, remove=(3, 14))
|
||||||
other = type(self)(other, *_deprecated)
|
other = self.with_segments(other, *_deprecated)
|
||||||
return other == self or other in self.parents
|
return other == self or other in self.parents
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -617,13 +621,13 @@ class PurePath(object):
|
|||||||
else:
|
else:
|
||||||
return tuple(self._tail)
|
return tuple(self._tail)
|
||||||
|
|
||||||
def joinpath(self, *args):
|
def joinpath(self, *pathsegments):
|
||||||
"""Combine this path with one or several arguments, and return a
|
"""Combine this path with one or several arguments, and return a
|
||||||
new path representing either a subpath (if all arguments are relative
|
new path representing either a subpath (if all arguments are relative
|
||||||
paths) or a totally different path (if one of the arguments is
|
paths) or a totally different path (if one of the arguments is
|
||||||
anchored).
|
anchored).
|
||||||
"""
|
"""
|
||||||
return self.__class__(self, *args)
|
return self.with_segments(self, *pathsegments)
|
||||||
|
|
||||||
def __truediv__(self, key):
|
def __truediv__(self, key):
|
||||||
try:
|
try:
|
||||||
@ -633,7 +637,7 @@ class PurePath(object):
|
|||||||
|
|
||||||
def __rtruediv__(self, key):
|
def __rtruediv__(self, key):
|
||||||
try:
|
try:
|
||||||
return type(self)(key, self)
|
return self.with_segments(key, self)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
@ -650,6 +654,8 @@ class PurePath(object):
|
|||||||
@property
|
@property
|
||||||
def parents(self):
|
def parents(self):
|
||||||
"""A sequence of this path's logical parents."""
|
"""A sequence of this path's logical parents."""
|
||||||
|
# The value of this property should not be cached on the path object,
|
||||||
|
# as doing so would introduce a reference cycle.
|
||||||
return _PathParents(self)
|
return _PathParents(self)
|
||||||
|
|
||||||
def is_absolute(self):
|
def is_absolute(self):
|
||||||
@ -680,7 +686,7 @@ class PurePath(object):
|
|||||||
"""
|
"""
|
||||||
Return True if this path matches the given pattern.
|
Return True if this path matches the given pattern.
|
||||||
"""
|
"""
|
||||||
pat = type(self)(path_pattern)
|
pat = self.with_segments(path_pattern)
|
||||||
if not pat.parts:
|
if not pat.parts:
|
||||||
raise ValueError("empty pattern")
|
raise ValueError("empty pattern")
|
||||||
pat_parts = pat._parts_normcase
|
pat_parts = pat._parts_normcase
|
||||||
@ -755,7 +761,7 @@ class Path(PurePath):
|
|||||||
path_str = f'{path_str}{name}'
|
path_str = f'{path_str}{name}'
|
||||||
else:
|
else:
|
||||||
path_str = name
|
path_str = name
|
||||||
path = type(self)(path_str)
|
path = self.with_segments(path_str)
|
||||||
path._str = path_str
|
path._str = path_str
|
||||||
path._drv = self.drive
|
path._drv = self.drive
|
||||||
path._root = self.root
|
path._root = self.root
|
||||||
@ -805,7 +811,7 @@ class Path(PurePath):
|
|||||||
try:
|
try:
|
||||||
other_st = other_path.stat()
|
other_st = other_path.stat()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
other_st = self.__class__(other_path).stat()
|
other_st = self.with_segments(other_path).stat()
|
||||||
return self._flavour.samestat(st, other_st)
|
return self._flavour.samestat(st, other_st)
|
||||||
|
|
||||||
def iterdir(self):
|
def iterdir(self):
|
||||||
@ -867,7 +873,7 @@ class Path(PurePath):
|
|||||||
cwd = self._flavour.abspath(self.drive)
|
cwd = self._flavour.abspath(self.drive)
|
||||||
else:
|
else:
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
return type(self)(cwd, self)
|
return self.with_segments(cwd, self)
|
||||||
|
|
||||||
def resolve(self, strict=False):
|
def resolve(self, strict=False):
|
||||||
"""
|
"""
|
||||||
@ -885,7 +891,7 @@ class Path(PurePath):
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
check_eloop(e)
|
check_eloop(e)
|
||||||
raise
|
raise
|
||||||
p = type(self)(s)
|
p = self.with_segments(s)
|
||||||
|
|
||||||
# In non-strict mode, realpath() doesn't raise on symlink loops.
|
# In non-strict mode, realpath() doesn't raise on symlink loops.
|
||||||
# Ensure we get an exception by calling stat()
|
# Ensure we get an exception by calling stat()
|
||||||
@ -975,7 +981,7 @@ class Path(PurePath):
|
|||||||
"""
|
"""
|
||||||
if not hasattr(os, "readlink"):
|
if not hasattr(os, "readlink"):
|
||||||
raise NotImplementedError("os.readlink() not available on this system")
|
raise NotImplementedError("os.readlink() not available on this system")
|
||||||
return type(self)(os.readlink(self))
|
return self.with_segments(os.readlink(self))
|
||||||
|
|
||||||
def touch(self, mode=0o666, exist_ok=True):
|
def touch(self, mode=0o666, exist_ok=True):
|
||||||
"""
|
"""
|
||||||
@ -1064,7 +1070,7 @@ class Path(PurePath):
|
|||||||
Returns the new Path instance pointing to the target path.
|
Returns the new Path instance pointing to the target path.
|
||||||
"""
|
"""
|
||||||
os.rename(self, target)
|
os.rename(self, target)
|
||||||
return self.__class__(target)
|
return self.with_segments(target)
|
||||||
|
|
||||||
def replace(self, target):
|
def replace(self, target):
|
||||||
"""
|
"""
|
||||||
@ -1077,7 +1083,7 @@ class Path(PurePath):
|
|||||||
Returns the new Path instance pointing to the target path.
|
Returns the new Path instance pointing to the target path.
|
||||||
"""
|
"""
|
||||||
os.replace(self, target)
|
os.replace(self, target)
|
||||||
return self.__class__(target)
|
return self.with_segments(target)
|
||||||
|
|
||||||
def symlink_to(self, target, target_is_directory=False):
|
def symlink_to(self, target, target_is_directory=False):
|
||||||
"""
|
"""
|
||||||
|
@ -29,11 +29,12 @@ except ImportError:
|
|||||||
#
|
#
|
||||||
|
|
||||||
class _BasePurePathSubclass(object):
|
class _BasePurePathSubclass(object):
|
||||||
init_called = False
|
def __init__(self, *pathsegments, session_id):
|
||||||
|
super().__init__(*pathsegments)
|
||||||
|
self.session_id = session_id
|
||||||
|
|
||||||
def __init__(self, *args):
|
def with_segments(self, *pathsegments):
|
||||||
super().__init__(*args)
|
return type(self)(*pathsegments, session_id=self.session_id)
|
||||||
self.init_called = True
|
|
||||||
|
|
||||||
|
|
||||||
class _BasePurePathTest(object):
|
class _BasePurePathTest(object):
|
||||||
@ -121,20 +122,21 @@ class _BasePurePathTest(object):
|
|||||||
self._check_str_subclass('a/b.txt')
|
self._check_str_subclass('a/b.txt')
|
||||||
self._check_str_subclass('/a/b.txt')
|
self._check_str_subclass('/a/b.txt')
|
||||||
|
|
||||||
def test_init_called_common(self):
|
def test_with_segments_common(self):
|
||||||
class P(_BasePurePathSubclass, self.cls):
|
class P(_BasePurePathSubclass, self.cls):
|
||||||
pass
|
pass
|
||||||
p = P('foo', 'bar')
|
p = P('foo', 'bar', session_id=42)
|
||||||
self.assertTrue((p / 'foo').init_called)
|
self.assertEqual(42, (p / 'foo').session_id)
|
||||||
self.assertTrue(('foo' / p).init_called)
|
self.assertEqual(42, ('foo' / p).session_id)
|
||||||
self.assertTrue(p.joinpath('foo').init_called)
|
self.assertEqual(42, p.joinpath('foo').session_id)
|
||||||
self.assertTrue(p.with_name('foo').init_called)
|
self.assertEqual(42, p.with_name('foo').session_id)
|
||||||
self.assertTrue(p.with_stem('foo').init_called)
|
self.assertEqual(42, p.with_stem('foo').session_id)
|
||||||
self.assertTrue(p.with_suffix('.foo').init_called)
|
self.assertEqual(42, p.with_suffix('.foo').session_id)
|
||||||
self.assertTrue(p.relative_to('foo').init_called)
|
self.assertEqual(42, p.with_segments('foo').session_id)
|
||||||
self.assertTrue(p.parent.init_called)
|
self.assertEqual(42, p.relative_to('foo').session_id)
|
||||||
|
self.assertEqual(42, p.parent.session_id)
|
||||||
for parent in p.parents:
|
for parent in p.parents:
|
||||||
self.assertTrue(parent.init_called)
|
self.assertEqual(42, parent.session_id)
|
||||||
|
|
||||||
def _get_drive_root_parts(self, parts):
|
def _get_drive_root_parts(self, parts):
|
||||||
path = self.cls(*parts)
|
path = self.cls(*parts)
|
||||||
@ -1647,6 +1649,26 @@ class _BasePathTest(object):
|
|||||||
env['HOME'] = os.path.join(BASE, 'home')
|
env['HOME'] = os.path.join(BASE, 'home')
|
||||||
self._test_home(self.cls.home())
|
self._test_home(self.cls.home())
|
||||||
|
|
||||||
|
def test_with_segments(self):
|
||||||
|
class P(_BasePurePathSubclass, self.cls):
|
||||||
|
pass
|
||||||
|
p = P(BASE, session_id=42)
|
||||||
|
self.assertEqual(42, p.absolute().session_id)
|
||||||
|
self.assertEqual(42, p.resolve().session_id)
|
||||||
|
self.assertEqual(42, p.with_segments('~').expanduser().session_id)
|
||||||
|
self.assertEqual(42, (p / 'fileA').rename(p / 'fileB').session_id)
|
||||||
|
self.assertEqual(42, (p / 'fileB').replace(p / 'fileA').session_id)
|
||||||
|
if os_helper.can_symlink():
|
||||||
|
self.assertEqual(42, (p / 'linkA').readlink().session_id)
|
||||||
|
for path in p.iterdir():
|
||||||
|
self.assertEqual(42, path.session_id)
|
||||||
|
for path in p.glob('*'):
|
||||||
|
self.assertEqual(42, path.session_id)
|
||||||
|
for path in p.rglob('*'):
|
||||||
|
self.assertEqual(42, path.session_id)
|
||||||
|
for dirpath, dirnames, filenames in p.walk():
|
||||||
|
self.assertEqual(42, dirpath.session_id)
|
||||||
|
|
||||||
def test_samefile(self):
|
def test_samefile(self):
|
||||||
fileA_path = os.path.join(BASE, 'fileA')
|
fileA_path = os.path.join(BASE, 'fileA')
|
||||||
fileB_path = os.path.join(BASE, 'dirB', 'fileB')
|
fileB_path = os.path.join(BASE, 'dirB', 'fileB')
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
Add :meth:`pathlib.PurePath.with_segments`, which creates a path object from
|
||||||
|
arguments. This method is called whenever a derivative path is created, such
|
||||||
|
as from :attr:`pathlib.PurePath.parent`. Subclasses may override this method
|
||||||
|
to share information between path objects.
|
Loading…
x
Reference in New Issue
Block a user