From 00002e6d8b0ccdb6e0d9e98a9a7f9c9edfdf1311 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Wed, 1 Apr 2020 15:10:51 +0100 Subject: [PATCH] bpo-39682: make `pathlib.Path` immutable by removing (undocumented) support for "closing" a path by using it as a context manager (GH-18846) Support for using a path as a context manager remains, and is now a no-op. --- Lib/pathlib.py | 51 ++++--------------- Lib/test/test_pathlib.py | 16 +++--- .../2020-03-08-11-00-01.bpo-39682.AxXZNz.rst | 3 ++ 3 files changed, 21 insertions(+), 49 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-03-08-11-00-01.bpo-39682.AxXZNz.rst diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 293462000c2..96b8b59530c 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1041,7 +1041,6 @@ class Path(PurePath): """ __slots__ = ( '_accessor', - '_closed', ) def __new__(cls, *args, **kwargs): @@ -1058,7 +1057,6 @@ class Path(PurePath): # Private non-constructor arguments template=None, ): - self._closed = False if template is not None: self._accessor = template._accessor else: @@ -1071,15 +1069,18 @@ class Path(PurePath): return self._from_parsed_parts(self._drv, self._root, parts) def __enter__(self): - if self._closed: - self._raise_closed() return self def __exit__(self, t, v, tb): - self._closed = True - - def _raise_closed(self): - raise ValueError("I/O operation on closed path") + # https://bugs.python.org/issue39682 + # In previous versions of pathlib, this method marked this path as + # closed; subsequent attempts to perform I/O would raise an IOError. + # This functionality was never documented, and had the effect of + # making Path objects mutable, contrary to PEP 428. In Python 3.9 the + # _closed attribute was removed, and this method made a no-op. + # This method and __enter__()/__exit__() should be deprecated and + # removed in the future. + pass def _opener(self, name, flags, mode=0o666): # A stub for the opener argument to built-in open() @@ -1090,8 +1091,6 @@ class Path(PurePath): Open the file pointed by this path and return a file descriptor, as os.open() does. """ - if self._closed: - self._raise_closed() return self._accessor.open(self, flags, mode) # Public API @@ -1125,15 +1124,11 @@ class Path(PurePath): """Iterate over the files in this directory. Does not yield any result for the special paths '.' and '..'. """ - if self._closed: - self._raise_closed() for name in self._accessor.listdir(self): if name in {'.', '..'}: # Yielding a path object for these makes little sense continue yield self._make_child_relpath(name) - if self._closed: - self._raise_closed() def glob(self, pattern): """Iterate over this subtree and yield all existing files (of any @@ -1170,8 +1165,6 @@ class Path(PurePath): Use resolve() to get the canonical path to a file. """ # XXX untested yet! - if self._closed: - self._raise_closed() if self.is_absolute(): return self # FIXME this must defer to the specific flavour (and, under Windows, @@ -1186,8 +1179,6 @@ class Path(PurePath): normalizing it (for example turning slashes into backslashes under Windows). """ - if self._closed: - self._raise_closed() s = self._flavour.resolve(self, strict=strict) if s is None: # No symlink resolution => for consistency, raise an error if @@ -1227,8 +1218,6 @@ class Path(PurePath): Open the file pointed by this path and return a file object, as the built-in open() function does. """ - if self._closed: - self._raise_closed() return io.open(self, mode, buffering, encoding, errors, newline, opener=self._opener) @@ -1278,8 +1267,6 @@ class Path(PurePath): """ Create this file with the given access mode, if it doesn't exist. """ - if self._closed: - self._raise_closed() if exist_ok: # First try to bump modification time # Implementation note: GNU touch uses the UTIME_NOW option of @@ -1301,8 +1288,6 @@ class Path(PurePath): """ Create a new directory at this given path. """ - if self._closed: - self._raise_closed() try: self._accessor.mkdir(self, mode) except FileNotFoundError: @@ -1320,8 +1305,6 @@ class Path(PurePath): """ Change the permissions of the path, like os.chmod(). """ - if self._closed: - self._raise_closed() self._accessor.chmod(self, mode) def lchmod(self, mode): @@ -1329,8 +1312,6 @@ class Path(PurePath): Like chmod(), except if the path points to a symlink, the symlink's permissions are changed, rather than its target's. """ - if self._closed: - self._raise_closed() self._accessor.lchmod(self, mode) def unlink(self, missing_ok=False): @@ -1338,8 +1319,6 @@ class Path(PurePath): Remove this file or link. If the path is a directory, use rmdir() instead. """ - if self._closed: - self._raise_closed() try: self._accessor.unlink(self) except FileNotFoundError: @@ -1350,8 +1329,6 @@ class Path(PurePath): """ Remove this directory. The directory must be empty. """ - if self._closed: - self._raise_closed() self._accessor.rmdir(self) def lstat(self): @@ -1359,16 +1336,12 @@ class Path(PurePath): Like stat(), except if the path points to a symlink, the symlink's status information is returned, rather than its target's. """ - if self._closed: - self._raise_closed() return self._accessor.lstat(self) def link_to(self, target): """ Create a hard link pointing to a path named target. """ - if self._closed: - self._raise_closed() self._accessor.link_to(self, target) def rename(self, target): @@ -1376,8 +1349,6 @@ class Path(PurePath): Rename this path to the given path, and return a new Path instance pointing to the given path. """ - if self._closed: - self._raise_closed() self._accessor.rename(self, target) return self.__class__(target) @@ -1387,8 +1358,6 @@ class Path(PurePath): destination if it exists, and return a new Path instance pointing to the given path. """ - if self._closed: - self._raise_closed() self._accessor.replace(self, target) return self.__class__(target) @@ -1397,8 +1366,6 @@ class Path(PurePath): Make this path a symlink pointing to the given path. Note the order of arguments (self, target) is the reverse of os.symlink's. """ - if self._closed: - self._raise_closed() self._accessor.symlink(target, self, target_is_directory) # Convenience functions for querying the stat results diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 5b362a0ff3d..8b19cffba37 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1720,13 +1720,15 @@ class _BasePathTest(object): next(it2) with p: pass - # I/O operation on closed path. - self.assertRaises(ValueError, next, it) - self.assertRaises(ValueError, next, it2) - self.assertRaises(ValueError, p.open) - self.assertRaises(ValueError, p.resolve) - self.assertRaises(ValueError, p.absolute) - self.assertRaises(ValueError, p.__enter__) + # Using a path as a context manager is a no-op, thus the following + # operations should still succeed after the context manage exits. + next(it) + next(it2) + p.exists() + p.resolve() + p.absolute() + with p: + pass def test_chmod(self): p = self.cls(BASE) / 'fileA' diff --git a/Misc/NEWS.d/next/Library/2020-03-08-11-00-01.bpo-39682.AxXZNz.rst b/Misc/NEWS.d/next/Library/2020-03-08-11-00-01.bpo-39682.AxXZNz.rst new file mode 100644 index 00000000000..d71a32132af --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-03-08-11-00-01.bpo-39682.AxXZNz.rst @@ -0,0 +1,3 @@ +Remove undocumented support for *closing* a `pathlib.Path` object via its +context manager. The context manager magic methods remain, but they are now a +no-op, making `Path` objects immutable.