[Python-checkins] GH-89812: Add `pathlib.UnsupportedOperation` (GH-105926)

barneygale webhook-mailer at python.org
Thu Jun 22 09:35:55 EDT 2023


https://github.com/python/cpython/commit/a8006706f7d6e8825b90f1970beed7845d1d72ed
commit: a8006706f7d6e8825b90f1970beed7845d1d72ed
branch: main
author: Barney Gale <barney.gale at gmail.com>
committer: barneygale <barney.gale at gmail.com>
date: 2023-06-22T14:35:51+01:00
summary:

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()`.

files:
A Misc/NEWS.d/next/Library/2023-06-19-22-20-41.gh-issue-89812.z2l_e8.rst
M Doc/library/pathlib.rst
M Doc/whatsnew/3.13.rst
M Lib/pathlib.py
M Lib/test/test_pathlib.py

diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst
index ad801d5d7cdc..3a4e1e64685c 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 "<stdin>", line 1, in <module>
      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 6cf2bd242637..d1f13a50335b 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -106,6 +106,10 @@ built on debug mode <debug-build>`.
 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 d8c597f1027f..a36ffdd73d8a 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -21,6 +21,7 @@
 
 
 __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 @@ def owner(self):
             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 @@ def group(self):
             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 @@ def symlink_to(self, target, target_is_directory=False):
         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 @@ 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")
+            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 02a0f25e0bd9..f9356909cb09 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -24,6 +24,12 @@
     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 @@ def test_readlink(self):
         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 @@ def test_unsupported_flavour(self):
         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 @@ def test_hardlink_to(self):
         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 @@ def test_symlink_to(self):
         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 000000000000..f1ef11e26bc5
--- /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.



More information about the Python-checkins mailing list