From a6fdd0e3050b157290f11550747b22b4f08f75b7 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sun, 20 Nov 2022 00:34:21 +0000 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 7f12faa26b8bf0ee3db436499d90b02521a558b2 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 25 Apr 2023 18:05:29 +0100 Subject: [PATCH 11/16] Replace `makepath()` method with `template` initialiser argument. --- Doc/library/pathlib.rst | 51 ++++++++++--------- Doc/whatsnew/3.12.rst | 2 - Lib/pathlib.py | 40 +++++++-------- Lib/test/test_pathlib.py | 19 +++---- ...-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 6 +-- 5 files changed, 56 insertions(+), 62 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 426552d9627ae7..c000747e740f41 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -96,7 +96,7 @@ Pure path objects provide path-handling operations which don't actually access a filesystem. There are three ways to access these classes, which we also call *flavours*: -.. class:: PurePath(*pathsegments) +.. class:: PurePath(*pathsegments, template=None) A generic class that represents the system's path flavour (instantiating it creates either a :class:`PurePosixPath` or a :class:`PureWindowsPath`):: @@ -150,6 +150,31 @@ we also call *flavours*: to ``PurePosixPath('bar')``, which is wrong if ``foo`` is a symbolic link to another directory) + The optional *template* argument may provide another path object. It is + supplied whenever a new path object is created from an existing one, such + as in :attr:`parent` or :meth:`relative_to`. Subclasses may use this to + pass information between path objects. For example:: + + from pathlib import PurePosixPath + + class MyPath(PurePosixPath): + def __init__(self, *pathsegments, template=None, session_id=None): + super().__init__(*pathsegments) + if template: + self.session_id = template.session_id + else: + self.session_id = session_id + + etc = MyPath('/etc', session_id=42) + hosts = etc / 'hosts' + print(hosts.session_id) # 42 + + .. note:: + The classes provided in this module ignore the *template* argument. + + .. versionadded:: 3.12 + The *template* argument. + Pure path objects implement the :class:`os.PathLike` interface, allowing them to be used anywhere the interface is accepted. @@ -545,30 +570,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`` diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 4fc0b7debd66d3..76fb9ea0c21532 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -280,8 +280,6 @@ 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`. diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 45f289bc23be4b..d2c39914dde25d 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -298,7 +298,7 @@ def __reduce__(self): # when pickling related paths. return (self.__class__, self.parts) - def __init__(self, *args): + def __init__(self, *args, template=None): if not args: path = '' elif len(args) == 1: @@ -335,7 +335,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 = type(self)(path_str, template=self) path._str = path_str or '.' path._drv = drv path._root = root @@ -574,7 +574,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.makepath(other, *_deprecated) + path_cls = type(self) + other = path_cls(other, *_deprecated, template=self) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -583,7 +584,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 path_cls(*parts, template=self) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -594,7 +595,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 = type(self)(other, *_deprecated, template=self) return other == self or other in self.parents @property @@ -606,20 +607,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 type(self)(self._raw_path, *pathsegments, template=self) def __truediv__(self, key): try: @@ -629,7 +623,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return self.makepath(key, self._raw_path) + return type(self)(key, self._raw_path, template=self) except TypeError: return NotImplemented @@ -678,7 +672,7 @@ def match(self, path_pattern): """ Return True if this path matches the given pattern. """ - pat = self.makepath(path_pattern) + pat = type(self)(path_pattern, template=self) if not pat.parts: raise ValueError("empty pattern") pat_parts = pat._parts_normcase @@ -732,7 +726,7 @@ class Path(PurePath): """ __slots__ = () - def __init__(self, *args, **kwargs): + def __init__(self, *args, template=None, **kwargs): if kwargs: msg = ("support for supplying keyword arguments to pathlib.PurePath " "is deprecated and scheduled for removal in Python {remove}") @@ -753,7 +747,7 @@ def _make_child_relpath(self, name): path_str = f'{path_str}{name}' else: path_str = name - path = self.makepath(path_str) + path = type(self)(path_str, template=self) path._str = path_str path._drv = self.drive path._root = self.root @@ -803,7 +797,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = self.makepath(other_path).stat() + other_st = type(self)(other_path, template=self).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -865,7 +859,7 @@ def absolute(self): cwd = self._flavour.abspath(self.drive) else: cwd = os.getcwd() - return self.makepath(cwd, self._raw_path) + return type(self)(cwd, self._raw_path, template=self) def resolve(self, strict=False): """ @@ -883,7 +877,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = self.makepath(s) + p = type(self)(s, template=self) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -973,7 +967,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 type(self)(os.readlink(self), template=self) def touch(self, mode=0o666, exist_ok=True): """ @@ -1062,7 +1056,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return self.makepath(target) + return type(self)(target, template=self) def replace(self, target): """ @@ -1075,7 +1069,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return self.makepath(target) + return type(self)(target, template=self) 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..8ca43537ce1357 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): + def __init__(self, *args, template=None, session_id=None): super().__init__(*args) - self.session_id = session_id - - def makepath(self, *args): - return type(self)(*args, session_id=self.session_id) + if template: + self.session_id = template.session_id + else: + self.session_id = session_id class _BasePurePathTest(object): @@ -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_template_common(self): class P(_BasePurePathSubclass, self.cls): pass p = P('foo', 'bar', session_id=42) + self.assertEqual(42, P(template=p).session_id) 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) @@ -1625,13 +1625,14 @@ def test_home(self): env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) - def test_makepath(self): + def test_template(self): class P(_BasePurePathSubclass, self.cls): pass p = P(BASE, session_id=42) + self.assertEqual(42, P(template=p).session_id) 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('~', template=p).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..89dd3c197fdad3 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 -arguments. This method is called whenever a derivative path is created, such -as from :attr:`pathlib.PurePath.parent`. Subclasses may override this method +Add optional *template* argument to :class:`pathlib.PurePath` and +:class:`~pathlib.Path`. This argument is supplied whenever a derivative path +is created, such as from :attr:`pathlib.PurePath.parent`. Subclasses may use to pass information to derivative paths. From 687c7640c04d71438f71cf0d120e1b303d630a0f Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 25 Apr 2023 18:23:54 +0100 Subject: [PATCH 12/16] Apply suggestions from code review Co-authored-by: Alex Waygood --- Doc/library/pathlib.rst | 1 + .../next/Library/2023-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index c000747e740f41..ed050312b8eb4c 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -171,6 +171,7 @@ we also call *flavours*: .. note:: The classes provided in this module ignore the *template* argument. + It is there purely as a hook for user-defined subclasses. .. versionadded:: 3.12 The *template* argument. 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 89dd3c197fdad3..24ca85f4c5f36a 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 optional *template* argument to :class:`pathlib.PurePath` and :class:`~pathlib.Path`. This argument is supplied whenever a derivative path is created, such as from :attr:`pathlib.PurePath.parent`. Subclasses may use -to pass information to derivative paths. +to pass information to derivative paths. Patch by Barney Gale. From d7e326a6f8e52cf9fdea77a76998f06a2d4ec653 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 25 Apr 2023 18:46:28 +0100 Subject: [PATCH 13/16] Fix docs for other classes. --- Doc/library/pathlib.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index ed050312b8eb4c..e51367229823d5 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -182,7 +182,7 @@ we also call *flavours*: .. versionchanged:: 3.6 Added support for the :class:`os.PathLike` interface. -.. class:: PurePosixPath(*pathsegments) +.. class:: PurePosixPath(*pathsegments, template=None) A subclass of :class:`PurePath`, this path flavour represents non-Windows filesystem paths:: @@ -190,9 +190,9 @@ we also call *flavours*: >>> PurePosixPath('/etc') PurePosixPath('/etc') - *pathsegments* is specified similarly to :class:`PurePath`. + *pathsegments* and *template* are specified similarly to :class:`PurePath`. -.. class:: PureWindowsPath(*pathsegments) +.. class:: PureWindowsPath(*pathsegments, template=None) A subclass of :class:`PurePath`, this path flavour represents Windows filesystem paths, including `UNC paths`_:: @@ -202,7 +202,7 @@ we also call *flavours*: >>> PureWindowsPath('//server/share/file') PureWindowsPath('//server/share/file') - *pathsegments* is specified similarly to :class:`PurePath`. + *pathsegments* and *template* are specified similarly to :class:`PurePath`. .. _unc paths: https://en.wikipedia.org/wiki/Path_(computing)#UNC @@ -716,7 +716,7 @@ Concrete paths are subclasses of the pure path classes. In addition to operations provided by the latter, they also provide methods to do system calls on path objects. There are three ways to instantiate concrete paths: -.. class:: Path(*pathsegments) +.. class:: Path(*pathsegments, template=None) A subclass of :class:`PurePath`, this class represents concrete paths of the system's path flavour (instantiating it creates either a @@ -725,9 +725,9 @@ calls on path objects. There are three ways to instantiate concrete paths: >>> Path('setup.py') PosixPath('setup.py') - *pathsegments* is specified similarly to :class:`PurePath`. + *pathsegments* and *template* are specified similarly to :class:`PurePath`. -.. class:: PosixPath(*pathsegments) +.. class:: PosixPath(*pathsegments, template=None) A subclass of :class:`Path` and :class:`PurePosixPath`, this class represents concrete non-Windows filesystem paths:: @@ -735,9 +735,9 @@ calls on path objects. There are three ways to instantiate concrete paths: >>> PosixPath('/etc') PosixPath('/etc') - *pathsegments* is specified similarly to :class:`PurePath`. + *pathsegments* and *template* are specified similarly to :class:`PurePath`. -.. class:: WindowsPath(*pathsegments) +.. class:: WindowsPath(*pathsegments, template=None) A subclass of :class:`Path` and :class:`PureWindowsPath`, this class represents concrete Windows filesystem paths:: @@ -745,7 +745,7 @@ calls on path objects. There are three ways to instantiate concrete paths: >>> WindowsPath('c:/Program Files/') WindowsPath('c:/Program Files') - *pathsegments* is specified similarly to :class:`PurePath`. + *pathsegments* and *template* are specified similarly to :class:`PurePath`. You can only instantiate the class flavour that corresponds to your system (allowing system calls on non-compatible path flavours could lead to From a65d499c5f3efb10b20e6cfe0efa7132566714b7 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 27 Apr 2023 00:12:15 +0100 Subject: [PATCH 14/16] Pass template to `super()` to support diamond inheritance. --- Doc/library/pathlib.rst | 2 +- Lib/test/test_pathlib.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index e51367229823d5..15eb7a8b00250b 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -159,7 +159,7 @@ we also call *flavours*: class MyPath(PurePosixPath): def __init__(self, *pathsegments, template=None, session_id=None): - super().__init__(*pathsegments) + super().__init__(*pathsegments, template=template) if template: self.session_id = template.session_id else: diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 8ca43537ce1357..3d910bf727750b 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -30,7 +30,7 @@ class _BasePurePathSubclass(object): def __init__(self, *args, template=None, session_id=None): - super().__init__(*args) + super().__init__(*args, template=template) if template: self.session_id = template.session_id else: From d4b15d7d1462dbf1534883d01ac547c6525b797f Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 27 Apr 2023 00:21:49 +0100 Subject: [PATCH 15/16] Fixed missed `template` argument to super(). --- Lib/pathlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index d2c39914dde25d..7536019732b7aa 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -731,7 +731,7 @@ def __init__(self, *args, template=None, **kwargs): msg = ("support for supplying keyword arguments to pathlib.PurePath " "is deprecated and scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath(**kwargs)", msg, remove=(3, 14)) - super().__init__(*args) + super().__init__(*args, template=template) def __new__(cls, *args, **kwargs): if cls is Path: From 958b183feaa334a1575e7e91e8f6106eb843e6f7 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 27 Apr 2023 22:06:58 +0100 Subject: [PATCH 16/16] template --> blueprint --- Doc/library/pathlib.rst | 36 +++++++++---------- Lib/pathlib.py | 34 +++++++++--------- Lib/test/test_pathlib.py | 18 +++++----- ...-04-03-22-02-35.gh-issue-100479.kNBjQm.rst | 2 +- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 15eb7a8b00250b..24ad3c40c22ca0 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -96,7 +96,7 @@ Pure path objects provide path-handling operations which don't actually access a filesystem. There are three ways to access these classes, which we also call *flavours*: -.. class:: PurePath(*pathsegments, template=None) +.. class:: PurePath(*pathsegments, blueprint=None) A generic class that represents the system's path flavour (instantiating it creates either a :class:`PurePosixPath` or a :class:`PureWindowsPath`):: @@ -150,7 +150,7 @@ we also call *flavours*: to ``PurePosixPath('bar')``, which is wrong if ``foo`` is a symbolic link to another directory) - The optional *template* argument may provide another path object. It is + The optional *blueprint* argument may provide another path object. It is supplied whenever a new path object is created from an existing one, such as in :attr:`parent` or :meth:`relative_to`. Subclasses may use this to pass information between path objects. For example:: @@ -158,10 +158,10 @@ we also call *flavours*: from pathlib import PurePosixPath class MyPath(PurePosixPath): - def __init__(self, *pathsegments, template=None, session_id=None): - super().__init__(*pathsegments, template=template) - if template: - self.session_id = template.session_id + def __init__(self, *pathsegments, blueprint=None, session_id=None): + super().__init__(*pathsegments, blueprint=blueprint) + if blueprint: + self.session_id = blueprint.session_id else: self.session_id = session_id @@ -170,11 +170,11 @@ we also call *flavours*: print(hosts.session_id) # 42 .. note:: - The classes provided in this module ignore the *template* argument. + The classes provided in this module ignore the *blueprint* argument. It is there purely as a hook for user-defined subclasses. .. versionadded:: 3.12 - The *template* argument. + The *blueprint* argument. Pure path objects implement the :class:`os.PathLike` interface, allowing them to be used anywhere the interface is accepted. @@ -182,7 +182,7 @@ we also call *flavours*: .. versionchanged:: 3.6 Added support for the :class:`os.PathLike` interface. -.. class:: PurePosixPath(*pathsegments, template=None) +.. class:: PurePosixPath(*pathsegments, blueprint=None) A subclass of :class:`PurePath`, this path flavour represents non-Windows filesystem paths:: @@ -190,9 +190,9 @@ we also call *flavours*: >>> PurePosixPath('/etc') PurePosixPath('/etc') - *pathsegments* and *template* are specified similarly to :class:`PurePath`. + *pathsegments* and *blueprint* are specified similarly to :class:`PurePath`. -.. class:: PureWindowsPath(*pathsegments, template=None) +.. class:: PureWindowsPath(*pathsegments, blueprint=None) A subclass of :class:`PurePath`, this path flavour represents Windows filesystem paths, including `UNC paths`_:: @@ -202,7 +202,7 @@ we also call *flavours*: >>> PureWindowsPath('//server/share/file') PureWindowsPath('//server/share/file') - *pathsegments* and *template* are specified similarly to :class:`PurePath`. + *pathsegments* and *blueprint* are specified similarly to :class:`PurePath`. .. _unc paths: https://en.wikipedia.org/wiki/Path_(computing)#UNC @@ -716,7 +716,7 @@ Concrete paths are subclasses of the pure path classes. In addition to operations provided by the latter, they also provide methods to do system calls on path objects. There are three ways to instantiate concrete paths: -.. class:: Path(*pathsegments, template=None) +.. class:: Path(*pathsegments, blueprint=None) A subclass of :class:`PurePath`, this class represents concrete paths of the system's path flavour (instantiating it creates either a @@ -725,9 +725,9 @@ calls on path objects. There are three ways to instantiate concrete paths: >>> Path('setup.py') PosixPath('setup.py') - *pathsegments* and *template* are specified similarly to :class:`PurePath`. + *pathsegments* and *blueprint* are specified similarly to :class:`PurePath`. -.. class:: PosixPath(*pathsegments, template=None) +.. class:: PosixPath(*pathsegments, blueprint=None) A subclass of :class:`Path` and :class:`PurePosixPath`, this class represents concrete non-Windows filesystem paths:: @@ -735,9 +735,9 @@ calls on path objects. There are three ways to instantiate concrete paths: >>> PosixPath('/etc') PosixPath('/etc') - *pathsegments* and *template* are specified similarly to :class:`PurePath`. + *pathsegments* and *blueprint* are specified similarly to :class:`PurePath`. -.. class:: WindowsPath(*pathsegments, template=None) +.. class:: WindowsPath(*pathsegments, blueprint=None) A subclass of :class:`Path` and :class:`PureWindowsPath`, this class represents concrete Windows filesystem paths:: @@ -745,7 +745,7 @@ calls on path objects. There are three ways to instantiate concrete paths: >>> WindowsPath('c:/Program Files/') WindowsPath('c:/Program Files') - *pathsegments* and *template* are specified similarly to :class:`PurePath`. + *pathsegments* and *blueprint* are specified similarly to :class:`PurePath`. You can only instantiate the class flavour that corresponds to your system (allowing system calls on non-compatible path flavours could lead to diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 7536019732b7aa..395af21a86c12b 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -298,7 +298,7 @@ def __reduce__(self): # when pickling related paths. return (self.__class__, self.parts) - def __init__(self, *args, template=None): + def __init__(self, *args, blueprint=None): if not args: path = '' elif len(args) == 1: @@ -335,7 +335,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)(path_str, template=self) + path = type(self)(path_str, blueprint=self) path._str = path_str or '.' path._drv = drv path._root = root @@ -575,7 +575,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg, remove=(3, 14)) path_cls = type(self) - other = path_cls(other, *_deprecated, template=self) + other = path_cls(other, *_deprecated, blueprint=self) for step, path in enumerate([other] + list(other.parents)): if self.is_relative_to(path): break @@ -584,7 +584,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(*parts, template=self) + return path_cls(*parts, blueprint=self) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -595,7 +595,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, template=self) + other = type(self)(other, *_deprecated, blueprint=self) return other == self or other in self.parents @property @@ -613,7 +613,7 @@ def joinpath(self, *pathsegments): paths) or a totally different path (if one of the arguments is anchored). """ - return type(self)(self._raw_path, *pathsegments, template=self) + return type(self)(self._raw_path, *pathsegments, blueprint=self) def __truediv__(self, key): try: @@ -623,7 +623,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return type(self)(key, self._raw_path, template=self) + return type(self)(key, self._raw_path, blueprint=self) except TypeError: return NotImplemented @@ -672,7 +672,7 @@ def match(self, path_pattern): """ Return True if this path matches the given pattern. """ - pat = type(self)(path_pattern, template=self) + pat = type(self)(path_pattern, blueprint=self) if not pat.parts: raise ValueError("empty pattern") pat_parts = pat._parts_normcase @@ -726,12 +726,12 @@ class Path(PurePath): """ __slots__ = () - def __init__(self, *args, template=None, **kwargs): + def __init__(self, *args, blueprint=None, **kwargs): if kwargs: msg = ("support for supplying keyword arguments to pathlib.PurePath " "is deprecated and scheduled for removal in Python {remove}") warnings._deprecated("pathlib.PurePath(**kwargs)", msg, remove=(3, 14)) - super().__init__(*args, template=template) + super().__init__(*args, blueprint=blueprint) def __new__(cls, *args, **kwargs): if cls is Path: @@ -747,7 +747,7 @@ def _make_child_relpath(self, name): path_str = f'{path_str}{name}' else: path_str = name - path = type(self)(path_str, template=self) + path = type(self)(path_str, blueprint=self) path._str = path_str path._drv = self.drive path._root = self.root @@ -797,7 +797,7 @@ def samefile(self, other_path): try: other_st = other_path.stat() except AttributeError: - other_st = type(self)(other_path, template=self).stat() + other_st = type(self)(other_path, blueprint=self).stat() return self._flavour.samestat(st, other_st) def iterdir(self): @@ -859,7 +859,7 @@ def absolute(self): cwd = self._flavour.abspath(self.drive) else: cwd = os.getcwd() - return type(self)(cwd, self._raw_path, template=self) + return type(self)(cwd, self._raw_path, blueprint=self) def resolve(self, strict=False): """ @@ -877,7 +877,7 @@ def check_eloop(e): except OSError as e: check_eloop(e) raise - p = type(self)(s, template=self) + p = type(self)(s, blueprint=self) # In non-strict mode, realpath() doesn't raise on symlink loops. # Ensure we get an exception by calling stat() @@ -967,7 +967,7 @@ def readlink(self): """ if not hasattr(os, "readlink"): raise NotImplementedError("os.readlink() not available on this system") - return type(self)(os.readlink(self), template=self) + return type(self)(os.readlink(self), blueprint=self) def touch(self, mode=0o666, exist_ok=True): """ @@ -1056,7 +1056,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ os.rename(self, target) - return type(self)(target, template=self) + return type(self)(target, blueprint=self) def replace(self, target): """ @@ -1069,7 +1069,7 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ os.replace(self, target) - return type(self)(target, template=self) + return type(self)(target, blueprint=self) def symlink_to(self, target, target_is_directory=False): """ diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 3d910bf727750b..05a6adf44938e2 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -29,10 +29,10 @@ # class _BasePurePathSubclass(object): - def __init__(self, *args, template=None, session_id=None): - super().__init__(*args, template=template) - if template: - self.session_id = template.session_id + def __init__(self, *args, blueprint=None, session_id=None): + super().__init__(*args, blueprint=blueprint) + if blueprint: + self.session_id = blueprint.session_id else: self.session_id = session_id @@ -122,11 +122,11 @@ def test_str_subclass_common(self): self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') - def test_template_common(self): + def test_blueprint_common(self): class P(_BasePurePathSubclass, self.cls): pass p = P('foo', 'bar', session_id=42) - self.assertEqual(42, P(template=p).session_id) + self.assertEqual(42, P(blueprint=p).session_id) self.assertEqual(42, (p / 'foo').session_id) self.assertEqual(42, ('foo' / p).session_id) self.assertEqual(42, p.joinpath('foo').session_id) @@ -1625,14 +1625,14 @@ def test_home(self): env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) - def test_template(self): + def test_blueprint(self): class P(_BasePurePathSubclass, self.cls): pass p = P(BASE, session_id=42) - self.assertEqual(42, P(template=p).session_id) + self.assertEqual(42, P(blueprint=p).session_id) self.assertEqual(42, p.absolute().session_id) self.assertEqual(42, p.resolve().session_id) - self.assertEqual(42, P('~', template=p).expanduser().session_id) + self.assertEqual(42, P('~', blueprint=p).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 24ca85f4c5f36a..23d3056795d57f 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 optional *template* argument to :class:`pathlib.PurePath` and +Add optional *blueprint* argument to :class:`pathlib.PurePath` and :class:`~pathlib.Path`. This argument is supplied whenever a derivative path is created, such as from :attr:`pathlib.PurePath.parent`. Subclasses may use to pass information to derivative paths. Patch by Barney Gale.