Skip to content

Commit a800670

Browse files
authored
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()`.
1 parent 04492cb commit a800670

File tree

5 files changed

+91
-13
lines changed

5 files changed

+91
-13
lines changed

Doc/library/pathlib.rst

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ Opening a file::
8888
'#!/bin/bash\n'
8989

9090

91+
Exceptions
92+
----------
93+
94+
.. exception:: UnsupportedOperation
95+
96+
An exception inheriting :exc:`NotImplementedError` that is raised when an
97+
unsupported operation is called on a path object.
98+
99+
.. versionadded:: 3.13
100+
101+
91102
.. _pure-paths:
92103

93104
Pure paths
@@ -752,6 +763,11 @@ calls on path objects. There are three ways to instantiate concrete paths:
752763

753764
*pathsegments* is specified similarly to :class:`PurePath`.
754765

766+
.. versionchanged:: 3.13
767+
Raises :exc:`UnsupportedOperation` on Windows. In previous versions,
768+
:exc:`NotImplementedError` was raised instead.
769+
770+
755771
.. class:: WindowsPath(*pathsegments)
756772

757773
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:
762778

763779
*pathsegments* is specified similarly to :class:`PurePath`.
764780

781+
.. versionchanged:: 3.13
782+
Raises :exc:`UnsupportedOperation` on non-Windows platforms. In previous
783+
versions, :exc:`NotImplementedError` was raised instead.
784+
785+
765786
You can only instantiate the class flavour that corresponds to your system
766787
(allowing system calls on non-compatible path flavours could lead to
767788
bugs or failures in your application)::
@@ -778,7 +799,7 @@ bugs or failures in your application)::
778799
File "<stdin>", line 1, in <module>
779800
File "pathlib.py", line 798, in __new__
780801
% (cls.__name__,))
781-
NotImplementedError: cannot instantiate 'WindowsPath' on your system
802+
UnsupportedOperation: cannot instantiate 'WindowsPath' on your system
782803

783804

784805
Methods
@@ -952,6 +973,10 @@ call fails (for example because the path doesn't exist).
952973
Return the name of the group owning the file. :exc:`KeyError` is raised
953974
if the file's gid isn't found in the system database.
954975

976+
.. versionchanged:: 3.13
977+
Raises :exc:`UnsupportedOperation` if the :mod:`grp` module is not
978+
available. In previous versions, :exc:`NotImplementedError` was raised.
979+
955980

956981
.. method:: Path.is_dir()
957982

@@ -1210,6 +1235,10 @@ call fails (for example because the path doesn't exist).
12101235
Return the name of the user owning the file. :exc:`KeyError` is raised
12111236
if the file's uid isn't found in the system database.
12121237

1238+
.. versionchanged:: 3.13
1239+
Raises :exc:`UnsupportedOperation` if the :mod:`pwd` module is not
1240+
available. In previous versions, :exc:`NotImplementedError` was raised.
1241+
12131242

12141243
.. method:: Path.read_bytes()
12151244

@@ -1252,6 +1281,10 @@ call fails (for example because the path doesn't exist).
12521281

12531282
.. versionadded:: 3.9
12541283

1284+
.. versionchanged:: 3.13
1285+
Raises :exc:`UnsupportedOperation` if :func:`os.readlink` is not
1286+
available. In previous versions, :exc:`NotImplementedError` was raised.
1287+
12551288

12561289
.. method:: Path.rename(target)
12571290

@@ -1414,6 +1447,11 @@ call fails (for example because the path doesn't exist).
14141447
The order of arguments (link, target) is the reverse
14151448
of :func:`os.symlink`'s.
14161449

1450+
.. versionchanged:: 3.13
1451+
Raises :exc:`UnsupportedOperation` if :func:`os.symlink` is not
1452+
available. In previous versions, :exc:`NotImplementedError` was raised.
1453+
1454+
14171455
.. method:: Path.hardlink_to(target)
14181456

14191457
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).
14241462

14251463
.. versionadded:: 3.10
14261464

1465+
.. versionchanged:: 3.13
1466+
Raises :exc:`UnsupportedOperation` if :func:`os.link` is not
1467+
available. In previous versions, :exc:`NotImplementedError` was raised.
1468+
14271469

14281470
.. method:: Path.touch(mode=0o666, exist_ok=True)
14291471

Doc/whatsnew/3.13.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ built on debug mode <debug-build>`.
106106
pathlib
107107
-------
108108

