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.
This commit is contained in:
Barney Gale 2020-04-01 15:10:51 +01:00 committed by GitHub
parent 975ac326ff
commit 00002e6d8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 21 additions and 49 deletions

View File

@ -1041,7 +1041,6 @@ class Path(PurePath):
""" """
__slots__ = ( __slots__ = (
'_accessor', '_accessor',
'_closed',
) )
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
@ -1058,7 +1057,6 @@ class Path(PurePath):
# Private non-constructor arguments # Private non-constructor arguments
template=None, template=None,
): ):
self._closed = False
if template is not None: if template is not None:
self._accessor = template._accessor self._accessor = template._accessor
else: else:
@ -1071,15 +1069,18 @@ class Path(PurePath):
return self._from_parsed_parts(self._drv, self._root, parts) return self._from_parsed_parts(self._drv, self._root, parts)
def __enter__(self): def __enter__(self):
if self._closed:
self._raise_closed()
return self return self
def __exit__(self, t, v, tb): def __exit__(self, t, v, tb):
self._closed = True # https://bugs.python.org/issue39682
# In previous versions of pathlib, this method marked this path as
def _raise_closed(self): # closed; subsequent attempts to perform I/O would raise an IOError.
raise ValueError("I/O operation on closed path") # 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): def _opener(self, name, flags, mode=0o666):
# A stub for the opener argument to built-in open() # 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, Open the file pointed by this path and return a file descriptor,
as os.open() does. as os.open() does.
""" """
if self._closed:
self._raise_closed()
return self._accessor.open(self, flags, mode) return self._accessor.open(self, flags, mode)
# Public API # Public API
@ -1125,15 +1124,11 @@ class Path(PurePath):
"""Iterate over the files in this directory. Does not yield any """Iterate over the files in this directory. Does not yield any
result for the special paths '.' and '..'. result for the special paths '.' and '..'.
""" """
if self._closed:
self._raise_closed()
for name in self._accessor.listdir(self): for name in self._accessor.listdir(self):
if name in {'.', '..'}: if name in {'.', '..'}:
# Yielding a path object for these makes little sense # Yielding a path object for these makes little sense
continue continue
yield self._make_child_relpath(name) yield self._make_child_relpath(name)
if self._closed:
self._raise_closed()
def glob(self, pattern): def glob(self, pattern):
"""Iterate over this subtree and yield all existing files (of any """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. Use resolve() to get the canonical path to a file.
""" """
# XXX untested yet! # XXX untested yet!
if self._closed:
self._raise_closed()
if self.is_absolute(): if self.is_absolute():
return self return self
# FIXME this must defer to the specific flavour (and, under Windows, # 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 normalizing it (for example turning slashes into backslashes under
Windows). Windows).
""" """
if self._closed:
self._raise_closed()
s = self._flavour.resolve(self, strict=strict) s = self._flavour.resolve(self, strict=strict)
if s is None: if s is None:
# No symlink resolution => for consistency, raise an error if # 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 Open the file pointed by this path and return a file object, as
the built-in open() function does. the built-in open() function does.
""" """
if self._closed:
self._raise_closed()
return io.open(self, mode, buffering, encoding, errors, newline, return io.open(self, mode, buffering, encoding, errors, newline,
opener=self._opener) opener=self._opener)
@ -1278,8 +1267,6 @@ class Path(PurePath):
""" """
Create this file with the given access mode, if it doesn't exist. Create this file with the given access mode, if it doesn't exist.
""" """
if self._closed:
self._raise_closed()
if exist_ok: if exist_ok:
# First try to bump modification time # First try to bump modification time
# Implementation note: GNU touch uses the UTIME_NOW option of # 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. Create a new directory at this given path.
""" """
if self._closed:
self._raise_closed()
try: try:
self._accessor.mkdir(self, mode) self._accessor.mkdir(self, mode)
except FileNotFoundError: except FileNotFoundError:
@ -1320,8 +1305,6 @@ class Path(PurePath):
""" """
Change the permissions of the path, like os.chmod(). Change the permissions of the path, like os.chmod().
""" """
if self._closed:
self._raise_closed()
self._accessor.chmod(self, mode) self._accessor.chmod(self, mode)
def lchmod(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 Like chmod(), except if the path points to a symlink, the symlink's
permissions are changed, rather than its target's. permissions are changed, rather than its target's.
""" """
if self._closed:
self._raise_closed()
self._accessor.lchmod(self, mode) self._accessor.lchmod(self, mode)
def unlink(self, missing_ok=False): def unlink(self, missing_ok=False):
@ -1338,8 +1319,6 @@ class Path(PurePath):
Remove this file or link. Remove this file or link.
If the path is a directory, use rmdir() instead. If the path is a directory, use rmdir() instead.
""" """
if self._closed:
self._raise_closed()
try: try:
self._accessor.unlink(self) self._accessor.unlink(self)
except FileNotFoundError: except FileNotFoundError:
@ -1350,8 +1329,6 @@ class Path(PurePath):
""" """
Remove this directory. The directory must be empty. Remove this directory. The directory must be empty.
""" """
if self._closed:
self._raise_closed()
self._accessor.rmdir(self) self._accessor.rmdir(self)
def lstat(self): def lstat(self):
@ -1359,16 +1336,12 @@ class Path(PurePath):
Like stat(), except if the path points to a symlink, the symlink's Like stat(), except if the path points to a symlink, the symlink's
status information is returned, rather than its target's. status information is returned, rather than its target's.
""" """
if self._closed:
self._raise_closed()
return self._accessor.lstat(self) return self._accessor.lstat(self)
def link_to(self, target): def link_to(self, target):
""" """
Create a hard link pointing to a path named target. Create a hard link pointing to a path named target.
""" """
if self._closed:
self._raise_closed()
self._accessor.link_to(self, target) self._accessor.link_to(self, target)
def rename(self, target): def rename(self, target):
@ -1376,8 +1349,6 @@ class Path(PurePath):
Rename this path to the given path, Rename this path to the given path,
and return a new Path instance pointing 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) self._accessor.rename(self, target)
return self.__class__(target) return self.__class__(target)
@ -1387,8 +1358,6 @@ class Path(PurePath):
destination if it exists, and return a new Path instance destination if it exists, and return a new Path instance
pointing to the given path. pointing to the given path.
""" """
if self._closed:
self._raise_closed()
self._accessor.replace(self, target) self._accessor.replace(self, target)
return self.__class__(target) return self.__class__(target)
@ -1397,8 +1366,6 @@ class Path(PurePath):
Make this path a symlink pointing to the given path. Make this path a symlink pointing to the given path.
Note the order of arguments (self, target) is the reverse of os.symlink's. 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) self._accessor.symlink(target, self, target_is_directory)
# Convenience functions for querying the stat results # Convenience functions for querying the stat results

View File

@ -1720,13 +1720,15 @@ class _BasePathTest(object):
next(it2) next(it2)
with p: with p:
pass pass
# I/O operation on closed path. # Using a path as a context manager is a no-op, thus the following
self.assertRaises(ValueError, next, it) # operations should still succeed after the context manage exits.
self.assertRaises(ValueError, next, it2) next(it)
self.assertRaises(ValueError, p.open) next(it2)
self.assertRaises(ValueError, p.resolve) p.exists()
self.assertRaises(ValueError, p.absolute) p.resolve()
self.assertRaises(ValueError, p.__enter__) p.absolute()
with p:
pass
def test_chmod(self): def test_chmod(self):
p = self.cls(BASE) / 'fileA' p = self.cls(BASE) / 'fileA'

View File

@ -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.