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()`.
This commit is contained in:
Barney Gale 2023-06-22 14:35:51 +01:00 committed by GitHub
parent 04492cbc9a
commit a8006706f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 91 additions and 13 deletions

View File

@ -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)

View File

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

View File

@ -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")

View File

@ -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)

View File

@ -0,0 +1,2 @@
Add :exc:`pathlib.UnsupportedOperation`, which is raised instead of
:exc:`NotImplementedError` when a path operation isn't supported.