Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-14243: Optionally delete NamedTemporaryFile on content manager exit but not on file close #22431

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
144db7f
inial update. Tests do not work yet
Ev2geny Sep 27, 2020
21f7df0
delete_on_close flag is added to NamedTemporaryFile functionality
Ev2geny Sep 27, 2020
2d7927b
possibility for user to delete temporary file is added without conten…
Ev2geny Sep 27, 2020
4fcddaf
typo in test name is fixed
Ev2geny Sep 27, 2020
76f28f3
📜🤖 Added by blurb_it.
blurb-it[bot] Sep 28, 2020
6be9be6
whatsnew and News file is updated
Ev2geny Sep 28, 2020
faf1a34
bro-14243: update to NEWS.d
Ev2geny Sep 28, 2020
589d0b8
Update Lib/test/test_tempfile.py
Ev2geny May 27, 2022
e22a3db
Doc/whatsnew/3.10.rst is restored from the main. changes will no go …
Ev2geny May 29, 2022
3be2f47
Documentation update
Ev2geny May 29, 2022
39e8b50
Minor documentation update
Ev2geny May 29, 2022
4d8f152
Training whitespaces removed in Doc/library/tempfile.rst
Ev2geny May 30, 2022
9ec55f5
New line is added to the end of the news file
Ev2geny May 30, 2022
d6f382e
Update Doc/library/tempfile.rst
Ev2geny May 30, 2022
f9dc427
Update Doc/whatsnew/3.12.rst
Ev2geny May 30, 2022
e29b7d2
Update Doc/whatsnew/3.12.rst
Ev2geny May 31, 2022
d003a39
Documentation in tempfile.rst is updated based on the feedback from @…
Ev2geny May 31, 2022
e2fc6db
Merge branch 'fix-issue-14243' of https://github.com/Ev2geny/cpython …
Ev2geny May 31, 2022
3ac4e20
Information in whatsnew is moved to the section Improved modules base…
Ev2geny May 31, 2022
653cf27
Update Doc/whatsnew/3.12.rst
Ev2geny Jun 7, 2022
d5d07f4
Update Doc/library/tempfile.rst
Ev2geny Jun 9, 2022
0b32b28
Update Doc/library/tempfile.rst
Ev2geny Jun 9, 2022
5773e71
wrapping at 80 characters is implemented on the Doc/library/tempfile.rs
Ev2geny Jun 9, 2022
d98f270
Update Doc/whatsnew/3.12.rst
Ev2geny Sep 17, 2022
9598e6b
Update Doc/library/tempfile.rst
Ev2geny Sep 17, 2022
5a7e177
Restored (probably accidentry removed by @eryksun's suggestion) parag…
Ev2geny Sep 17, 2022
7e7ad85
Additional minor changes to documentation, based on suggestions from …
Ev2geny Sep 17, 2022
4d5677e
Update Lib/tempfile.py
Ev2geny Sep 17, 2022
cf5b1b5
Update Lib/tempfile.py
Ev2geny Sep 17, 2022
7fd339a
Update Lib/tempfile.py
Ev2geny Sep 17, 2022
da71a96
Update Lib/tempfile.py
Ev2geny Sep 17, 2022
5e22c9c
Merge branch 'fix-issue-14243' of https://github.com/Ev2geny/cpython …
Ev2geny Sep 17, 2022
b1f5152
Update Doc/library/tempfile.rst
Ev2geny Sep 17, 2022
07076e8
Merged with master and resolved merge conflicsts in Doc/whatsnew/3.12…
Ev2geny Sep 18, 2022
a0a8ae3
Revert "Merged with master and resolved merge conflicsts in Doc/whats…
Ev2geny Sep 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions Doc/library/tempfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,54 @@ The module defines the following user-callable items:
Added *errors* parameter.


.. function:: NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None, newline=None, suffix=None, prefix=None, dir=None, delete=True, *, errors=None)

This function operates exactly as :func:`TemporaryFile` does, except that
the file is guaranteed to have a visible name in the file system (on
Unix, the directory entry is not unlinked). That name can be retrieved
from the :attr:`name` attribute of the returned
file-like object. Whether the name can be
used to open the file a second time, while the named temporary file is
still open, varies across platforms (it can be so used on Unix; it cannot
on Windows). If *delete* is true (the default), the file is
deleted as soon as it is closed.
.. function:: NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None, newline=None, suffix=None, prefix=None, dir=None, delete=True, *, errors=None, delete_on_close=True)

This function operates exactly as :func:`TemporaryFile` does, except the
following differences:

* The file is guaranteed to have a visible name in the file system (on Unix,
the directory entry is not unlinked).