109+
* Add :exc:`pathlib.UnsupportedOperation`, which is raised instead of
110+
:exc:`NotImplementedError` when a path operation isn't supported.
111+
(Contributed by Barney Gale in :gh:`89812`.)
112+
109113
* Add support for recursive wildcards in :meth:`pathlib.PurePath.match`.
110114
(Contributed by Barney Gale in :gh:`73435`.)
111115

Lib/pathlib.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222

2323
__all__ = [
24+
"UnsupportedOperation",
2425
"PurePath", "PurePosixPath", "PureWindowsPath",
2526
"Path", "PosixPath", "WindowsPath",
2627
]
@@ -207,6 +208,13 @@ def _select_unique(paths):
207208
# Public API
208209
#
209210

211+
class UnsupportedOperation(NotImplementedError):
212+
"""An exception that is raised when an unsupported operation is called on
213+
a path object.
214+
"""
215+
pass
216+
217+
210218
class _PathParents(Sequence):
211219
"""This object provides sequence-like access to the logical ancestors
212220
of a path. Don't try to construct it yourself."""
@@ -1241,7 +1249,7 @@ def owner(self):
12411249
import pwd
12421250
return pwd.getpwuid(self.stat().st_uid).pw_name
12431251
except ImportError:
1244-
raise NotImplementedError("Path.owner() is unsupported on this system")
1252+
raise UnsupportedOperation("Path.owner() is unsupported on this system")
12451253

12461254
def group(self):
12471255
"""
@@ -1252,14 +1260,14 @@ def group(self):
12521260
import grp
12531261
return grp.getgrgid(self.stat().st_gid).gr_name
12541262
except ImportError:
1255-
raise NotImplementedError("Path.group() is unsupported on this system")
1263+
raise UnsupportedOperation("Path.group() is unsupported on this system")
12561264

12571265
def readlink(self):
12581266
"""
12591267
Return the path to which the symbolic link points.
12601268
"""
12611269
if not hasattr(os, "readlink"):
1262-
raise NotImplementedError("os.readlink() not available on this system")
1270+
raise UnsupportedOperation("os.readlink() not available on this system")
12631271
return self.with_segments(os.readlink(self))
12641272

12651273
def touch(self, mode=0o666, exist_ok=True):
@@ -1363,7 +1371,7 @@ def symlink_to(self, target, target_is_directory=False):
13631371
Note the order of arguments (link, target) is the reverse of os.symlink.
13641372
"""
13651373
if not hasattr(os, "symlink"):
1366-
raise NotImplementedError("os.symlink() not available on this system")
1374+
raise UnsupportedOperation("os.symlink() not available on this system")
13671375
os.symlink(target, self, target_is_directory)
13681376

