From 66f8eb1fdf4783e3e4dc581f78dffc8fa3a9be59 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 2 Feb 2022 17:16:17 +0000 Subject: [PATCH 1/8] Move `Path._make_child_relpath()` to `PurePath` --- Lib/pathlib.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 920e1f425a0c06..f4188a5f350025 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -530,6 +530,12 @@ def _make_child(self, args): self._drv, self._root, self._parts, drv, root, parts) return self._from_parsed_parts(drv, root, parts) + 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 __str__(self): """Return the string representation of the path, suitable for passing to system calls.""" @@ -873,12 +879,6 @@ def __new__(cls, *args, **kwargs): % (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): return self From 27d8f7f05346bc3fda7ede09cbabe7545f6a157b Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 2 Feb 2022 17:38:11 +0000 Subject: [PATCH 2/8] Add `_AbstractPath` class The `Path` class is now an *implementation* of `_AbstractPath`; its methods call functions in `os.path`, `io`, `pwd`, etc, whereas the corresponding methods in `_AbstractPath` instead raise `NotImplementedError`. --- Lib/pathlib.py | 277 ++++++++++++++++++++++++++++++------------------- 1 file changed, 172 insertions(+), 105 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index f4188a5f350025..13409efca58d1d 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -859,26 +859,16 @@ class PureWindowsPath(PurePath): # Filesystem-accessing classes -class Path(PurePath): - """PurePath subclass that can make system calls. +class _AbstractPath(PurePath): + """PurePath subclass with abstract methods for making system calls. - Path represents a filesystem path but unlike PurePath, also offers - methods to do system calls on path objects. Depending on your system, - instantiating a Path will return either a PosixPath or a WindowsPath - object. You can also instantiate a PosixPath or WindowsPath directly, - but cannot instantiate a WindowsPath on a POSIX system or vice versa. + In a future version of Python, the this class's interface may be exposed + as a public API, losing its underscore prefix. However, in its current + state it should be considered a private implementation detail with no + stability guarantees. Please direct feedback to bpo-24132. """ __slots__ = () - def __new__(cls, *args, **kwargs): - if cls is Path: - cls = WindowsPath if os.name == 'nt' else PosixPath - self = cls._from_parts(args) - if not self._flavour.is_supported: - raise NotImplementedError("cannot instantiate %r on your system" - % (cls.__name__,)) - return self - def __enter__(self): return self @@ -900,7 +890,7 @@ def cwd(cls): """Return a new path pointing to the current working directory (as returned by os.getcwd()). """ - return cls(os.getcwd()) + raise NotImplementedError @classmethod def home(cls): @@ -924,14 +914,13 @@ def iterdir(self): """Iterate over the files in this directory. Does not yield any result for the special paths '.' and '..'. """ - for name in os.listdir(self): - yield self._make_child_relpath(name) + raise NotImplementedError def _scandir(self): # bpo-24132: a future version of pathlib will support subclassing of # pathlib.Path to customize how the filesystem is accessed. This # includes scandir(), which is used to implement glob(). - return os.scandir(self) + return self.iterdir() def glob(self, pattern): """Iterate over this subtree and yield all existing files (of any @@ -975,55 +964,26 @@ def resolve(self, strict=False): Make the path absolute, resolving all symlinks on the way and also normalizing it. """ - - def check_eloop(e): - winerror = getattr(e, 'winerror', 0) - if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME: - raise RuntimeError("Symlink loop from %r" % e.filename) - - try: - s = os.path.realpath(self, strict=strict) - except OSError as e: - check_eloop(e) - raise - p = self._from_parts((s,)) - - # In non-strict mode, realpath() doesn't raise on symlink loops. - # Ensure we get an exception by calling stat() - if not strict: - try: - p.stat() - except OSError as e: - check_eloop(e) - return p + raise NotImplementedError def stat(self, *, follow_symlinks=True): """ Return the result of the stat() system call on this path, like os.stat() does. """ - return os.stat(self, follow_symlinks=follow_symlinks) + raise NotImplementedError def owner(self): """ Return the login name of the file owner. """ - try: - import pwd - return pwd.getpwuid(self.stat().st_uid).pw_name - except ImportError: - raise NotImplementedError("Path.owner() is unsupported on this system") + raise NotImplementedError def group(self): """ Return the group name of the file gid. """ - - try: - import grp - return grp.getgrgid(self.stat().st_gid).gr_name - except ImportError: - raise NotImplementedError("Path.group() is unsupported on this system") + raise NotImplementedError def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): @@ -1031,9 +991,7 @@ def open(self, mode='r', buffering=-1, encoding=None, Open the file pointed by this path and return a file object, as the built-in open() function does. """ - if "b" not in mode: - encoding = io.text_encoding(encoding) - return io.open(self, mode, buffering, encoding, errors, newline) + raise NotImplementedError def read_bytes(self): """ @@ -1074,54 +1032,25 @@ def readlink(self): """ Return the path to which the symbolic link points. """ - if not hasattr(os, "readlink"): - raise NotImplementedError("os.readlink() not available on this system") - return self._from_parts((os.readlink(self),)) + raise NotImplementedError def touch(self, mode=0o666, exist_ok=True): """ Create this file with the given access mode, if it doesn't exist. """ - - if exist_ok: - # First try to bump modification time - # Implementation note: GNU touch uses the UTIME_NOW option of - # the utimensat() / futimens() functions. - try: - os.utime(self, None) - except OSError: - # Avoid exception chaining - pass - else: - return - flags = os.O_CREAT | os.O_WRONLY - if not exist_ok: - flags |= os.O_EXCL - fd = os.open(self, flags, mode) - os.close(fd) + raise NotImplementedError def mkdir(self, mode=0o777, parents=False, exist_ok=False): """ Create a new directory at this given path. """ - try: - os.mkdir(self, mode) - except FileNotFoundError: - if not parents or self.parent == self: - raise - self.parent.mkdir(parents=True, exist_ok=True) - self.mkdir(mode, parents=False, exist_ok=exist_ok) - except OSError: - # Cannot rely on checking for EEXIST, since the operating system - # could give priority to other errors like EACCES or EROFS - if not exist_ok or not self.is_dir(): - raise + raise NotImplementedError def chmod(self, mode, *, follow_symlinks=True): """ Change the permissions of the path, like os.chmod(). """ - os.chmod(self, mode, follow_symlinks=follow_symlinks) + raise NotImplementedError def lchmod(self, mode): """ @@ -1135,17 +1064,13 @@ def unlink(self, missing_ok=False): Remove this file or link. If the path is a directory, use rmdir() instead. """ - try: - os.unlink(self) - except FileNotFoundError: - if not missing_ok: - raise + raise NotImplementedError def rmdir(self): """ Remove this directory. The directory must be empty. """ - os.rmdir(self) + raise NotImplementedError def lstat(self): """ @@ -1164,8 +1089,7 @@ def rename(self, target): Returns the new Path instance pointing to the target path. """ - os.rename(self, target) - return self.__class__(target) + raise NotImplementedError def replace(self, target): """ @@ -1177,17 +1101,14 @@ def replace(self, target): Returns the new Path instance pointing to the target path. """ - os.replace(self, target) - return self.__class__(target) + raise NotImplementedError def symlink_to(self, target, target_is_directory=False): """ Make this path a symlink pointing to the target path. Note the order of arguments (link, target) is the reverse of os.symlink. """ - if not hasattr(os, "symlink"): - raise NotImplementedError("os.symlink() not available on this system") - os.symlink(target, self, target_is_directory) + raise NotImplementedError def hardlink_to(self, target): """ @@ -1195,9 +1116,7 @@ def hardlink_to(self, target): Note the order of arguments (self, target) is the reverse of os.link's. """ - if not hasattr(os, "link"): - raise NotImplementedError("os.link() not available on this system") - os.link(target, self) + raise NotImplementedError def link_to(self, target): """ @@ -1370,6 +1289,154 @@ def expanduser(self): """ Return a new path with expanded ~ and ~user constructs (as returned by os.path.expanduser) """ + raise NotImplementedError + + +class Path(_AbstractPath): + """PurePath subclass that can make system calls. + + Path represents a filesystem path but unlike PurePath, also offers + methods to do system calls on path objects. Depending on your system, + instantiating a Path will return either a PosixPath or a WindowsPath + object. You can also instantiate a PosixPath or WindowsPath directly, + but cannot instantiate a WindowsPath on a POSIX system or vice versa. + """ + __slots__ = () + + def __new__(cls, *args, **kwargs): + if cls is Path: + cls = WindowsPath if os.name == 'nt' else PosixPath + self = cls._from_parts(args) + if not self._flavour.is_supported: + raise NotImplementedError("cannot instantiate %r on your system" + % (cls.__name__,)) + return self + + @classmethod + def cwd(cls): + return cls(os.getcwd()) + + def iterdir(self): + for name in os.listdir(self): + yield self._make_child_relpath(name) + + def _scandir(self): + return os.scandir(self) + + def resolve(self, strict=False): + def check_eloop(e): + winerror = getattr(e, 'winerror', 0) + if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME: + raise RuntimeError("Symlink loop from %r" % e.filename) + + try: + s = os.path.realpath(self, strict=strict) + except OSError as e: + check_eloop(e) + raise + p = self._from_parts((s,)) + + # In non-strict mode, realpath() doesn't raise on symlink loops. + # Ensure we get an exception by calling stat() + if not strict: + try: + p.stat() + except OSError as e: + check_eloop(e) + return p + + def stat(self, *, follow_symlinks=True): + return os.stat(self, follow_symlinks=follow_symlinks) + + def owner(self): + try: + import pwd + return pwd.getpwuid(self.stat().st_uid).pw_name + except ImportError: + raise NotImplementedError("Path.owner() is unsupported on this system") + + def group(self): + try: + import grp + return grp.getgrgid(self.stat().st_gid).gr_name + except ImportError: + raise NotImplementedError("Path.group() is unsupported on this system") + + def open(self, mode='r', buffering=-1, encoding=None, + errors=None, newline=None): + if "b" not in mode: + encoding = io.text_encoding(encoding) + return io.open(self, mode, buffering, encoding, errors, newline) + + def readlink(self): + if not hasattr(os, "readlink"): + raise NotImplementedError("os.readlink() not available on this system") + return self._from_parts((os.readlink(self),)) + + def touch(self, mode=0o666, exist_ok=True): + if exist_ok: + # First try to bump modification time + # Implementation note: GNU touch uses the UTIME_NOW option of + # the utimensat() / futimens() functions. + try: + os.utime(self, None) + except OSError: + # Avoid exception chaining + pass + else: + return + flags = os.O_CREAT | os.O_WRONLY + if not exist_ok: + flags |= os.O_EXCL + fd = os.open(self, flags, mode) + os.close(fd) + + def mkdir(self, mode=0o777, parents=False, exist_ok=False): + try: + os.mkdir(self, mode) + except FileNotFoundError: + if not parents or self.parent == self: + raise + self.parent.mkdir(parents=True, exist_ok=True) + self.mkdir(mode, parents=False, exist_ok=exist_ok) + except OSError: + # Cannot rely on checking for EEXIST, since the operating system + # could give priority to other errors like EACCES or EROFS + if not exist_ok or not self.is_dir(): + raise + + def chmod(self, mode, *, follow_symlinks=True): + os.chmod(self, mode, follow_symlinks=follow_symlinks) + + def unlink(self, missing_ok=False): + try: + os.unlink(self) + except FileNotFoundError: + if not missing_ok: + raise + + def rmdir(self): + os.rmdir(self) + + def rename(self, target): + os.rename(self, target) + return self.__class__(target) + + def replace(self, target): + os.replace(self, target) + return self.__class__(target) + + def symlink_to(self, target, target_is_directory=False): + if not hasattr(os, "symlink"): + raise NotImplementedError("os.symlink() not available on this system") + os.symlink(target, self, target_is_directory) + + def hardlink_to(self, target): + if not hasattr(os, "link"): + raise NotImplementedError("os.link() not available on this system") + os.link(target, self) + + def expanduser(self): if (not (self._drv or self._root) and self._parts and self._parts[0][:1] == '~'): homedir = os.path.expanduser(self._parts[0]) From 47191edbfd5bd4c9822615f5c66012bdb21b8000 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 2 Feb 2022 17:43:20 +0000 Subject: [PATCH 3/8] Optimize `Path` methods that may raise `NotImplementedError` Check the presence of the necessary `os`, `pwd` and `grp` functions at import time. --- Lib/pathlib.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 13409efca58d1d..ef437b246079d9 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -14,6 +14,15 @@ from urllib.parse import quote_from_bytes as urlquote_from_bytes from types import GenericAlias +try: + import pwd +except ImportError: + pwd = None +try: + import grp +except ImportError: + grp = None + __all__ = [ "PurePath", "PurePosixPath", "PureWindowsPath", @@ -1348,19 +1357,13 @@ def check_eloop(e): def stat(self, *, follow_symlinks=True): return os.stat(self, follow_symlinks=follow_symlinks) - def owner(self): - try: - import pwd + if hasattr(pwd, 'getpwuid'): + def owner(self): return pwd.getpwuid(self.stat().st_uid).pw_name - except ImportError: - raise NotImplementedError("Path.owner() is unsupported on this system") - def group(self): - try: - import grp + if hasattr(grp, 'getgrgid'): + def group(self): return grp.getgrgid(self.stat().st_gid).gr_name - except ImportError: - raise NotImplementedError("Path.group() is unsupported on this system") def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): @@ -1368,10 +1371,9 @@ def open(self, mode='r', buffering=-1, encoding=None, encoding = io.text_encoding(encoding) return io.open(self, mode, buffering, encoding, errors, newline) - def readlink(self): - if not hasattr(os, "readlink"): - raise NotImplementedError("os.readlink() not available on this system") - return self._from_parts((os.readlink(self),)) + if hasattr(os, 'readlink'): + def readlink(self): + return self._from_parts((os.readlink(self),)) def touch(self, mode=0o666, exist_ok=True): if exist_ok: @@ -1426,15 +1428,13 @@ def replace(self, target): os.replace(self, target) return self.__class__(target) - def symlink_to(self, target, target_is_directory=False): - if not hasattr(os, "symlink"): - raise NotImplementedError("os.symlink() not available on this system") - os.symlink(target, self, target_is_directory) + if hasattr(os, 'symlink'): + def symlink_to(self, target, target_is_directory=False): + os.symlink(target, self, target_is_directory) - def hardlink_to(self, target): - if not hasattr(os, "link"): - raise NotImplementedError("os.link() not available on this system") - os.link(target, self) + if hasattr(os, 'link'): + def hardlink_to(self, target): + os.link(target, self) def expanduser(self): if (not (self._drv or self._root) and From bbd7fd1cbbbcd64219f6b27620cafab7534edeab Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 2 Feb 2022 18:59:42 +0000 Subject: [PATCH 4/8] Add NEWS entry --- .../next/Library/2022-02-02-18-59-09.bpo-24132.mddFFe.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2022-02-02-18-59-09.bpo-24132.mddFFe.rst diff --git a/Misc/NEWS.d/next/Library/2022-02-02-18-59-09.bpo-24132.mddFFe.rst b/Misc/NEWS.d/next/Library/2022-02-02-18-59-09.bpo-24132.mddFFe.rst new file mode 100644 index 00000000000000..21daca18ee97db --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-02-02-18-59-09.bpo-24132.mddFFe.rst @@ -0,0 +1,2 @@ +Add :class:`pathlib._AbstractPath` class. This internal class sets the stage +for supporting user-defined path subclasses in a future version of Python. From 67e5e9582ab81c8a044b903e37646d500931a9ed Mon Sep 17 00:00:00 2001 From: barneygale Date: Fri, 11 Feb 2022 19:41:59 +0000 Subject: [PATCH 5/8] Subclass `abc.ABC` and decorate core methods with `@abc.abstractmethod`. --- Lib/pathlib.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index ef437b246079d9..c0dd91cc5e427d 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -8,6 +8,7 @@ import sys import warnings from _collections_abc import Sequence +from abc import ABC, abstractmethod 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 @@ -458,7 +459,7 @@ def __repr__(self): return "<{}.parents>".format(self._pathcls.__name__) -class PurePath(object): +class PurePath(os.PathLike): """Base class for manipulating paths without I/O. PurePath represents a filesystem path and offers operations which @@ -840,10 +841,6 @@ def match(self, path_pattern): return False return True -# Can't subclass os.PathLike from PurePath and keep the constructor -# optimizations in PurePath._parse_args(). -os.PathLike.register(PurePath) - class PurePosixPath(PurePath): """PurePath subclass for non-Windows systems. @@ -868,7 +865,7 @@ class PureWindowsPath(PurePath): # Filesystem-accessing classes -class _AbstractPath(PurePath): +class _AbstractPath(PurePath, ABC): """PurePath subclass with abstract methods for making system calls. In a future version of Python, the this class's interface may be exposed @@ -919,6 +916,7 @@ def samefile(self, other_path): other_st = self.__class__(other_path).stat() return os.path.samestat(st, other_st) + @abstractmethod def iterdir(self): """Iterate over the files in this directory. Does not yield any result for the special paths '.' and '..'. @@ -975,6 +973,7 @@ def resolve(self, strict=False): """ raise NotImplementedError + @abstractmethod def stat(self, *, follow_symlinks=True): """ Return the result of the stat() system call on this path, like @@ -994,6 +993,7 @@ def group(self): """ raise NotImplementedError + @abstractmethod def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): """ From ef8d13e15502b8f096bc6878ad93501e0c8940c8 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 14 Feb 2022 14:12:17 +0000 Subject: [PATCH 6/8] Remove quasi-abstract methods from AbstractPath --- Lib/pathlib.py | 332 +++++++++++++++++++++---------------------------- 1 file changed, 145 insertions(+), 187 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index c0dd91cc5e427d..bab45399d2a898 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -15,15 +15,6 @@ from urllib.parse import quote_from_bytes as urlquote_from_bytes from types import GenericAlias -try: - import pwd -except ImportError: - pwd = None -try: - import grp -except ImportError: - grp = None - __all__ = [ "PurePath", "PurePosixPath", "PureWindowsPath", @@ -875,35 +866,6 @@ class _AbstractPath(PurePath, ABC): """ __slots__ = () - def __enter__(self): - return self - - def __exit__(self, t, v, tb): - # https://bugs.python.org/issue39682 - # In previous versions of pathlib, this method marked this path as - # closed; subsequent attempts to perform I/O would raise an IOError. - # This functionality was never documented, and had the effect of - # making Path objects mutable, contrary to PEP 428. In Python 3.9 the - # _closed attribute was removed, and this method made a no-op. - # This method and __enter__()/__exit__() should be deprecated and - # removed in the future. - pass - - # Public API - - @classmethod - def cwd(cls): - """Return a new path pointing to the current working directory - (as returned by os.getcwd()). - """ - raise NotImplementedError - - @classmethod - def home(cls): - """Return a new path pointing to the user's home directory (as - returned by os.path.expanduser('~')). - """ - return cls("~").expanduser() def samefile(self, other_path): """Return whether other_path is the same or not as this file @@ -956,23 +918,6 @@ def rglob(self, pattern): for p in selector.select_from(self): yield p - def absolute(self): - """Return an absolute version of this path by prepending the current - working directory. No normalization or symlink resolution is performed. - - Use resolve() to get the canonical path to a file. - """ - if self.is_absolute(): - return self - return self._from_parts([self.cwd()] + self._parts) - - def resolve(self, strict=False): - """ - Make the path absolute, resolving all symlinks on the way and also - normalizing it. - """ - raise NotImplementedError - @abstractmethod def stat(self, *, follow_symlinks=True): """ @@ -981,18 +926,6 @@ def stat(self, *, follow_symlinks=True): """ raise NotImplementedError - def owner(self): - """ - Return the login name of the file owner. - """ - raise NotImplementedError - - def group(self): - """ - Return the group name of the file gid. - """ - raise NotImplementedError - @abstractmethod def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): @@ -1037,50 +970,6 @@ def write_text(self, data, encoding=None, errors=None, newline=None): with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f: return f.write(data) - def readlink(self): - """ - Return the path to which the symbolic link points. - """ - raise NotImplementedError - - def touch(self, mode=0o666, exist_ok=True): - """ - Create this file with the given access mode, if it doesn't exist. - """ - raise NotImplementedError - - def mkdir(self, mode=0o777, parents=False, exist_ok=False): - """ - Create a new directory at this given path. - """ - raise NotImplementedError - - def chmod(self, mode, *, follow_symlinks=True): - """ - Change the permissions of the path, like os.chmod(). - """ - raise NotImplementedError - - def lchmod(self, mode): - """ - Like chmod(), except if the path points to a symlink, the symlink's - permissions are changed, rather than its target's. - """ - self.chmod(mode, follow_symlinks=False) - - def unlink(self, missing_ok=False): - """ - Remove this file or link. - If the path is a directory, use rmdir() instead. - """ - raise NotImplementedError - - def rmdir(self): - """ - Remove this directory. The directory must be empty. - """ - raise NotImplementedError - def lstat(self): """ Like stat(), except if the path points to a symlink, the symlink's @@ -1088,63 +977,6 @@ def lstat(self): """ return self.stat(follow_symlinks=False) - def rename(self, target): - """ - Rename this path to the target path. - - The target path may be absolute or relative. Relative paths are - interpreted relative to the current working directory, *not* the - directory of the Path object. - - Returns the new Path instance pointing to the target path. - """ - raise NotImplementedError - - def replace(self, target): - """ - Rename this path to the target path, overwriting if that path exists. - - The target path may be absolute or relative. Relative paths are - interpreted relative to the current working directory, *not* the - directory of the Path object. - - Returns the new Path instance pointing to the target path. - """ - raise NotImplementedError - - def symlink_to(self, target, target_is_directory=False): - """ - Make this path a symlink pointing to the target path. - Note the order of arguments (link, target) is the reverse of os.symlink. - """ - raise NotImplementedError - - def hardlink_to(self, target): - """ - Make this path a hard link pointing to the same file as *target*. - - Note the order of arguments (self, target) is the reverse of os.link's. - """ - raise NotImplementedError - - def link_to(self, target): - """ - Make the target path a hard link pointing to this path. - - Note this function does not make this path a hard link to *target*, - despite the implication of the function and argument names. The order - of arguments (target, link) is the reverse of Path.symlink_to, but - matches that of os.link. - - Deprecated since Python 3.10 and scheduled for removal in Python 3.12. - Use `hardlink_to()` instead. - """ - warnings.warn("pathlib.Path.link_to() is deprecated and is scheduled " - "for removal in Python 3.12. " - "Use pathlib.Path.hardlink_to() instead.", - DeprecationWarning, stacklevel=2) - self.__class__(target).hardlink_to(self) - # Convenience functions for querying the stat results def exists(self): @@ -1294,12 +1126,6 @@ def is_socket(self): # Non-encodable path return False - def expanduser(self): - """ Return a new path with expanded ~ and ~user constructs - (as returned by os.path.expanduser) - """ - raise NotImplementedError - class Path(_AbstractPath): """PurePath subclass that can make system calls. @@ -1321,10 +1147,36 @@ def __new__(cls, *args, **kwargs): % (cls.__name__,)) return self + def __enter__(self): + return self + + def __exit__(self, t, v, tb): + # https://bugs.python.org/issue39682 + # In previous versions of pathlib, this method marked this path as + # closed; subsequent attempts to perform I/O would raise an IOError. + # This functionality was never documented, and had the effect of + # making Path objects mutable, contrary to PEP 428. In Python 3.9 the + # _closed attribute was removed, and this method made a no-op. + # This method and __enter__()/__exit__() should be deprecated and + # removed in the future. + pass + + # Public API + @classmethod def cwd(cls): + """Return a new path pointing to the current working directory + (as returned by os.getcwd()). + """ return cls(os.getcwd()) + @classmethod + def home(cls): + """Return a new path pointing to the user's home directory (as + returned by os.path.expanduser('~')). + """ + return cls("~").expanduser() + def iterdir(self): for name in os.listdir(self): yield self._make_child_relpath(name) @@ -1332,7 +1184,22 @@ def iterdir(self): def _scandir(self): return os.scandir(self) + def absolute(self): + """Return an absolute version of this path by prepending the current + working directory. No normalization or symlink resolution is performed. + + Use resolve() to get the canonical path to a file. + """ + if self.is_absolute(): + return self + return self._from_parts([self.cwd()] + self._parts) + def resolve(self, strict=False): + """ + Make the path absolute, resolving all symlinks on the way and also + normalizing it. + """ + def check_eloop(e): winerror = getattr(e, 'winerror', 0) if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME: @@ -1357,13 +1224,26 @@ def check_eloop(e): def stat(self, *, follow_symlinks=True): return os.stat(self, follow_symlinks=follow_symlinks) - if hasattr(pwd, 'getpwuid'): - def owner(self): + def owner(self): + """ + Return the login name of the file owner. + """ + try: + import pwd return pwd.getpwuid(self.stat().st_uid).pw_name + except ImportError: + raise NotImplementedError("Path.owner() is unsupported on this system") + + def group(self): + """ + Return the group name of the file gid. + """ - if hasattr(grp, 'getgrgid'): - def group(self): + try: + import grp return grp.getgrgid(self.stat().st_gid).gr_name + except ImportError: + raise NotImplementedError("Path.group() is unsupported on this system") def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): @@ -1371,11 +1251,19 @@ def open(self, mode='r', buffering=-1, encoding=None, encoding = io.text_encoding(encoding) return io.open(self, mode, buffering, encoding, errors, newline) - if hasattr(os, 'readlink'): - def readlink(self): - return self._from_parts((os.readlink(self),)) + def readlink(self): + """ + Return the path to which the symbolic link points. + """ + if not hasattr(os, "readlink"): + raise NotImplementedError("os.readlink() not available on this system") + return self._from_parts((os.readlink(self),)) def touch(self, mode=0o666, exist_ok=True): + """ + Create this file with the given access mode, if it doesn't exist. + """ + if exist_ok: # First try to bump modification time # Implementation note: GNU touch uses the UTIME_NOW option of @@ -1394,6 +1282,9 @@ def touch(self, mode=0o666, exist_ok=True): os.close(fd) def mkdir(self, mode=0o777, parents=False, exist_ok=False): + """ + Create a new directory at this given path. + """ try: os.mkdir(self, mode) except FileNotFoundError: @@ -1408,9 +1299,23 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False): raise def chmod(self, mode, *, follow_symlinks=True): + """ + Change the permissions of the path, like os.chmod(). + """ os.chmod(self, mode, follow_symlinks=follow_symlinks) + def lchmod(self, mode): + """ + Like chmod(), except if the path points to a symlink, the symlink's + permissions are changed, rather than its target's. + """ + self.chmod(mode, follow_symlinks=False) + def unlink(self, missing_ok=False): + """ + Remove this file or link. + If the path is a directory, use rmdir() instead. + """ try: os.unlink(self) except FileNotFoundError: @@ -1418,25 +1323,78 @@ def unlink(self, missing_ok=False): raise def rmdir(self): + """ + Remove this directory. The directory must be empty. + """ os.rmdir(self) def rename(self, target): + """ + Rename this path to the target path. + + The target path may be absolute or relative. Relative paths are + interpreted relative to the current working directory, *not* the + directory of the Path object. + + Returns the new Path instance pointing to the target path. + """ os.rename(self, target) return self.__class__(target) def replace(self, target): + """ + Rename this path to the target path, overwriting if that path exists. + + The target path may be absolute or relative. Relative paths are + interpreted relative to the current working directory, *not* the + directory of the Path object. + + Returns the new Path instance pointing to the target path. + """ os.replace(self, target) return self.__class__(target) - if hasattr(os, 'symlink'): - def symlink_to(self, target, target_is_directory=False): - os.symlink(target, self, target_is_directory) + def symlink_to(self, target, target_is_directory=False): + """ + Make this path a symlink pointing to the target path. + Note the order of arguments (link, target) is the reverse of os.symlink. + """ + if not hasattr(os, "symlink"): + raise NotImplementedError("os.symlink() not available on this system") + os.symlink(target, self, target_is_directory) - if hasattr(os, 'link'): - def hardlink_to(self, target): - os.link(target, self) + def hardlink_to(self, target): + """ + Make this path a hard link pointing to the same file as *target*. + + Note the order of arguments (self, target) is the reverse of os.link's. + """ + if not hasattr(os, "link"): + raise NotImplementedError("os.link() not available on this system") + os.link(target, self) + + def link_to(self, target): + """ + Make the target path a hard link pointing to this path. + + Note this function does not make this path a hard link to *target*, + despite the implication of the function and argument names. The order + of arguments (target, link) is the reverse of Path.symlink_to, but + matches that of os.link. + + Deprecated since Python 3.10 and scheduled for removal in Python 3.12. + Use `hardlink_to()` instead. + """ + warnings.warn("pathlib.Path.link_to() is deprecated and is scheduled " + "for removal in Python 3.12. " + "Use pathlib.Path.hardlink_to() instead.", + DeprecationWarning, stacklevel=2) + self.__class__(target).hardlink_to(self) def expanduser(self): + """ Return a new path with expanded ~ and ~user constructs + (as returned by os.path.expanduser) + """ if (not (self._drv or self._root) and self._parts and self._parts[0][:1] == '~'): homedir = os.path.expanduser(self._parts[0]) From 18a2c3896c4b88b4e518ec48c503df2d7e055a17 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 14 Feb 2022 20:15:58 +0000 Subject: [PATCH 7/8] Improve _scandir() comment. --- Lib/pathlib.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 995c98701c3976..cbc62348663a65 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -882,9 +882,10 @@ def iterdir(self): raise NotImplementedError def _scandir(self): - # bpo-24132: a future version of pathlib will support subclassing of - # pathlib.Path to customize how the filesystem is accessed. This - # includes scandir(), which is used to implement glob(). + # This method is used to implement glob(). It yields os.DirEntry-like + # objects with some stat() data pre-cached. As the AbstractPath API is + # a superset of the DirEntry API, the default implementation forwards + # to iterdir() as a convenience. return self.iterdir() def glob(self, pattern): From 3312870453b38de035924ff5c1bc60b4a4770ef3 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 21 Feb 2022 03:05:57 +0000 Subject: [PATCH 8/8] Remove speculation/promises from `_AbstractPath` docstring. --- Lib/pathlib.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index cbc62348663a65..f48ad4216d9220 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -854,12 +854,8 @@ class PureWindowsPath(PurePath): class _AbstractPath(PurePath, ABC): - """PurePath subclass with abstract methods for making system calls. - - In a future version of Python, the this class's interface may be exposed - as a public API, losing its underscore prefix. However, in its current - state it should be considered a private implementation detail with no - stability guarantees. Please direct feedback to bpo-24132. + """Private PurePath subclass with abstract methods for opening files and + listing directories. """ __slots__ = ()