* There is more granularity in the deletion behaviour of the file (see
``delete_on_close`` below)

The returned object is always a file-like object whose :attr:`!file`
attribute is the underlying true file object. This file-like object can
be used in a :keyword:`with` statement, just like a normal file.
attribute is the underlying true file object. This file-like object can be
used in a :keyword:`with` statement, just like a normal file.The name of the
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
temporary file can be retrieved from the :attr:`name` attribute of the
returned file-like object.

If *delete* is true (the default) and *delete_on_close* is true (the
default), the file is deleted as soon as it is closed. If *delete* is true
and *delete_on_close* is false, the file is deleted on context manager exit
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
only. If *delete* is false, the value of *delete_on_close* is ignored.

Whether the name of the temporary file can be used to open the file a second
time, while the named temporary file is still open, varies across platforms:

* It can be so used on Unix.
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved

* In Windows, the file can be opened again if *delete_on_close* is false or
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
if the open shares delete access (e.g. by calling :func:`os.open` with the
flag ``O_TEMPORARY``). If *delete_on_close* is false, and the file is
opened again without sharing delete access (e.g. via builtin :func:`open`),
then the second open must be closed before exiting the context manager,
else the :func:`os.unlink` call on context manager exit will fail with a
``PermissionError``.

To use the name of the temporary file to open the closed file second time,
either make sure not to delete the file upon closure (set the *delete*
parameter to be false) or, in case the temporary file is created in a
:keyword:`with` statement, set the *delete_on_close* to be false. The latter
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
approach is recommended as it provides assistance in automatic cleaning of
the temporary file upon the context manager exit.
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved

In Windows, if *delete_on_close* is false, and the file is created in a
directory for which the user lacks delete access, then the :func:`os.unlink`
call on exit of the context manager will fail with a ``PermissionError``.
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
This cannot happen when *delete_on_close* is true because delete access is
requested by the open, which fails immediately if the requested access is not
granted.

On POSIX (only), a process that is terminated abruptly with SIGKILL
cannot automatically delete any NamedTemporaryFiles it created.
Expand All @@ -98,6 +132,9 @@ The module defines the following user-callable items:
.. versionchanged:: 3.8
Added *errors* parameter.

.. versionchanged:: 3.12
Added *delete_on_close* parameter.


.. class:: SpooledTemporaryFile(max_size=0, mode='w+b', buffering=-1, encoding=None, newline=None, suffix=None, prefix=None, dir=None, *, errors=None)

Expand Down Expand Up @@ -346,6 +383,22 @@ Here are some examples of typical usage of the :mod:`tempfile` module::
>>>
# file is now closed and removed

# create a temporary file using a context manager, note the name,
# close the file, use the name to open the file again
>>> with tempfile.TemporaryFile(delete_on_close=False) as fp:
... fp.write(b'Hello world!')
... fp_name = fp.name
... fp.close()
# the file is closed, but not removed
# open the file again by using its name
... f = open(fp_name)
... f.seek(0)
... f.read()
b'Hello world!'
... f.close()
>>>
# file is now removed

# create a temporary directory using the context manager
>>> with tempfile.TemporaryDirectory() as tmpdirname:
... print('created temporary directory', tmpdirname)
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ New Modules
Improved Modules
================

tempfile
--------

The :class:`tempfile.NamedTemporaryFile` has a new optional parameter
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
*delete_on_close* (Contributed by Evgeny Zorin in :gh:`58451`.)

Optimizations
=============
Expand Down
34 changes: 26 additions & 8 deletions Lib/tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,11 @@ class _TemporaryFileCloser:
file = None # Set here since __del__ checks it
close_called = False

def __init__(self, file, name, delete=True):
def __init__(self, file, name, delete=True, delete_on_close=True):
self.file = file
self.name = name
self.delete = delete
self.delete_on_close = delete_on_close

# NT provides delete-on-close as a primitive, so we don't need
# the wrapper to do anything special. We still use it so that
Expand All @@ -442,7 +443,7 @@ def close(self, unlink=_os.unlink):
try:
self.file.close()
finally:
if self.delete:
if self.delete and self.delete_on_close:
unlink(self.name)