13691377
def hardlink_to(self, target):
@@ -1373,7 +1381,7 @@ def hardlink_to(self, target):
13731381
Note the order of arguments (self, target) is the reverse of os.link's.
13741382
"""
13751383
if not hasattr(os, "link"):
1376-
raise NotImplementedError("os.link() not available on this system")
1384+
raise UnsupportedOperation("os.link() not available on this system")
13771385
os.link(target, self)
13781386

13791387
def expanduser(self):
@@ -1400,7 +1408,7 @@ class PosixPath(Path, PurePosixPath):
14001408

14011409
if os.name == 'nt':
14021410
def __new__(cls, *args, **kwargs):
1403-
raise NotImplementedError(
1411+
raise UnsupportedOperation(
14041412
f"cannot instantiate {cls.__name__!r} on your system")
14051413

14061414
class WindowsPath(Path, PureWindowsPath):
@@ -1412,5 +1420,5 @@ class WindowsPath(Path, PureWindowsPath):
14121420

14131421
if os.name != 'nt':
14141422
def __new__(cls, *args, **kwargs):
1415-
raise NotImplementedError(
1423+
raise UnsupportedOperation(
14161424
f"cannot instantiate {cls.__name__!r} on your system")

Lib/test/test_pathlib.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
grp = pwd = None
2525

2626

27+
class UnsupportedOperationTest(unittest.TestCase):
28+
def test_is_notimplemented(self):
29+
self.assertTrue(issubclass(pathlib.UnsupportedOperation, NotImplementedError))
30+
self.assertTrue(isinstance(pathlib.UnsupportedOperation(), NotImplementedError))
31+
32+
2733
# Make sure any symbolic links in the base test path are resolved.
2834
BASE = os.path.realpath(TESTFN)
2935
join = lambda *x: os.path.join(BASE, *x)
@@ -1550,12 +1556,12 @@ class WindowsPathAsPureTest(PureWindowsPathTest):
15501556

15511557
def test_owner(self):
15521558
P = self.cls
1553-
with self.assertRaises(NotImplementedError):
1559+
with self.assertRaises(pathlib.UnsupportedOperation):
15541560
P('c:/').owner()
15551561

15561562
def test_group(self):
15571563
P = self.cls
1558-
with self.assertRaises(NotImplementedError):
1564+
with self.assertRaises(pathlib.UnsupportedOperation):
15591565
P('c:/').group()
15601566

15611567

@@ -2055,6 +2061,13 @@ def test_readlink(self):
20552061
with self.assertRaises(OSError):
20562062
(P / 'fileA').readlink()
20572063

2064+
@unittest.skipIf(hasattr(os, "readlink"), "os.readlink() is present")
2065+
def test_readlink_unsupported(self):
2066+
P = self.cls(BASE)
2067+
p = P / 'fileA'
2068+
with self.assertRaises(pathlib.UnsupportedOperation):
2069+
q.readlink(p)
2070+
20582071
def _check_resolve(self, p, expected, strict=True):
20592072
q = p.resolve(strict)
20602073
self.assertEqual(q, expected)
@@ -2343,7 +2356,7 @@ def test_unsupported_flavour(self):
23432356
if self.cls._flavour is os.path:
23442357
self.skipTest("path flavour is supported")
23452358
else:
2346-
self.assertRaises(NotImplementedError, self.cls)
2359+
self.assertRaises(pathlib.UnsupportedOperation, self.cls)
23472360

23482361
def _test_cwd(self, p):
23492362
q = self.cls(os.getcwd())
@@ -2543,12 +2556,12 @@ def test_hardlink_to(self):
25432556
self.assertTrue(link2.exists())
25442557

25452558
@unittest.skipIf(hasattr(os, "link"), "os.link() is present")
2546-
def test_link_to_not_implemented(self):
2559+
def test_hardlink_to_unsupported(self):
25472560
P = self.cls(BASE)
25482561
p = P / 'fileA'
25492562
# linking to another path.
25502563
q = P / 'dirA' / 'fileAA'
2551-
with self.assertRaises(NotImplementedError):
2564+
with self.assertRaises(pathlib.UnsupportedOperation):
25522565
q.hardlink_to(p)
25532566

25542567
def test_rename(self):
@@ -2776,6 +2789,15 @@ def test_symlink_to(self):
27762789
self.assertTrue(link.is_dir())
27772790
self.assertTrue(list(link.iterdir()))
27782791

2792+
@unittest.skipIf(hasattr(os, "symlink"), "os.symlink() is present")
2793+
def test_symlink_to_unsupported(self):
2794+
P = self.cls(BASE)
2795+
p = P / 'fileA'
2796+
# linking to another path.
2797+
q = P / 'dirA' / 'fileAA'
2798+
with self.assertRaises(pathlib.UnsupportedOperation):
2799+
q.symlink_to(p)
2800+
27792801
def test_is_junction(self):
27802802
P = self.cls(BASE)
27812803

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :exc:`pathlib.UnsupportedOperation`, which is raised instead of
2+
:exc:`NotImplementedError` when a path operation isn't supported.

0 commit comments

Comments
 (0)