From a8006706f7d6e8825b90f1970beed7845d1d72ed Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Thu, 22 Jun 2023 14:35:51 +0100 Subject: [PATCH] GH-89812: Add `pathlib.UnsupportedOperation` (GH-105926) This new exception type is raised instead of `NotImplementedError` when a path operation is not supported. It can be raised from `Path.readlink()`, `symlink_to()`, `hardlink_to()`, `owner()` and `group()`. In a future version of pathlib, it will be raised by `AbstractPath` for these methods and others, such as `AbstractPath.mkdir()` and `unlink()`. --- Doc/library/pathlib.rst | 44 ++++++++++++++++++- Doc/whatsnew/3.13.rst | 4 ++ Lib/pathlib.py | 22 +++++++--- Lib/test/test_pathlib.py | 32 +++++++++++--- ...3-06-19-22-20-41.gh-issue-89812.z2l_e8.rst | 2 + 5 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-06-19-22-20-41.gh-issue-89812.z2l_e8.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index ad801d5d7cd..3a4e1e64685 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -88,6 +88,17 @@ Opening a file:: '#!/bin/bash\n' +Exceptions +---------- + +.. exception:: UnsupportedOperation + + An exception inheriting :exc:`NotImplementedError` that is raised when an + unsupported operation is called on a path object. + + .. versionadded:: 3.13 + + .. _pure-paths: Pure paths @@ -752,6 +763,11 @@ calls on path objects. There are three ways to instantiate concrete paths: *pathsegments* is specified similarly to :class:`PurePath`. + .. versionchanged:: 3.13 + Raises :exc:`UnsupportedOperation` on Windows. In previous versions, + :exc:`NotImplementedError` was raised instead. + + .. class:: WindowsPath(*pathsegments) A subclass of :class:`Path` and :class:`PureWindowsPath`, this class @@ -762,6 +778,11 @@ calls on path objects. There are three ways to instantiate concrete paths: *pathsegments* is specified similarly to :class:`PurePath`. + .. versionchanged:: 3.13 + Raises :exc:`UnsupportedOperation` on non-Windows platforms. In previous + versions, :exc:`NotImplementedError` was raised instead. + + You can only instantiate the class flavour that corresponds to your system (allowing system calls on non-compatible path flavours could lead to bugs or failures in your application):: @@ -778,7 +799,7 @@ bugs or failures in your application):: File "", line 1, in File "pathlib.py", line 798, in __new__ % (cls.__name__,)) - NotImplementedError: cannot instantiate 'WindowsPath' on your system + UnsupportedOperation: cannot instantiate 'WindowsPath' on your system Methods @@ -952,6 +973,10 @@ call fails (for example because the path doesn't exist). Return the name of the group owning the file. :exc:`KeyError` is raised if the file's gid isn't found in the system database. + .. versionchanged:: 3.13 + Raises :exc:`UnsupportedOperation` if the :mod:`grp` module is not + available. In previous versions, :exc:`NotImplementedError` was raised. + .. method:: Path.is_dir() @@ -1210,6 +1235,10 @@ call fails (for example because the path doesn't exist). Return the name of the user owning the file. :exc:`KeyError` is raised if the file's uid isn't found in the system database. + .. versionchanged:: 3.13 + Raises :exc:`UnsupportedOperation` if the :mod:`pwd` module is not + available. In previous versions, :exc:`NotImplementedError` was raised. + .. method:: Path.read_bytes() @@ -1252,6 +1281,10 @@ call fails (for example because the path doesn't exist). .. versionadded:: 3.9 + .. versionchanged:: 3.13 + Raises :exc:`UnsupportedOperation` if :func:`os.readlink` is not + available. In previous versions, :exc:`NotImplementedError` was raised. + .. method:: Path.rename(target) @@ -1414,6 +1447,11 @@ call fails (for example because the path doesn't exist). The order of arguments (link, target) is the reverse of :func:`os.symlink`'s. + .. versionchanged:: 3.13 + Raises :exc:`UnsupportedOperation` if :func:`os.symlink` is not + available. In previous versions, :exc:`NotImplementedError` was raised. + + .. method:: Path.hardlink_to(target) Make this path a hard link to the same file as *target*. @@ -1424,6 +1462,10 @@ call fails (for example because the path doesn't exist). .. versionadded:: 3.10 + .. versionchanged:: 3.13 + Raises :exc:`UnsupportedOperation` if :func:`os.link` is not + available. In previous versions, :exc:`NotImplementedError` was raised. + .. method:: Path.touch(mode=0o666, exist_ok=True) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 6cf2bd24263..d1f13a50335 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -106,6 +106,10 @@ built on debug mode `. pathlib ------- +* Add :exc:`pathlib.UnsupportedOperation`, which is raised instead of + :exc:`NotImplementedError` when a path operation isn't supported. + (Contributed by Barney Gale in :gh:`89812`.) + * Add support for recursive wildcards in :meth:`pathlib.PurePath.match`. (Contributed by Barney Gale in :gh:`73435`.) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index d8c597f1027..a36ffdd73d8 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -21,6 +21,7 @@ from urllib.parse import quote_from_bytes as urlquote_from_bytes __all__ = [ + "UnsupportedOperation", "PurePath", "PurePosixPath", "PureWindowsPath", "Path", "PosixPath", "WindowsPath", ] @@ -207,6 +208,13 @@ def _select_unique(paths): # Public API # +class UnsupportedOperation(NotImplementedError): + """An exception that is raised when an unsupported operation is called on + a path object. + """ + pass + + class _PathParents(Sequence): """This object provides sequence-like access to the logical ancestors of a path. Don't try to construct it yourself.""" @@ -1241,7 +1249,7 @@ class Path(PurePath): import pwd return pwd.getpwuid(self.stat().st_uid).pw_name except ImportError: - raise NotImplementedError("Path.owner() is unsupported on this system") + raise UnsupportedOperation("Path.owner() is unsupported on this system") def group(self): """ @@ -1252,14 +1260,14 @@ class Path(PurePath): import grp return grp.getgrgid(self.stat().st_gid).gr_name except ImportError: - raise NotImplementedError("Path.group() is unsupported on this system") + raise UnsupportedOperation("Path.group() is unsupported on this system") 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") + raise UnsupportedOperation("os.readlink() not available on this system") return self.with_segments(os.readlink(self)) def touch(self, mode=0o666, exist_ok=True): @@ -1363,7 +1371,7 @@ class Path(PurePath): 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") + raise UnsupportedOperation("os.symlink() not available on this system") os.symlink(target, self, target_is_directory) def hardlink_to(self, target): @@ -1373,7 +1381,7 @@ class Path(PurePath): 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") + raise UnsupportedOperation("os.link() not available on this system") os.link(target, self) def expanduser(self): @@ -1400,7 +1408,7 @@ class PosixPath(Path, PurePosixPath): if os.name == 'nt': def __new__(cls, *args, **kwargs): - raise NotImplementedError( + raise UnsupportedOperation( f"cannot instantiate {cls.__name__!r} on your system") class WindowsPath(Path, PureWindowsPath): @@ -1412,5 +1420,5 @@ class WindowsPath(Path, PureWindowsPath): if os.name != 'nt': def __new__(cls, *args, **kwargs): - raise NotImplementedError( + raise UnsupportedOperation( f"cannot instantiate {cls.__name__!r} on your system") diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 02a0f25e0bd..f9356909cb0 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -24,6 +24,12 @@ except ImportError: grp = pwd = None +class UnsupportedOperationTest(unittest.TestCase): + def test_is_notimplemented(self): + self.assertTrue(issubclass(pathlib.UnsupportedOperation, NotImplementedError)) + self.assertTrue(isinstance(pathlib.UnsupportedOperation(), NotImplementedError)) + + # Make sure any symbolic links in the base test path are resolved. BASE = os.path.realpath(TESTFN) join = lambda *x: os.path.join(BASE, *x) @@ -1550,12 +1556,12 @@ class WindowsPathAsPureTest(PureWindowsPathTest): def test_owner(self): P = self.cls - with self.assertRaises(NotImplementedError): + with self.assertRaises(pathlib.UnsupportedOperation): P('c:/').owner() def test_group(self): P = self.cls - with self.assertRaises(NotImplementedError): + with self.assertRaises(pathlib.UnsupportedOperation): P('c:/').group() @@ -2055,6 +2061,13 @@ class PathTest(unittest.TestCase): with self.assertRaises(OSError): (P / 'fileA').readlink() + @unittest.skipIf(hasattr(os, "readlink"), "os.readlink() is present") + def test_readlink_unsupported(self): + P = self.cls(BASE) + p = P / 'fileA' + with self.assertRaises(pathlib.UnsupportedOperation): + q.readlink(p) + def _check_resolve(self, p, expected, strict=True): q = p.resolve(strict) self.assertEqual(q, expected) @@ -2343,7 +2356,7 @@ class PathTest(unittest.TestCase): if self.cls._flavour is os.path: self.skipTest("path flavour is supported") else: - self.assertRaises(NotImplementedError, self.cls) + self.assertRaises(pathlib.UnsupportedOperation, self.cls) def _test_cwd(self, p): q = self.cls(os.getcwd()) @@ -2543,12 +2556,12 @@ class PathTest(unittest.TestCase): self.assertTrue(link2.exists()) @unittest.skipIf(hasattr(os, "link"), "os.link() is present") - def test_link_to_not_implemented(self): + def test_hardlink_to_unsupported(self): P = self.cls(BASE) p = P / 'fileA' # linking to another path. q = P / 'dirA' / 'fileAA' - with self.assertRaises(NotImplementedError): + with self.assertRaises(pathlib.UnsupportedOperation): q.hardlink_to(p) def test_rename(self): @@ -2776,6 +2789,15 @@ class PathTest(unittest.TestCase): self.assertTrue(link.is_dir()) self.assertTrue(list(link.iterdir())) + @unittest.skipIf(hasattr(os, "symlink"), "os.symlink() is present") + def test_symlink_to_unsupported(self): + P = self.cls(BASE) + p = P / 'fileA' + # linking to another path. + q = P / 'dirA' / 'fileAA' + with self.assertRaises(pathlib.UnsupportedOperation): + q.symlink_to(p) + def test_is_junction(self): P = self.cls(BASE) diff --git a/Misc/NEWS.d/next/Library/2023-06-19-22-20-41.gh-issue-89812.z2l_e8.rst b/Misc/NEWS.d/next/Library/2023-06-19-22-20-41.gh-issue-89812.z2l_e8.rst new file mode 100644 index 00000000000..f1ef11e26bc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-06-19-22-20-41.gh-issue-89812.z2l_e8.rst @@ -0,0 +1,2 @@ +Add :exc:`pathlib.UnsupportedOperation`, which is raised instead of +:exc:`NotImplementedError` when a path operation isn't supported.