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' '#!/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:
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`. *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) .. class:: WindowsPath(*pathsegments)
A subclass of :class:`Path` and :class:`PureWindowsPath`, this class 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`. *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 You can only instantiate the class flavour that corresponds to your system
(allowing system calls on non-compatible path flavours could lead to (allowing system calls on non-compatible path flavours could lead to
bugs or failures in your application):: bugs or failures in your application)::
@ -778,7 +799,7 @@ bugs or failures in your application)::
File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <module>
File "pathlib.py", line 798, in __new__ File "pathlib.py", line 798, in __new__
% (cls.__name__,)) % (cls.__name__,))
NotImplementedError: cannot instantiate 'WindowsPath' on your system UnsupportedOperation: cannot instantiate 'WindowsPath' on your system
Methods 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 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. 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() .. 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 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. 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() .. method:: Path.read_bytes()
@ -1252,6 +1281,10 @@ call fails (for example because the path doesn't exist).
.. versionadded:: 3.9 .. 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) .. 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 The order of arguments (link, target) is the reverse
of :func:`os.symlink`'s. 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) .. method:: Path.hardlink_to(target)
Make this path a hard link to the same file as *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 .. 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) .. method:: Path.touch(mode=0o666, exist_ok=True)

View File

@ -106,6 +106,10 @@ built on debug mode <debug-build>`.
pathlib 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`. * Add support for recursive wildcards in :meth:`pathlib.PurePath.match`.
(Contributed by Barney Gale in :gh:`73435`.) (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__ = [ __all__ = [
"UnsupportedOperation",
"PurePath", "PurePosixPath", "PureWindowsPath", "PurePath", "PurePosixPath", "PureWindowsPath",
"Path", "PosixPath", "WindowsPath", "Path", "PosixPath", "WindowsPath",
] ]
@ -207,6 +208,13 @@ def _select_unique(paths):
# Public API # Public API
# #
class UnsupportedOperation(NotImplementedError):
"""An exception that is raised when an unsupported operation is called on
a path object.
"""
pass
class _PathParents(Sequence): class _PathParents(Sequence):
"""This object provides sequence-like access to the logical ancestors """This object provides sequence-like access to the logical ancestors
of a path. Don't try to construct it yourself.""" of a path. Don't try to construct it yourself."""
@ -1241,7 +1249,7 @@ class Path(PurePath):
import pwd import pwd
return pwd.getpwuid(self.stat().st_uid).pw_name return pwd.getpwuid(self.stat().st_uid).pw_name
except ImportError: except ImportError:
raise NotImplementedError("Path.owner() is unsupported on this system") raise UnsupportedOperation("Path.owner() is unsupported on this system")
def group(self): def group(self):
""" """
@ -1252,14 +1260,14 @@ class Path(PurePath):
import grp import grp
return grp.getgrgid(self.stat().st_gid).gr_name return grp.getgrgid(self.stat().st_gid).gr_name
except ImportError: except ImportError:
raise NotImplementedError("Path.group() is unsupported on this system") raise UnsupportedOperation("Path.group() is unsupported on this system")
def readlink(self): def readlink(self):
""" """
Return the path to which the symbolic link points. Return the path to which the symbolic link points.
""" """
if not hasattr(os, "readlink"): 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)) return self.with_segments(os.readlink(self))
def touch(self, mode=0o666, exist_ok=True): 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. Note the order of arguments (link, target) is the reverse of os.symlink.
""" """
if not hasattr(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) os.symlink(target, self, target_is_directory)
def hardlink_to(self, target): 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. Note the order of arguments (self, target) is the reverse of os.link's.
""" """
if not hasattr(os, "link"): 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) os.link(target, self)
def expanduser(self): def expanduser(self):
@ -1400,7 +1408,7 @@ class PosixPath(Path, PurePosixPath):
if os.name == 'nt': if os.name == 'nt':
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
raise NotImplementedError( raise UnsupportedOperation(
f"cannot instantiate {cls.__name__!r} on your system") f"cannot instantiate {cls.__name__!r} on your system")
class WindowsPath(Path, PureWindowsPath): class WindowsPath(Path, PureWindowsPath):
@ -1412,5 +1420,5 @@ class WindowsPath(Path, PureWindowsPath):
if os.name != 'nt': if os.name != 'nt':
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
raise NotImplementedError( raise UnsupportedOperation(
f"cannot instantiate {cls.__name__!r} on your system") f"cannot instantiate {cls.__name__!r} on your system")

View File

@ -24,6 +24,12 @@ except ImportError:
grp = pwd = None 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. # Make sure any symbolic links in the base test path are resolved.
BASE = os.path.realpath(TESTFN) BASE = os.path.realpath(TESTFN)
join = lambda *x: os.path.join(BASE, *x) join = lambda *x: os.path.join(BASE, *x)
@ -1550,12 +1556,12 @@ class WindowsPathAsPureTest(PureWindowsPathTest):
def test_owner(self): def test_owner(self):
P = self.cls P = self.cls
with self.assertRaises(NotImplementedError): with self.assertRaises(pathlib.UnsupportedOperation):
P('c:/').owner() P('c:/').owner()
def test_group(self): def test_group(self):
P = self.cls P = self.cls
with self.assertRaises(NotImplementedError): with self.assertRaises(pathlib.UnsupportedOperation):
P('c:/').group() P('c:/').group()
@ -2055,6 +2061,13 @@ class PathTest(unittest.TestCase):
with self.assertRaises(OSError): with self.assertRaises(OSError):
(P / 'fileA').readlink() (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): def _check_resolve(self, p, expected, strict=True):
q = p.resolve(strict) q = p.resolve(strict)
self.assertEqual(q, expected) self.assertEqual(q, expected)
@ -2343,7 +2356,7 @@ class PathTest(unittest.TestCase):
if self.cls._flavour is os.path: if self.cls._flavour is os.path:
self.skipTest("path flavour is supported") self.skipTest("path flavour is supported")
else: else:
self.assertRaises(NotImplementedError, self.cls) self.assertRaises(pathlib.UnsupportedOperation, self.cls)
def _test_cwd(self, p): def _test_cwd(self, p):
q = self.cls(os.getcwd()) q = self.cls(os.getcwd())
@ -2543,12 +2556,12 @@ class PathTest(unittest.TestCase):
self.assertTrue(link2.exists()) self.assertTrue(link2.exists())
@unittest.skipIf(hasattr(os, "link"), "os.link() is present") @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 = self.cls(BASE)
p = P / 'fileA' p = P / 'fileA'
# linking to another path. # linking to another path.
q = P / 'dirA' / 'fileAA' q = P / 'dirA' / 'fileAA'
with self.assertRaises(NotImplementedError): with self.assertRaises(pathlib.UnsupportedOperation):
q.hardlink_to(p) q.hardlink_to(p)
def test_rename(self): def test_rename(self):
@ -2776,6 +2789,15 @@ class PathTest(unittest.TestCase):
self.assertTrue(link.is_dir()) self.assertTrue(link.is_dir())
self.assertTrue(list(link.iterdir())) 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): def test_is_junction(self):
P = self.cls(BASE) 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.