[Python-checkins] bpo-39682: make `pathlib.Path` immutable by removing (undocumented) support for "closing" a path by using it as a context manager (GH-18846)

Barney Gale webhook-mailer at python.org
Wed Apr 1 10:11:01 EDT 2020


https://github.com/python/cpython/commit/00002e6d8b0ccdb6e0d9e98a9a7f9c9edfdf1311
commit: 00002e6d8b0ccdb6e0d9e98a9a7f9c9edfdf1311
branch: master
author: Barney Gale <barney.gale at gmail.com>
committer: GitHub <noreply at github.com>
date: 2020-04-01T16:10:51+02:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2020-03-08-11-00-01.bpo-39682.AxXZNz.rst
M Lib/pathlib.py
M Lib/test/test_pathlib.py

diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index 293462000c2a6..96b8b59530c72 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 @@ def _init(self,
               # Private non-constructor arguments
               template=None,
               ):
-        self._closed = False
         if template is not None:
             self._accessor = template._accessor
         else:
@@ -1071,15 +1069,18 @@ def _make_child_relpath(self, part):
         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 @@ def _raw_open(self, flags, mode=0o777):
         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 @@ def iterdir(self):
         """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 @@ def absolute(self):
         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 @@ def resolve(self, strict=False):
         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 @@ def open(self, mode='r', buffering=-1, encoding=None,
         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 @@ def touch(self, mode=0o666, exist_ok=True):
         """
         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 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
         """
         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 @@ def chmod(self, mode):
         """
         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 @@ def lchmod(self, mode):
         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 @@ def unlink(self, missing_ok=False):
         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 @@ def rmdir(self):
         """
         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 @@ def lstat(self):
         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 @@ def rename(self, target):
         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 @@ def replace(self, target):
         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 @@ def symlink_to(self, target, target_is_directory=False):
         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 5b362a0ff3d9b..8b19cffba37e9 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -1720,13 +1720,15 @@ def test_with(self):
         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 0000000000000..d71a32132af9d
--- /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.



More information about the Python-checkins mailing list