From a6fdd0e3050b157290f11550747b22b4f08f75b7 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sun, 20 Nov 2022 00:34:21 +0000 Subject: [PATCH 01/17] Add `pathlib.PurePath.makepath()`; unify path object construction --- Doc/library/pathlib.rst | 30 ++++++++- Lib/pathlib.py | 137 ++++++++++++--------------------------- Lib/test/test_pathlib.py | 45 +++++++++++++ 3 files changed, 113 insertions(+), 99 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 47687400c14e3a..f9e866a54032df 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -351,8 +351,7 @@ Pure paths provide the following methods and properties: .. data:: PurePath.parents - An immutable sequence providing access to the logical ancestors of - the path:: + A tuple providing access to the logical ancestors of the path:: >>> p = PureWindowsPath('c:/foo/bar/setup.py') >>> p.parents[0] @@ -365,6 +364,9 @@ Pure paths provide the following methods and properties: .. versionchanged:: 3.10 The parents sequence now supports :term:`slices ` and negative index values. + .. versionchanged:: 3.12 + Type changed from a tuple-like immutable sequence to a true tuple. + .. data:: PurePath.parent The logical parent of the path:: @@ -537,6 +539,30 @@ Pure paths provide the following methods and properties: PureWindowsPath('c:/Program Files') +.. method:: PurePath.makepath(*other) + + Create a new path object of the same type by combining the *other* + arguments. This method is called whenever a derivative path is created, + such as from :data:`parent` and :method:`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, *args, session_id): + super().__init__(*args) + self.session_id = session_id + + def makepath(self, *other): + return type(self)(*other, session_id=self.session_id) + + etc = MyPath('/etc', session_id=42) + hosts = etc / 'hosts' + print(hosts.session_id) # 42 + + .. versionadded:: 3.12 + + .. method:: PurePath.match(pattern) Match this path against the provided glob-style pattern. Return ``True`` diff --git a/Lib/pathlib.py b/Lib/pathlib.py index b959e85d18406a..e04dd1e63ac655 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -14,7 +14,6 @@ import re import sys import warnings -from _collections_abc import Sequence from errno import ENOENT, ENOTDIR, EBADF, ELOOP from operator import attrgetter from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO @@ -121,7 +120,7 @@ def __init__(self, name, child_parts, flavour): def _select_from(self, parent_path, is_dir, exists, scandir, normcase): try: - path = parent_path._make_child_relpath(self.name) + path = parent_path.joinpath(self.name) if (is_dir if self.dironly else exists)(path): for p in self.successor._select_from(path, is_dir, exists, scandir, normcase): yield p @@ -155,7 +154,7 @@ def _select_from(self, parent_path, is_dir, exists, scandir, normcase): continue name = entry.name if self.match(normcase(name)): - path = parent_path._make_child_relpath(name) + path = parent_path.joinpath(name) for p in self.successor._select_from(path, is_dir, exists, scandir, normcase): yield p except PermissionError: @@ -182,7 +181,7 @@ def _iterate_directories(self, parent_path, is_dir, scandir): if not _ignore_error(e): raise if entry_is_dir and not entry.is_symlink(): - path = parent_path._make_child_relpath(entry.name) + path = parent_path.joinpath(entry.name) for p in self._iterate_directories(path, is_dir, scandir): yield p except PermissionError: @@ -208,38 +207,6 @@ def _select_from(self, parent_path, is_dir, exists, scandir, normcase): # Public API # -class _PathParents(Sequence): - """This object provides sequence-like access to the logical ancestors - of a path. Don't try to construct it yourself.""" - __slots__ = ('_pathcls', '_drv', '_root', '_parts') - - def __init__(self, path): - # We don't store the instance to avoid reference cycles - self._pathcls = type(path) - self._drv = path._drv - self._root = path._root - self._parts = path._parts - - def __len__(self): - if self._drv or self._root: - return len(self._parts) - 1 - else: - return len(self._parts) - - def __getitem__(self, idx): - if isinstance(idx, slice): - return tuple(self[i] for i in range(*idx.indices(len(self)))) - - if idx >= len(self) or idx < -len(self): - raise IndexError(idx) - if idx < 0: - idx += len(self) - return self._pathcls._from_parsed_parts(self._drv, self._root, - self._parts[:-idx - 1]) - - def __repr__(self): - return "<{}.parents>".format(self._pathcls.__name__) - class PurePath(object): """Base class for manipulating paths without I/O. @@ -256,7 +223,7 @@ class PurePath(object): ) _flavour = os.path - def __new__(cls, *args): + def __new__(cls, *args, **kwargs): """Construct a PurePath from one or several strings and or existing PurePath objects. The strings and path objects are combined so as to yield a canonicalized path, which is incorporated into the @@ -264,7 +231,7 @@ def __new__(cls, *args): """ if cls is PurePath: cls = PureWindowsPath if os.name == 'nt' else PurePosixPath - return cls._from_parts(args) + return super().__new__(cls) def __reduce__(self): # Using the parts tuple helps share interned path parts @@ -318,24 +285,11 @@ def _parse_args(cls, args): % type(a)) return cls._parse_parts(parts) - @classmethod - def _from_parts(cls, args): - # We need to call _parse_args on the instance, so as to get the - # right flavour. - self = object.__new__(cls) + def __init__(self, *args): drv, root, parts = self._parse_args(args) self._drv = drv self._root = root self._parts = parts - return self - - @classmethod - def _from_parsed_parts(cls, drv, root, parts): - self = object.__new__(cls) - self._drv = drv - self._root = root - self._parts = parts - return self @classmethod def _format_parsed_parts(cls, drv, root, parts): @@ -497,8 +451,7 @@ def with_name(self, name): if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep] or drv or root or len(parts) != 1): raise ValueError("Invalid name %r" % (name)) - return self._from_parsed_parts(self._drv, self._root, - self._parts[:-1] + [name]) + return self.makepath(*self._parts[:-1], name) def with_stem(self, stem): """Return a new path with the stem changed.""" @@ -522,8 +475,7 @@ def with_suffix(self, suffix): name = name + suffix else: name = name[:-len(old_suffix)] + suffix - return self._from_parsed_parts(self._drv, self._root, - self._parts[:-1] + [name]) + return self.makepath(*self._parts[:-1], name) def relative_to(self, other, /, *_deprecated, walk_up=False): """Return the relative path to another path identified by the passed @@ -539,8 +491,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg, remove=(3, 14)) - path_cls = type(self) - other = path_cls(other, *_deprecated) + other = self.makepath(other, *_deprecated) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -549,7 +500,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): if step and not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") parts = ('..',) * step + self.parts[len(path.parts):] - return path_cls(*parts) + return self.makepath(*parts) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -560,7 +511,7 @@ def is_relative_to(self, other, /, *_deprecated): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.is_relative_to(*args)", msg, remove=(3, 14)) - other = type(self)(other, *_deprecated) + other = self.makepath(other, *_deprecated) return other == self or other in self.parents @property @@ -575,28 +526,20 @@ def parts(self): self._parts_tuple = tuple(self._parts) return self._parts_tuple + def makepath(self, *args): + """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)(*args) + def joinpath(self, *args): """Combine this path with one or several arguments, and return a new path representing either a subpath (if all arguments are relative paths) or a totally different path (if one of the arguments is anchored). """ - drv1, root1, parts1 = self._drv, self._root, self._parts - drv2, root2, parts2 = self._parse_args(args) - if root2: - if not drv2 and drv1: - return self._from_parsed_parts(drv1, root2, [drv1 + root2] + parts2[1:]) - else: - return self._from_parsed_parts(drv2, root2, parts2) - elif drv2: - if drv2 == drv1 or self._flavour.normcase(drv2) == self._flavour.normcase(drv1): - # Same drive => second path is relative to the first. - return self._from_parsed_parts(drv1, root1, parts1 + parts2[1:]) - else: - return self._from_parsed_parts(drv2, root2, parts2) - else: - # Second path is non-anchored (common case). - return self._from_parsed_parts(drv1, root1, parts1 + parts2) + return self.makepath(*self._parts, *args) def __truediv__(self, key): try: @@ -606,7 +549,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return self._from_parts([key] + self._parts) + return self.makepath(key, *self._parts) except TypeError: return NotImplemented @@ -618,12 +561,18 @@ def parent(self): parts = self._parts if len(parts) == 1 and (drv or root): return self - return self._from_parsed_parts(drv, root, parts[:-1]) + return self.makepath(*parts[:-1]) @property def parents(self): - """A sequence of this path's logical parents.""" - return _PathParents(self) + """A tuple of this path's logical parents.""" + path = self + parent = self.parent + parents = [] + while path != parent: + parents.append(parent) + path, parent = parent, parent.parent + return tuple(parents) def is_absolute(self): """True if the path is absolute (has both a root and, if applicable, @@ -715,18 +664,12 @@ class Path(PurePath): def __new__(cls, *args, **kwargs): if cls is Path: cls = WindowsPath if os.name == 'nt' else PosixPath - self = cls._from_parts(args) + self = super().__new__(cls) if self._flavour is not os.path: raise NotImplementedError("cannot instantiate %r on your system" % (cls.__name__,)) return self - def _make_child_relpath(self, part): - # This is an optimization used for dir walking. `part` must be - # a single part relative to this path. - parts = self._parts + [part] - return self._from_parsed_parts(self._drv, self._root, parts) - def __enter__(self): # In previous versions of pathlib, __exit__() marked this path as # closed; subsequent attempts to perform I/O would raise an IOError. @@ -751,7 +694,7 @@ def cwd(cls): """Return a new path pointing to the current working directory (as returned by os.getcwd()). """ - return cls(os.getcwd()) + return cls().absolute() @classmethod def home(cls): @@ -768,7 +711,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = self.__class__(other_path).stat() + other_st = self.makepath(other_path).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -778,7 +721,7 @@ def iterdir(self): special entries '.' and '..' are not included. """ for name in os.listdir(self): - yield self._make_child_relpath(name) + yield self.joinpath(name) def _scandir(self): # bpo-24132: a future version of pathlib will support subclassing of @@ -825,7 +768,7 @@ def absolute(self): """ if self.is_absolute(): return self - return self._from_parts([self.cwd()] + self._parts) + return self.makepath(os.getcwd(), self) def resolve(self, strict=False): """ @@ -843,7 +786,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = self._from_parts((s,)) + p = self.makepath(s) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -933,7 +876,7 @@ def readlink(self): """ if not hasattr(os, "readlink"): raise NotImplementedError("os.readlink() not available on this system") - return self._from_parts((os.readlink(self),)) + return self.makepath(os.readlink(self)) def touch(self, mode=0o666, exist_ok=True): """ @@ -1022,7 +965,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return self.__class__(target) + return self.makepath(target) def replace(self, target): """ @@ -1035,7 +978,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return self.__class__(target) + return self.makepath(target) def symlink_to(self, target, target_is_directory=False): """ @@ -1207,7 +1150,7 @@ def expanduser(self): homedir = self._flavour.expanduser(self._parts[0]) if homedir[:1] == "~": raise RuntimeError("Could not determine home directory.") - return self._from_parts([homedir] + self._parts[1:]) + return self.makepath(homedir, *self._parts[1:]) return self @@ -1248,7 +1191,7 @@ def _walk(self, top_down, on_error, follow_symlinks): yield self, dirnames, filenames for dirname in dirnames: - dirpath = self._make_child_relpath(dirname) + dirpath = self.joinpath(dirname) yield from dirpath._walk(top_down, on_error, follow_symlinks) if not top_down: diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 7d4d782cf5f075..78801bb3b8f0ca 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -165,6 +165,15 @@ def test_splitroot(self): # Tests for the pure classes. # +class _BasePurePathSubclass(object): + def __init__(self, *args, session_id): + super().__init__(*args) + self.session_id = session_id + + def makepath(self, *args): + return type(self)(*args, session_id=self.session_id) + + class _BasePurePathTest(object): # Keys are canonical paths, values are list of tuples of arguments @@ -222,6 +231,22 @@ def test_str_subclass_common(self): self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') + def test_makepath_common(self): + class P(_BasePurePathSubclass, self.cls): + pass + p = P('foo', 'bar', session_id=42) + self.assertEqual(42, (p / 'foo').session_id) + self.assertEqual(42, ('foo' / p).session_id) + self.assertEqual(42, p.makepath('foo').session_id) + self.assertEqual(42, p.joinpath('foo').session_id) + self.assertEqual(42, p.with_name('foo').session_id) + self.assertEqual(42, p.with_stem('foo').session_id) + self.assertEqual(42, p.with_suffix('.foo').session_id) + self.assertEqual(42, p.relative_to('foo').session_id) + self.assertEqual(42, p.parent.session_id) + for parent in p.parents: + self.assertEqual(42, parent.session_id) + def test_join_common(self): P = self.cls p = P('a/b') @@ -1595,6 +1620,26 @@ def test_home(self): env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) + def test_makepath(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.makepath('~').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): fileA_path = os.path.join(BASE, 'fileA') fileB_path = os.path.join(BASE, 'dirB', 'fileB') From b0617478a4102d3e7ea25e5ad888231611c52e57 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sat, 24 Dec 2022 17:00:31 +0000 Subject: [PATCH 02/17] Fix reST role name. --- Doc/library/pathlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index f9e866a54032df..a5d40b80fe9e51 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -543,7 +543,7 @@ Pure paths provide the following methods and properties: Create a new path object of the same type by combining the *other* arguments. This method is called whenever a derivative path is created, - such as from :data:`parent` and :method:`relative_to`. Subclasses may + such as from :data:`parent` and :meth:`relative_to`. Subclasses may override this method to pass information to derivative paths, for example:: from pathlib import PurePosixPath From 99eb8b1012a2b5100e8f4a61d3f365c63c376639 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sat, 24 Dec 2022 17:56:18 +0000 Subject: [PATCH 03/17] Move call to `os.getcwd()` back into `Path.cwd()` --- Lib/pathlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index e04dd1e63ac655..06b5c77234c37e 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -694,7 +694,7 @@ def cwd(cls): """Return a new path pointing to the current working directory (as returned by os.getcwd()). """ - return cls().absolute() + return cls(os.getcwd()) @classmethod def home(cls): @@ -768,7 +768,7 @@ def absolute(self): """ if self.is_absolute(): return self - return self.makepath(os.getcwd(), self) + return self.makepath(self.cwd(), *self._parts) def resolve(self, strict=False): """ From 595b8ae873b7be220b0979cb6c3f4592c0027aab Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 3 Apr 2023 22:03:20 +0100 Subject: [PATCH 04/17] Add news blurb. --- .../Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst diff --git a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst new file mode 100644 index 00000000000000..a3a00276e20a21 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst @@ -0,0 +1,4 @@ +Add :meth:`pathlib.PurePath.makepath`, 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 pass information to derivative paths. From 117fe4bbc837ae21d0f97e23271eb5abea840021 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 10 Apr 2023 18:17:00 +0100 Subject: [PATCH 05/17] Add whatsnew entry --- Doc/whatsnew/3.12.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 651caed864fef7..5f987c73ae3ab5 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -267,6 +267,11 @@ inspect pathlib ------- +* Add support for subclassing :class:`pathlib.PurePath` and + :class:`~pathlib.Path`, plus their Posix- and Windows-specific variants. + Subclasses may override the new :meth:`~pathlib.PurePath.makepath` method to + pass information between path instances. + * Add :meth:`~pathlib.Path.walk` for walking the directory trees and generating all file or directory names within them, similar to :func:`os.walk`. (Contributed by Stanislav Zmiev in :gh:`90385`.) From f2f10480e22d4b71acae40a8f32c7e13fcdd33c8 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 24 Apr 2023 23:35:45 +0100 Subject: [PATCH 06/17] other --> pathsegments --- Doc/library/pathlib.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b356f2be97a4cf..78f1558fed44f9 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -532,10 +532,10 @@ Pure paths provide the following methods and properties: unintended effects. -.. method:: PurePath.joinpath(*other) +.. method:: PurePath.joinpath(*pathsegments) 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/passwd') @@ -547,10 +547,10 @@ Pure paths provide the following methods and properties: PureWindowsPath('c:/Program Files') -.. method:: PurePath.makepath(*other) +.. method:: PurePath.makepath(*pathsegments) - Create a new path object of the same type by combining the *other* - arguments. This method is called whenever a derivative path is created, + 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:: @@ -561,8 +561,8 @@ Pure paths provide the following methods and properties: super().__init__(*args) self.session_id = session_id - def makepath(self, *other): - return type(self)(*other, session_id=self.session_id) + def makepath(self, *pathsegments): + return type(self)(*pathsegments, session_id=self.session_id) etc = MyPath('/etc', session_id=42) hosts = etc / 'hosts' From 3c172fb9fba19f29422e1b262abec8d1bdad35e3 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Mon, 24 Apr 2023 23:38:02 +0100 Subject: [PATCH 07/17] Update Lib/pathlib.py Co-authored-by: Alex Waygood --- Lib/pathlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 8afec4597418b1..0d0ca8059c147d 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -576,12 +576,12 @@ def parts(self): else: return tuple(self._tail) - def makepath(self, *args): + def makepath(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)(*args) + return type(self)(*pathsegments) def joinpath(self, *args): """Combine this path with one or several arguments, and return a From 4637109df528563399302a739f50b2a99d661f2b Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 24 Apr 2023 23:49:11 +0100 Subject: [PATCH 08/17] joinpath(*args) --> joinpath(*pathsegments) --- Lib/pathlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 0d0ca8059c147d..1b3da465293892 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -583,13 +583,13 @@ def makepath(self, *pathsegments): """ return type(self)(*pathsegments) - def joinpath(self, *args): + 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 paths) or a totally different path (if one of the arguments is anchored). """ - return self.makepath(self._raw_path, *args) + return self.makepath(self._raw_path, *pathsegments) def __truediv__(self, key): try: From ae4845441281e9b6cf2623388df28587afa239c2 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 25 Apr 2023 12:34:44 +0100 Subject: [PATCH 09/17] Restore _PathParents --- Doc/library/pathlib.rst | 6 ++---- Lib/pathlib.py | 39 ++++++++++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 78f1558fed44f9..426552d9627ae7 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -359,7 +359,8 @@ Pure paths provide the following methods and properties: .. attribute:: PurePath.parents - A tuple providing access to the logical ancestors of the path:: + An immutable sequence providing access to the logical ancestors of + the path:: >>> p = PureWindowsPath('c:/foo/bar/setup.py') >>> p.parents[0] @@ -372,9 +373,6 @@ Pure paths provide the following methods and properties: .. versionchanged:: 3.10 The parents sequence now supports :term:`slices ` and negative index values. - .. versionchanged:: 3.12 - Type changed from a tuple-like immutable sequence to a true tuple. - .. attribute:: PurePath.parent The logical parent of the path:: diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 1b3da465293892..4d191d57062684 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -14,6 +14,7 @@ import re import sys import warnings +from _collections_abc import Sequence from errno import ENOENT, ENOTDIR, EBADF, ELOOP from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO from urllib.parse import quote_from_bytes as urlquote_from_bytes @@ -206,6 +207,35 @@ def _select_from(self, parent_path, is_dir, exists, scandir, normcase): # Public API # +class _PathParents(Sequence): + """This object provides sequence-like access to the logical ancestors + of a path. Don't try to construct it yourself.""" + __slots__ = ('_path', '_drv', '_root', '_tail') + + def __init__(self, path): + self._path = path + self._drv = path.drive + self._root = path.root + self._tail = path._tail + + def __len__(self): + return len(self._tail) + + def __getitem__(self, idx): + if isinstance(idx, slice): + return tuple(self[i] for i in range(*idx.indices(len(self)))) + + if idx >= len(self) or idx < -len(self): + raise IndexError(idx) + if idx < 0: + idx += len(self) + return self._path._from_parsed_parts(self._drv, self._root, + self._tail[:-idx - 1]) + + def __repr__(self): + return "<{}.parents>".format(type(self._path).__name__) + + class PurePath(object): """Base class for manipulating paths without I/O. @@ -615,13 +645,8 @@ def parent(self): @property def parents(self): - """A tuple of this path's logical parents.""" - drv = self.drive - root = self.root - tail = self._tail - return tuple( - self._from_parsed_parts(drv, root, tail[:idx]) - for idx in reversed(range(len(tail)))) + """A sequence of this path's logical parents.""" + return _PathParents(self) def is_absolute(self): """True if the path is absolute (has both a root and, if applicable, From e7a8fe38ed9dc41573577c86c6beea6e4c08d53a Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 25 Apr 2023 16:14:33 +0100 Subject: [PATCH 10/17] Add note to `parents` about potential reference cycle. --- Lib/pathlib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 4d191d57062684..45f289bc23be4b 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -646,6 +646,8 @@ def parent(self): @property def parents(self): """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) def is_absolute(self): From b3bb8bd5dcebeb35a197377d220846f8d33140f3 Mon Sep 17 00:00:00 2001 From: barneygale Date: Fri, 28 Apr 2023 18:50:36 +0100 Subject: [PATCH 11/17] makepath() --> __newpath__() --- Doc/library/pathlib.rst | 48 +++++++++---------- Doc/whatsnew/3.12.rst | 2 +- Lib/pathlib.py | 42 ++++++++-------- Lib/test/test_pathlib.py | 10 ++-- ...-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 4 +- 5 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 426552d9627ae7..aa280df97afbf5 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -545,30 +545,6 @@ Pure paths provide the following methods and properties: PureWindowsPath('c:/Program Files') -.. method:: PurePath.makepath(*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, *args, session_id): - super().__init__(*args) - self.session_id = session_id - - def makepath(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 - - .. method:: PurePath.match(pattern) Match this path against the provided glob-style pattern. Return ``True`` @@ -704,6 +680,30 @@ Pure paths provide the following methods and properties: PureWindowsPath('README') +.. method:: PurePath.__newpath__(*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, *args, session_id): + super().__init__(*args) + self.session_id = session_id + + def __newpath__(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: diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 4fc0b7debd66d3..7c3953aa8cb305 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -280,7 +280,7 @@ pathlib * Add support for subclassing :class:`pathlib.PurePath` and :class:`~pathlib.Path`, plus their Posix- and Windows-specific variants. - Subclasses may override the new :meth:`~pathlib.PurePath.makepath` method to + Subclasses may override the :meth:`~pathlib.PurePath.__newpath__` method to pass information between path instances. * Add :meth:`~pathlib.Path.walk` for walking the directory trees and generating diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 45f289bc23be4b..d05d87bb4c1edd 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -312,6 +312,13 @@ def __init__(self, *args): f"not {type(path).__name__!r}") self._raw_path = path + def __newpath__(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 def _parse_path(cls, path): if not path: @@ -335,7 +342,7 @@ def _load_parts(self): def _from_parsed_parts(self, drv, root, tail): path_str = self._format_parsed_parts(drv, root, tail) - path = self.makepath(path_str) + path = self.__newpath__(path_str) path._str = path_str or '.' path._drv = drv path._root = root @@ -574,7 +581,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg, remove=(3, 14)) - other = self.makepath(other, *_deprecated) + other = self.__newpath__(other, *_deprecated) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -583,7 +590,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): if step and not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") parts = ['..'] * step + self._tail[len(path._tail):] - return self.makepath(*parts) + return self.__newpath__(*parts) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -594,7 +601,7 @@ def is_relative_to(self, other, /, *_deprecated): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.is_relative_to(*args)", msg, remove=(3, 14)) - other = self.makepath(other, *_deprecated) + other = self.__newpath__(other, *_deprecated) return other == self or other in self.parents @property @@ -606,20 +613,13 @@ def parts(self): else: return tuple(self._tail) - def makepath(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 paths) or a totally different path (if one of the arguments is anchored). """ - return self.makepath(self._raw_path, *pathsegments) + return self.__newpath__(self._raw_path, *pathsegments) def __truediv__(self, key): try: @@ -629,7 +629,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return self.makepath(key, self._raw_path) + return self.__newpath__(key, self._raw_path) except TypeError: return NotImplemented @@ -678,7 +678,7 @@ def match(self, path_pattern): """ Return True if this path matches the given pattern. """ - pat = self.makepath(path_pattern) + pat = self.__newpath__(path_pattern) if not pat.parts: raise ValueError("empty pattern") pat_parts = pat._parts_normcase @@ -753,7 +753,7 @@ def _make_child_relpath(self, name): path_str = f'{path_str}{name}' else: path_str = name - path = self.makepath(path_str) + path = self.__newpath__(path_str) path._str = path_str path._drv = self.drive path._root = self.root @@ -803,7 +803,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = self.makepath(other_path).stat() + other_st = self.__newpath__(other_path).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -865,7 +865,7 @@ def absolute(self): cwd = self._flavour.abspath(self.drive) else: cwd = os.getcwd() - return self.makepath(cwd, self._raw_path) + return self.__newpath__(cwd, self._raw_path) def resolve(self, strict=False): """ @@ -883,7 +883,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = self.makepath(s) + p = self.__newpath__(s) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -973,7 +973,7 @@ def readlink(self): """ if not hasattr(os, "readlink"): raise NotImplementedError("os.readlink() not available on this system") - return self.makepath(os.readlink(self)) + return self.__newpath__(os.readlink(self)) def touch(self, mode=0o666, exist_ok=True): """ @@ -1062,7 +1062,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return self.makepath(target) + return self.__newpath__(target) def replace(self, target): """ @@ -1075,7 +1075,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return self.makepath(target) + return self.__newpath__(target) def symlink_to(self, target, target_is_directory=False): """ diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index b1ddb1f7a21782..00a07f788c7ff9 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -33,7 +33,7 @@ def __init__(self, *args, session_id): super().__init__(*args) self.session_id = session_id - def makepath(self, *args): + def __newpath__(self, *args): return type(self)(*args, session_id=self.session_id) @@ -122,13 +122,13 @@ def test_str_subclass_common(self): self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') - def test_makepath_common(self): + def test_newpath_common(self): class P(_BasePurePathSubclass, self.cls): pass p = P('foo', 'bar', session_id=42) self.assertEqual(42, (p / 'foo').session_id) self.assertEqual(42, ('foo' / p).session_id) - self.assertEqual(42, p.makepath('foo').session_id) + self.assertEqual(42, p.__newpath__('foo').session_id) self.assertEqual(42, p.joinpath('foo').session_id) self.assertEqual(42, p.with_name('foo').session_id) self.assertEqual(42, p.with_stem('foo').session_id) @@ -1625,13 +1625,13 @@ def test_home(self): env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) - def test_makepath(self): + def test_newpath(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.makepath('~').expanduser().session_id) + self.assertEqual(42, p.__newpath__('~').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(): diff --git a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst index a3a00276e20a21..da476fad5c2ddf 100644 --- a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst +++ b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst @@ -1,4 +1,4 @@ -Add :meth:`pathlib.PurePath.makepath`, which creates a path object from +Add :meth:`pathlib.PurePath.__newpath__`, 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 pass information to derivative paths. +to share information between path objects. From cdedc926aa4ad6e7031a4520047ae22a99a83a64 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 29 Apr 2023 17:51:07 +0100 Subject: [PATCH 12/17] Look up `__newpath__` on path type. --- Lib/pathlib.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index d05d87bb4c1edd..2e9a30f015be74 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -342,7 +342,7 @@ def _load_parts(self): def _from_parsed_parts(self, drv, root, tail): path_str = self._format_parsed_parts(drv, root, tail) - path = self.__newpath__(path_str) + path = type(self).__newpath__(self, path_str) path._str = path_str or '.' path._drv = drv path._root = root @@ -581,7 +581,8 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg, remove=(3, 14)) - other = self.__newpath__(other, *_deprecated) + path_cls = type(self) + other = path_cls.__newpath__(self, other, *_deprecated) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -590,7 +591,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): if step and not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") parts = ['..'] * step + self._tail[len(path._tail):] - return self.__newpath__(*parts) + return path_cls.__newpath__(self, *parts) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -601,7 +602,7 @@ def is_relative_to(self, other, /, *_deprecated): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.is_relative_to(*args)", msg, remove=(3, 14)) - other = self.__newpath__(other, *_deprecated) + other = type(self).__newpath__(self, other, *_deprecated) return other == self or other in self.parents @property @@ -619,7 +620,7 @@ def joinpath(self, *pathsegments): paths) or a totally different path (if one of the arguments is anchored). """ - return self.__newpath__(self._raw_path, *pathsegments) + return type(self).__newpath__(self, self._raw_path, *pathsegments) def __truediv__(self, key): try: @@ -629,7 +630,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return self.__newpath__(key, self._raw_path) + return type(self).__newpath__(self, key, self._raw_path) except TypeError: return NotImplemented @@ -678,7 +679,7 @@ def match(self, path_pattern): """ Return True if this path matches the given pattern. """ - pat = self.__newpath__(path_pattern) + pat = type(self).__newpath__(self, path_pattern) if not pat.parts: raise ValueError("empty pattern") pat_parts = pat._parts_normcase @@ -753,7 +754,7 @@ def _make_child_relpath(self, name): path_str = f'{path_str}{name}' else: path_str = name - path = self.__newpath__(path_str) + path = type(self).__newpath__(self, path_str) path._str = path_str path._drv = self.drive path._root = self.root @@ -803,7 +804,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = self.__newpath__(other_path).stat() + other_st = type(self).__newpath__(self, other_path).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -865,7 +866,7 @@ def absolute(self): cwd = self._flavour.abspath(self.drive) else: cwd = os.getcwd() - return self.__newpath__(cwd, self._raw_path) + return type(self).__newpath__(self, cwd, self._raw_path) def resolve(self, strict=False): """ @@ -883,7 +884,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = self.__newpath__(s) + p = type(self).__newpath__(self, s) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -973,7 +974,7 @@ def readlink(self): """ if not hasattr(os, "readlink"): raise NotImplementedError("os.readlink() not available on this system") - return self.__newpath__(os.readlink(self)) + return type(self).__newpath__(self, os.readlink(self)) def touch(self, mode=0o666, exist_ok=True): """ @@ -1062,7 +1063,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return self.__newpath__(target) + return type(self).__newpath__(self, target) def replace(self, target): """ @@ -1075,7 +1076,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return self.__newpath__(target) + return type(self).__newpath__(self, target) def symlink_to(self, target, target_is_directory=False): """ From 83415661a03f7285a157a156969dff38d7aea1d2 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 29 Apr 2023 21:40:39 +0100 Subject: [PATCH 13/17] `__newpath__` --> `_newpath_` --- Doc/library/pathlib.rst | 4 +-- Doc/whatsnew/3.12.rst | 2 +- Lib/pathlib.py | 31 +++++++++---------- Lib/test/test_pathlib.py | 6 ++-- ...-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 2 +- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index aa280df97afbf5..063aaacdc25be7 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -680,7 +680,7 @@ Pure paths provide the following methods and properties: PureWindowsPath('README') -.. method:: PurePath.__newpath__(*pathsegments) +.. method:: PurePath._newpath_(*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, @@ -694,7 +694,7 @@ Pure paths provide the following methods and properties: super().__init__(*args) self.session_id = session_id - def __newpath__(self, *pathsegments): + def _newpath_(self, *pathsegments): return type(self)(*pathsegments, session_id=self.session_id) etc = MyPath('/etc', session_id=42) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 7c3953aa8cb305..42d5feb3476bc9 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -280,7 +280,7 @@ 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.__newpath__` method to + Subclasses may override the :meth:`~pathlib.PurePath._newpath_` method to pass information between path instances. * Add :meth:`~pathlib.Path.walk` for walking the directory trees and generating diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 2e9a30f015be74..f0ad166f641259 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -312,7 +312,7 @@ def __init__(self, *args): f"not {type(path).__name__!r}") self._raw_path = path - def __newpath__(self, *pathsegments): + def _newpath_(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()`. @@ -342,7 +342,7 @@ def _load_parts(self): def _from_parsed_parts(self, drv, root, tail): path_str = self._format_parsed_parts(drv, root, tail) - path = type(self).__newpath__(self, path_str) + path = self._newpath_(path_str) path._str = path_str or '.' path._drv = drv path._root = root @@ -581,8 +581,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg, remove=(3, 14)) - path_cls = type(self) - other = path_cls.__newpath__(self, other, *_deprecated) + other = self._newpath_(other, *_deprecated) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -591,7 +590,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): if step and not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") parts = ['..'] * step + self._tail[len(path._tail):] - return path_cls.__newpath__(self, *parts) + return self._newpath_(*parts) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -602,7 +601,7 @@ def is_relative_to(self, other, /, *_deprecated): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.is_relative_to(*args)", msg, remove=(3, 14)) - other = type(self).__newpath__(self, other, *_deprecated) + other = self._newpath_(other, *_deprecated) return other == self or other in self.parents @property @@ -620,7 +619,7 @@ def joinpath(self, *pathsegments): paths) or a totally different path (if one of the arguments is anchored). """ - return type(self).__newpath__(self, self._raw_path, *pathsegments) + return self._newpath_(self._raw_path, *pathsegments) def __truediv__(self, key): try: @@ -630,7 +629,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return type(self).__newpath__(self, key, self._raw_path) + return self._newpath_(key, self._raw_path) except TypeError: return NotImplemented @@ -679,7 +678,7 @@ def match(self, path_pattern): """ Return True if this path matches the given pattern. """ - pat = type(self).__newpath__(self, path_pattern) + pat = self._newpath_(path_pattern) if not pat.parts: raise ValueError("empty pattern") pat_parts = pat._parts_normcase @@ -754,7 +753,7 @@ def _make_child_relpath(self, name): path_str = f'{path_str}{name}' else: path_str = name - path = type(self).__newpath__(self, path_str) + path = self._newpath_(path_str) path._str = path_str path._drv = self.drive path._root = self.root @@ -804,7 +803,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = type(self).__newpath__(self, other_path).stat() + other_st = self._newpath_(other_path).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -866,7 +865,7 @@ def absolute(self): cwd = self._flavour.abspath(self.drive) else: cwd = os.getcwd() - return type(self).__newpath__(self, cwd, self._raw_path) + return self._newpath_(cwd, self._raw_path) def resolve(self, strict=False): """ @@ -884,7 +883,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = type(self).__newpath__(self, s) + p = self._newpath_(s) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -974,7 +973,7 @@ def readlink(self): """ if not hasattr(os, "readlink"): raise NotImplementedError("os.readlink() not available on this system") - return type(self).__newpath__(self, os.readlink(self)) + return self._newpath_(os.readlink(self)) def touch(self, mode=0o666, exist_ok=True): """ @@ -1063,7 +1062,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return type(self).__newpath__(self, target) + return self._newpath_(target) def replace(self, target): """ @@ -1076,7 +1075,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return type(self).__newpath__(self, target) + return self._newpath_(target) def symlink_to(self, target, target_is_directory=False): """ diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 00a07f788c7ff9..cbe545fb3dd08a 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -33,7 +33,7 @@ def __init__(self, *args, session_id): super().__init__(*args) self.session_id = session_id - def __newpath__(self, *args): + def _newpath_(self, *args): return type(self)(*args, session_id=self.session_id) @@ -128,7 +128,7 @@ class P(_BasePurePathSubclass, self.cls): p = P('foo', 'bar', session_id=42) self.assertEqual(42, (p / 'foo').session_id) self.assertEqual(42, ('foo' / p).session_id) - self.assertEqual(42, p.__newpath__('foo').session_id) + self.assertEqual(42, p._newpath_('foo').session_id) self.assertEqual(42, p.joinpath('foo').session_id) self.assertEqual(42, p.with_name('foo').session_id) self.assertEqual(42, p.with_stem('foo').session_id) @@ -1631,7 +1631,7 @@ class P(_BasePurePathSubclass, self.cls): p = P(BASE, session_id=42) self.assertEqual(42, p.absolute().session_id) self.assertEqual(42, p.resolve().session_id) - self.assertEqual(42, p.__newpath__('~').expanduser().session_id) + self.assertEqual(42, p._newpath_('~').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(): diff --git a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst index da476fad5c2ddf..11297a4492f9af 100644 --- a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst +++ b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst @@ -1,4 +1,4 @@ -Add :meth:`pathlib.PurePath.__newpath__`, which creates a path object from +Add :meth:`pathlib.PurePath._newpath_`, 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. From 610b0c469e9f6860a61637b5f742f492ed5f6064 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 1 May 2023 13:39:18 +0100 Subject: [PATCH 14/17] `_newpath_` --> `with_segments` --- Doc/library/pathlib.rst | 4 +-- Doc/whatsnew/3.12.rst | 4 +-- Lib/pathlib.py | 30 +++++++++---------- Lib/test/test_pathlib.py | 10 +++---- ...-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 063aaacdc25be7..523bee9aab0074 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -680,7 +680,7 @@ Pure paths provide the following methods and properties: PureWindowsPath('README') -.. method:: PurePath._newpath_(*pathsegments) +.. 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, @@ -694,7 +694,7 @@ Pure paths provide the following methods and properties: super().__init__(*args) self.session_id = session_id - def _newpath_(self, *pathsegments): + def with_segments(self, *pathsegments): return type(self)(*pathsegments, session_id=self.session_id) etc = MyPath('/etc', session_id=42) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 42d5feb3476bc9..0a67a15661d132 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -280,8 +280,8 @@ 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._newpath_` method to - pass information between path instances. + 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 all file or directory names within them, similar to :func:`os.walk`. diff --git a/Lib/pathlib.py b/Lib/pathlib.py index f0ad166f641259..b1bc50ad2bb8a5 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -312,7 +312,7 @@ def __init__(self, *args): f"not {type(path).__name__!r}") self._raw_path = path - def _newpath_(self, *pathsegments): + 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()`. @@ -342,7 +342,7 @@ def _load_parts(self): def _from_parsed_parts(self, drv, root, tail): path_str = self._format_parsed_parts(drv, root, tail) - path = self._newpath_(path_str) + path = self.with_segments(path_str) path._str = path_str or '.' path._drv = drv path._root = root @@ -581,7 +581,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg, remove=(3, 14)) - other = self._newpath_(other, *_deprecated) + other = self.with_segments(other, *_deprecated) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -590,7 +590,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): if step and not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") parts = ['..'] * step + self._tail[len(path._tail):] - return self._newpath_(*parts) + return self.with_segments(*parts) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -601,7 +601,7 @@ def is_relative_to(self, other, /, *_deprecated): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.is_relative_to(*args)", msg, remove=(3, 14)) - other = self._newpath_(other, *_deprecated) + other = self.with_segments(other, *_deprecated) return other == self or other in self.parents @property @@ -619,7 +619,7 @@ def joinpath(self, *pathsegments): paths) or a totally different path (if one of the arguments is anchored). """ - return self._newpath_(self._raw_path, *pathsegments) + return self.with_segments(self._raw_path, *pathsegments) def __truediv__(self, key): try: @@ -629,7 +629,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return self._newpath_(key, self._raw_path) + return self.with_segments(key, self._raw_path) except TypeError: return NotImplemented @@ -678,7 +678,7 @@ def match(self, path_pattern): """ Return True if this path matches the given pattern. """ - pat = self._newpath_(path_pattern) + pat = self.with_segments(path_pattern) if not pat.parts: raise ValueError("empty pattern") pat_parts = pat._parts_normcase @@ -753,7 +753,7 @@ def _make_child_relpath(self, name): path_str = f'{path_str}{name}' else: path_str = name - path = self._newpath_(path_str) + path = self.with_segments(path_str) path._str = path_str path._drv = self.drive path._root = self.root @@ -803,7 +803,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = self._newpath_(other_path).stat() + other_st = self.with_segments(other_path).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -865,7 +865,7 @@ def absolute(self): cwd = self._flavour.abspath(self.drive) else: cwd = os.getcwd() - return self._newpath_(cwd, self._raw_path) + return self.with_segments(cwd, self._raw_path) def resolve(self, strict=False): """ @@ -883,7 +883,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = self._newpath_(s) + p = self.with_segments(s) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -973,7 +973,7 @@ def readlink(self): """ if not hasattr(os, "readlink"): raise NotImplementedError("os.readlink() not available on this system") - return self._newpath_(os.readlink(self)) + return self.with_segments(os.readlink(self)) def touch(self, mode=0o666, exist_ok=True): """ @@ -1062,7 +1062,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return self._newpath_(target) + return self.with_segments(target) def replace(self, target): """ @@ -1075,7 +1075,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return self._newpath_(target) + return self.with_segments(target) def symlink_to(self, target, target_is_directory=False): """ diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index cbe545fb3dd08a..f125ec6afed5c6 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -33,7 +33,7 @@ def __init__(self, *args, session_id): super().__init__(*args) self.session_id = session_id - def _newpath_(self, *args): + def with_segments(self, *args): return type(self)(*args, session_id=self.session_id) @@ -122,17 +122,17 @@ def test_str_subclass_common(self): self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') - def test_newpath_common(self): + def test_with_segments_common(self): class P(_BasePurePathSubclass, self.cls): pass p = P('foo', 'bar', session_id=42) self.assertEqual(42, (p / 'foo').session_id) self.assertEqual(42, ('foo' / p).session_id) - self.assertEqual(42, p._newpath_('foo').session_id) self.assertEqual(42, p.joinpath('foo').session_id) self.assertEqual(42, p.with_name('foo').session_id) self.assertEqual(42, p.with_stem('foo').session_id) self.assertEqual(42, p.with_suffix('.foo').session_id) + self.assertEqual(42, p.with_segments('foo').session_id) self.assertEqual(42, p.relative_to('foo').session_id) self.assertEqual(42, p.parent.session_id) for parent in p.parents: @@ -1625,13 +1625,13 @@ def test_home(self): env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) - def test_newpath(self): + 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._newpath_('~').expanduser().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(): diff --git a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst index 11297a4492f9af..58db90480d2ff0 100644 --- a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst +++ b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst @@ -1,4 +1,4 @@ -Add :meth:`pathlib.PurePath._newpath_`, which creates a path object from +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. From 7f28ed37eed59a956fe3ead59e466defa38f5936 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 1 May 2023 16:37:25 +0100 Subject: [PATCH 15/17] `with_segments` --> `with_path` --- Doc/library/pathlib.rst | 4 +-- Doc/whatsnew/3.12.rst | 2 +- Lib/pathlib.py | 30 +++++++++---------- Lib/test/test_pathlib.py | 10 +++---- ...-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 523bee9aab0074..842ea199f77232 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -680,7 +680,7 @@ Pure paths provide the following methods and properties: PureWindowsPath('README') -.. method:: PurePath.with_segments(*pathsegments) +.. method:: PurePath.with_path(*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, @@ -694,7 +694,7 @@ Pure paths provide the following methods and properties: super().__init__(*args) self.session_id = session_id - def with_segments(self, *pathsegments): + def with_path(self, *pathsegments): return type(self)(*pathsegments, session_id=self.session_id) etc = MyPath('/etc', session_id=42) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 0a67a15661d132..76cf39b573a940 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -280,7 +280,7 @@ 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 + Subclasses may override the :meth:`~pathlib.PurePath.with_path` method to pass information between path instances. * Add :meth:`~pathlib.Path.walk` for walking the directory trees and generating diff --git a/Lib/pathlib.py b/Lib/pathlib.py index b1bc50ad2bb8a5..e3b02efb87b1b3 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -312,7 +312,7 @@ def __init__(self, *args): f"not {type(path).__name__!r}") self._raw_path = path - def with_segments(self, *pathsegments): + def with_path(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()`. @@ -342,7 +342,7 @@ def _load_parts(self): def _from_parsed_parts(self, drv, root, tail): path_str = self._format_parsed_parts(drv, root, tail) - path = self.with_segments(path_str) + path = self.with_path(path_str) path._str = path_str or '.' path._drv = drv path._root = root @@ -581,7 +581,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg, remove=(3, 14)) - other = self.with_segments(other, *_deprecated) + other = self.with_path(other, *_deprecated) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -590,7 +590,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): if step and not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") parts = ['..'] * step + self._tail[len(path._tail):] - return self.with_segments(*parts) + return self.with_path(*parts) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -601,7 +601,7 @@ def is_relative_to(self, other, /, *_deprecated): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.is_relative_to(*args)", msg, remove=(3, 14)) - other = self.with_segments(other, *_deprecated) + other = self.with_path(other, *_deprecated) return other == self or other in self.parents @property @@ -619,7 +619,7 @@ def joinpath(self, *pathsegments): paths) or a totally different path (if one of the arguments is anchored). """ - return self.with_segments(self._raw_path, *pathsegments) + return self.with_path(self._raw_path, *pathsegments) def __truediv__(self, key): try: @@ -629,7 +629,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return self.with_segments(key, self._raw_path) + return self.with_path(key, self._raw_path) except TypeError: return NotImplemented @@ -678,7 +678,7 @@ def match(self, path_pattern): """ Return True if this path matches the given pattern. """ - pat = self.with_segments(path_pattern) + pat = self.with_path(path_pattern) if not pat.parts: raise ValueError("empty pattern") pat_parts = pat._parts_normcase @@ -753,7 +753,7 @@ def _make_child_relpath(self, name): path_str = f'{path_str}{name}' else: path_str = name - path = self.with_segments(path_str) + path = self.with_path(path_str) path._str = path_str path._drv = self.drive path._root = self.root @@ -803,7 +803,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = self.with_segments(other_path).stat() + other_st = self.with_path(other_path).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -865,7 +865,7 @@ def absolute(self): cwd = self._flavour.abspath(self.drive) else: cwd = os.getcwd() - return self.with_segments(cwd, self._raw_path) + return self.with_path(cwd, self._raw_path) def resolve(self, strict=False): """ @@ -883,7 +883,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = self.with_segments(s) + p = self.with_path(s) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -973,7 +973,7 @@ def readlink(self): """ if not hasattr(os, "readlink"): raise NotImplementedError("os.readlink() not available on this system") - return self.with_segments(os.readlink(self)) + return self.with_path(os.readlink(self)) def touch(self, mode=0o666, exist_ok=True): """ @@ -1062,7 +1062,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return self.with_segments(target) + return self.with_path(target) def replace(self, target): """ @@ -1075,7 +1075,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return self.with_segments(target) + return self.with_path(target) def symlink_to(self, target, target_is_directory=False): """ diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index f125ec6afed5c6..cf9ed948b2678f 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -33,7 +33,7 @@ def __init__(self, *args, session_id): super().__init__(*args) self.session_id = session_id - def with_segments(self, *args): + def with_path(self, *args): return type(self)(*args, session_id=self.session_id) @@ -122,7 +122,7 @@ def test_str_subclass_common(self): self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') - def test_with_segments_common(self): + def test_with_path_common(self): class P(_BasePurePathSubclass, self.cls): pass p = P('foo', 'bar', session_id=42) @@ -132,7 +132,7 @@ class P(_BasePurePathSubclass, self.cls): self.assertEqual(42, p.with_name('foo').session_id) self.assertEqual(42, p.with_stem('foo').session_id) self.assertEqual(42, p.with_suffix('.foo').session_id) - self.assertEqual(42, p.with_segments('foo').session_id) + self.assertEqual(42, p.with_path('foo').session_id) self.assertEqual(42, p.relative_to('foo').session_id) self.assertEqual(42, p.parent.session_id) for parent in p.parents: @@ -1625,13 +1625,13 @@ def test_home(self): env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) - def test_with_segments(self): + def test_with_path(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.with_path('~').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(): diff --git a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst index 58db90480d2ff0..64a63220124270 100644 --- a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst +++ b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst @@ -1,4 +1,4 @@ -Add :meth:`pathlib.PurePath.with_segments`, which creates a path object from +Add :meth:`pathlib.PurePath.with_path`, 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. From 961e2bfa68475a73296d5a9a65cfcb9b31730ea7 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 4 May 2023 19:07:25 +0100 Subject: [PATCH 16/17] Revert "`with_segments` --> `with_path`" This reverts commit 7f28ed37eed59a956fe3ead59e466defa38f5936. --- Doc/library/pathlib.rst | 4 +-- Doc/whatsnew/3.12.rst | 2 +- Lib/pathlib.py | 30 +++++++++---------- Lib/test/test_pathlib.py | 10 +++---- ...-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 842ea199f77232..523bee9aab0074 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -680,7 +680,7 @@ Pure paths provide the following methods and properties: PureWindowsPath('README') -.. method:: PurePath.with_path(*pathsegments) +.. 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, @@ -694,7 +694,7 @@ Pure paths provide the following methods and properties: super().__init__(*args) self.session_id = session_id - def with_path(self, *pathsegments): + def with_segments(self, *pathsegments): return type(self)(*pathsegments, session_id=self.session_id) etc = MyPath('/etc', session_id=42) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index ad5d3038a23da5..c2521bb90bfd14 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -333,7 +333,7 @@ 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_path` method + 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 diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 1714a9e7706d3b..5d866a4d9ab30f 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -321,7 +321,7 @@ def __init__(self, *args): else: self._raw_path = self._flavour.join(*paths) - def with_path(self, *pathsegments): + 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()`. @@ -356,7 +356,7 @@ def _load_parts(self): def _from_parsed_parts(self, drv, root, tail): path_str = self._format_parsed_parts(drv, root, tail) - path = self.with_path(path_str) + path = self.with_segments(path_str) path._str = path_str or '.' path._drv = drv path._root = root @@ -595,7 +595,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg, remove=(3, 14)) - other = self.with_path(other, *_deprecated) + other = self.with_segments(other, *_deprecated) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -604,7 +604,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): if step and not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") parts = ['..'] * step + self._tail[len(path._tail):] - return self.with_path(*parts) + return self.with_segments(*parts) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -615,7 +615,7 @@ def is_relative_to(self, other, /, *_deprecated): "scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath.is_relative_to(*args)", msg, remove=(3, 14)) - other = self.with_path(other, *_deprecated) + other = self.with_segments(other, *_deprecated) return other == self or other in self.parents @property @@ -633,7 +633,7 @@ def joinpath(self, *pathsegments): paths) or a totally different path (if one of the arguments is anchored). """ - return self.with_path(self, *pathsegments) + return self.with_segments(self, *pathsegments) def __truediv__(self, key): try: @@ -643,7 +643,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return self.with_path(key, self) + return self.with_segments(key, self) except TypeError: return NotImplemented @@ -692,7 +692,7 @@ def match(self, path_pattern): """ Return True if this path matches the given pattern. """ - pat = self.with_path(path_pattern) + pat = self.with_segments(path_pattern) if not pat.parts: raise ValueError("empty pattern") pat_parts = pat._parts_normcase @@ -767,7 +767,7 @@ def _make_child_relpath(self, name): path_str = f'{path_str}{name}' else: path_str = name - path = self.with_path(path_str) + path = self.with_segments(path_str) path._str = path_str path._drv = self.drive path._root = self.root @@ -817,7 +817,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = self.with_path(other_path).stat() + other_st = self.with_segments(other_path).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -879,7 +879,7 @@ def absolute(self): cwd = self._flavour.abspath(self.drive) else: cwd = os.getcwd() - return self.with_path(cwd, self) + return self.with_segments(cwd, self) def resolve(self, strict=False): """ @@ -897,7 +897,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = self.with_path(s) + p = self.with_segments(s) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -987,7 +987,7 @@ def readlink(self): """ if not hasattr(os, "readlink"): raise NotImplementedError("os.readlink() not available on this system") - return self.with_path(os.readlink(self)) + return self.with_segments(os.readlink(self)) def touch(self, mode=0o666, exist_ok=True): """ @@ -1076,7 +1076,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return self.with_path(target) + return self.with_segments(target) def replace(self, target): """ @@ -1089,7 +1089,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return self.with_path(target) + return self.with_segments(target) def symlink_to(self, target, target_is_directory=False): """ diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 2efd07dcb31a74..6b8e926ab6c195 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -33,7 +33,7 @@ def __init__(self, *args, session_id): super().__init__(*args) self.session_id = session_id - def with_path(self, *args): + def with_segments(self, *args): return type(self)(*args, session_id=self.session_id) @@ -122,7 +122,7 @@ def test_str_subclass_common(self): self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') - def test_with_path_common(self): + def test_with_segments_common(self): class P(_BasePurePathSubclass, self.cls): pass p = P('foo', 'bar', session_id=42) @@ -132,7 +132,7 @@ class P(_BasePurePathSubclass, self.cls): self.assertEqual(42, p.with_name('foo').session_id) self.assertEqual(42, p.with_stem('foo').session_id) self.assertEqual(42, p.with_suffix('.foo').session_id) - self.assertEqual(42, p.with_path('foo').session_id) + self.assertEqual(42, p.with_segments('foo').session_id) self.assertEqual(42, p.relative_to('foo').session_id) self.assertEqual(42, p.parent.session_id) for parent in p.parents: @@ -1649,13 +1649,13 @@ def test_home(self): env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) - def test_with_path(self): + 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_path('~').expanduser().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(): diff --git a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst index 64a63220124270..58db90480d2ff0 100644 --- a/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst +++ b/Misc/NEWS.d/next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst @@ -1,4 +1,4 @@ -Add :meth:`pathlib.PurePath.with_path`, which creates a path object from +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. From 4e289443f2726487dcda5b46a687e8905f6b331e Mon Sep 17 00:00:00 2001 From: barneygale Date: Fri, 5 May 2023 19:38:42 +0100 Subject: [PATCH 17/17] `*args` --> `*pathsegments` --- Doc/library/pathlib.rst | 4 ++-- Lib/test/test_pathlib.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 9503841a2cc282..5ffa33d4e61f19 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -690,8 +690,8 @@ Pure paths provide the following methods and properties: from pathlib import PurePosixPath class MyPath(PurePosixPath): - def __init__(self, *args, session_id): - super().__init__(*args) + def __init__(self, *pathsegments, session_id): + super().__init__(*pathsegments) self.session_id = session_id def with_segments(self, *pathsegments): diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 6d3780b01261c3..7586610833b063 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -29,12 +29,12 @@ # class _BasePurePathSubclass(object): - def __init__(self, *args, session_id): - super().__init__(*args) + def __init__(self, *pathsegments, session_id): + super().__init__(*pathsegments) self.session_id = session_id - def with_segments(self, *args): - return type(self)(*args, session_id=self.session_id) + def with_segments(self, *pathsegments): + return type(self)(*pathsegments, session_id=self.session_id) class _BasePurePathTest(object):