Skip to content

Commit

Permalink
pythonGH-89812: Add pathlib.UnsupportedOperation (pythonGH-105926)
Browse files Browse the repository at this point in the history
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()`.
  • Loading branch information
barneygale authored and bentasker committed Jun 23, 2023
1 parent b12f4ab commit 8fd3e13
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 13 deletions.
44 changes: 43 additions & 1 deletion Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)::
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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*.
Expand All @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.)

Expand Down
22 changes: 15 additions & 7 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@


__all__ = [
"UnsupportedOperation",
"PurePath", "PurePosixPath", "PureWindowsPath",
"Path", "PosixPath", "WindowsPath",
]
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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")
32 changes: 27 additions & 5 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :exc:`pathlib.UnsupportedOperation`, which is raised instead of
:exc:`NotImplementedError` when a path operation isn't supported.

0 comments on commit 8fd3e13

Please sign in to comment.