# Need to ensure the file is deleted on __del__
Expand All @@ -464,11 +465,13 @@ class _TemporaryFileWrapper:
remove the file when it is no longer needed.
"""

def __init__(self, file, name, delete=True):
def __init__(self, file, name, delete=True, delete_on_close=True):
self.file = file
self.name = name
self.delete = delete
self._closer = _TemporaryFileCloser(file, name, delete)
self.delete_on_close = delete_on_close
self._closer = _TemporaryFileCloser(file, name, delete,
delete_on_close)

def __getattr__(self, name):
# Attribute lookups are delegated to the underlying file
Expand Down Expand Up @@ -500,6 +503,15 @@ def __enter__(self):
def __exit__(self, exc, value, tb):
result = self.file.__exit__(exc, value, tb)
self.close()
# if file is to be deleted, bot not on closure, deleting it now
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
if self.delete and not self.delete_on_close:
try:
_os.unlink(self.name)
# It shall be Ok to ignore FileNotFoundError, because user may
# have deleted the file already before content manager came to it
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
except FileNotFoundError:
pass

return result

def close(self):
Expand All @@ -521,17 +533,23 @@ def __iter__(self):

def NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None,
newline=None, suffix=None, prefix=None,
dir=None, delete=True, *, errors=None):
dir=None, delete=True, *, errors=None,
delete_on_close=True):
"""Create and return a temporary file.
Arguments:
'prefix', 'suffix', 'dir' -- as for mkstemp.
'mode' -- the mode argument to io.open (default "w+b").
'buffering' -- the buffer size argument to io.open (default -1).
'encoding' -- the encoding argument to io.open (default None)
'newline' -- the newline argument to io.open (default None)
'delete' -- whether the file is deleted on close (default True).
'delete' -- whether the file is deleted at the end. When exactly file gets
deleted (either on close or on context manager exit) is determined by
parameter delete_on_close. (default True).
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
'errors' -- the errors argument to io.open (default None)
The file is created as mkstemp() would do it.
'delete_on_close' -- if 'delete', then determines whether file gets deleted
on close. Otherwise it gets deleted on context manager exit
(default True)
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved

Returns an object with a file-like interface; the name of the file
is accessible as its 'name' attribute. The file will be automatically
Expand All @@ -548,7 +566,7 @@ def NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None,

# Setting O_TEMPORARY in the flags causes the OS to delete
# the file when it is closed. This is only supported by Windows.
if _os.name == 'nt' and delete:
if _os.name == 'nt' and delete and delete_on_close:
flags |= _os.O_TEMPORARY

if "b" not in mode:
Expand All @@ -559,7 +577,7 @@ def NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None,
file = _io.open(fd, mode, buffering=buffering,
newline=newline, encoding=encoding, errors=errors)

return _TemporaryFileWrapper(file, name, delete)
return _TemporaryFileWrapper(file, name, delete, delete_on_close)
except BaseException:
_os.unlink(name)
_os.close(fd)
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/test_tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,65 @@ def test_del_on_close(self):
finally:
os.rmdir(dir)

def test_not_del_on_close_if_delete_on_close_false(self):
# A NamedTemporaryFile is NOT deleted when closed if
# delete_on_close= False, but is deleted on content manager exit
dir = tempfile.mkdtemp()
try:
with tempfile.NamedTemporaryFile(dir=dir,
delete=True,
delete_on_close=False) as f:
f.write(b'blat')
f_name = f.name
f.close()
with self.subTest():
# Testing that file is not deleted on close
self.assertTrue(os.path.exists(f.name),
"NamedTemporaryFile %s is incorrectly deleted\
on closure when delete_on_close= False" % f_name)

with self.subTest():
# Testing that file is deleted on content manager exit
self.assertFalse(os.path.exists(f.name),
"NamedTemporaryFile %s exists\
after content manager exit" % f.name)

finally:
os.rmdir(dir)

def test_ok_to_delete_manually(self):
# A NamedTemporaryFile can be deleted by a user before content manager
# comes to it. This will not generate an error

dir = tempfile.mkdtemp()
try:
with tempfile.NamedTemporaryFile(dir=dir,
delete=True,
delete_on_close=False) as f:
f.write(b'blat')
f.close()
os.unlink(f.name)

finally:
os.rmdir(dir)

def test_not_del_if_delete_false(self):
# A NamedTemporaryFile is not deleted if delete = False
dir = tempfile.mkdtemp()
f_name = ""
try:
# setting delete_on_close = True to test, that this does not have
# an effect, if delete = False
with tempfile.NamedTemporaryFile(dir=dir, delete=False,
delete_on_close=True) as f:
f.write(b'blat')
f_name = f.name
self.assertTrue(os.path.exists(f.name),
"NamedTemporaryFile %s exists after close" % f.name)
finally:
os.unlink(f_name)
os.rmdir(dir)

def test_dis_del_on_close(self):
# Tests that delete-on-close can be disabled
dir = tempfile.mkdtemp()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The :class:`tempfile.NamedTemporaryFile` has a new optional parameter *delete_on_close*