Skip to content

Commit

Permalink
gh-58451: Add optional delete_on_close parameter to NamedTemporaryFile (
Browse files Browse the repository at this point in the history
  • Loading branch information
Ev2geny authored Oct 4, 2022
1 parent bbc7cd6 commit 743453a
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 51 deletions.
85 changes: 71 additions & 14 deletions Doc/library/tempfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,61 @@ 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.
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.
.. 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:

* This function returns a file that is guaranteed to have a visible name in
the file system.
* To manage the named file, it extends the parameters of
:func:`TemporaryFile` with *delete* and *delete_on_close* parameters that
determine whether and how the named file should be automatically deleted.

The returned object is always a :term:`file-like object` whose :attr:`!file`
attribute is the underlying true file object. This :term:`file-like object`
can be used in a :keyword:`with` statement, just like a normal file. The
name of the temporary file can be retrieved from the :attr:`name` attribute
of the returned file-like object. On Unix, unlike with the
:func:`TemporaryFile`, the directory entry does not get unlinked immediately
after the file creation.

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
only, or else when the :term:`file-like object` is finalized. Deletion is not
always guaranteed in this case (see :meth:`object.__del__`). If *delete* is
false, the value of *delete_on_close* is ignored.

Therefore to use the name of the temporary file to reopen the file after
closing it, 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* parameter to be false.
The latter approach is recommended as it provides assistance in automatic
cleaning of the temporary file upon the context manager exit.

Opening the temporary file again by its name while it is still open works as
follows:

* On POSIX the file can always be opened again.
* On Windows, make sure that at least one of the following conditions are
fulfilled:

* *delete* is false
* additional open shares delete access (e.g. by calling :func:`os.open`
with the flag ``O_TEMPORARY``)
* *delete* is true but *delete_on_close* is false. Note, that in this
case the additional opens that do not share delete access (e.g.
created via builtin :func:`open`) must be closed before exiting the
context manager, else the :func:`os.unlink` call on context manager
exit will fail with a :exc:`PermissionError`.

On 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 :exc:`PermissionError`.
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 +139,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 +390,19 @@ 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
# 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.close()
# the file is closed, but not removed
# open the file again by using its name
... with open(fp.name) as f
... f.read()
b'Hello world!'
>>>
# 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 @@ -148,6 +148,11 @@ unicodedata
* The Unicode database has been updated to version 15.0.0. (Contributed by
Benjamin Peterson in :gh:`96734`).

tempfile
--------

The :class:`tempfile.NamedTemporaryFile` function has a new optional parameter
*delete_on_close* (Contributed by Evgeny Zorin in :gh:`58451`.)

Optimizations
=============
Expand Down
76 changes: 40 additions & 36 deletions Lib/tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,42 +418,42 @@ class _TemporaryFileCloser:
underlying file object, without adding a __del__ method to the
temporary file."""

file = None # Set here since __del__ checks it
cleanup_called = False
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
# file.name is useful (i.e. not "(fdopen)") with NamedTemporaryFile.
if _os.name != 'nt':
# Cache the unlinker so we don't get spurious errors at
# shutdown when the module-level "os" is None'd out. Note
# that this must be referenced as self.unlink, because the
# name TemporaryFileWrapper may also get None'd out before
# __del__ is called.

def close(self, unlink=_os.unlink):
if not self.close_called and self.file is not None:
self.close_called = True
try:
def cleanup(self, windows=(_os.name == 'nt'), unlink=_os.unlink):
if not self.cleanup_called:
self.cleanup_called = True
try:
if not self.close_called:
self.close_called = True
self.file.close()
finally:
if self.delete:
finally:
# Windows provides delete-on-close as a primitive, in which
# case the file was deleted by self.file.close().
if self.delete and not (windows and self.delete_on_close):
try:
unlink(self.name)
except FileNotFoundError:
pass

# Need to ensure the file is deleted on __del__
def __del__(self):
self.close()

else:
def close(self):
if not self.close_called:
self.close_called = True
def close(self):
if not self.close_called:
self.close_called = True
try:
self.file.close()
finally:
if self.delete and self.delete_on_close:
self.cleanup()

def __del__(self):
self.cleanup()


class _TemporaryFileWrapper:
Expand All @@ -464,11 +464,11 @@ 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._closer = _TemporaryFileCloser(file, name, delete,
delete_on_close)

def __getattr__(self, name):
# Attribute lookups are delegated to the underlying file
Expand Down Expand Up @@ -499,7 +499,7 @@ def __enter__(self):
# deleted when used in a with statement
def __exit__(self, exc, value, tb):
result = self.file.__exit__(exc, value, tb)
self.close()
self._closer.cleanup()
return result

def close(self):
Expand All @@ -518,18 +518,21 @@ def __iter__(self):
for line in self.file:
yield line


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 automatically deleted (default True).
'delete_on_close' -- if 'delete', whether the file is deleted on close
(default True) or otherwise either on context manager exit
(if context manager was used) or on object finalization. .
'errors' -- the errors argument to io.open (default None)
The file is created as mkstemp() would do it.
Expand All @@ -548,7 +551,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 @@ -567,12 +570,13 @@ def opener(*args):
raw = getattr(file, 'buffer', file)
raw = getattr(raw, 'raw', raw)
raw.name = name
return _TemporaryFileWrapper(file, name, delete)
return _TemporaryFileWrapper(file, name, delete, delete_on_close)
except:
file.close()
raise
except:
if name is not None and not (_os.name == 'nt' and delete):
if name is not None and not (
_os.name == 'nt' and delete and delete_on_close):
_os.unlink(name)
raise

Expand Down
100 changes: 99 additions & 1 deletion Lib/test/test_tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import stat
import types
import weakref
import gc
from unittest import mock

import unittest
Expand Down Expand Up @@ -1013,6 +1014,102 @@ def use_closed():
pass
self.assertRaises(ValueError, use_closed)

def test_context_man_not_del_on_close_if_delete_on_close_false(self):
# Issue gh-58451: tempfile.NamedTemporaryFile is not particulary useful
# on Windows
# A NamedTemporaryFile is NOT deleted when closed if
# delete_on_close=False, but is deleted on context 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),
f"NamedTemporaryFile {f.name!r} is incorrectly "
f"deleted on closure when delete_on_close=False")

with self.subTest():
# Testing that file is deleted on context manager exit
self.assertFalse(os.path.exists(f.name),
f"NamedTemporaryFile {f.name!r} exists "
f"after context manager exit")

finally:
os.rmdir(dir)

def test_context_man_ok_to_delete_manually(self):
# In the case of delete=True, a NamedTemporaryFile can be manually
# deleted in a with-statement context without causing 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_context_man_not_del_if_delete_false(self):
# A NamedTemporaryFile is not deleted if delete = False
dir = tempfile.mkdtemp()
f_name = ""
try:
# Test that delete_on_close=True has no 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),
f"NamedTemporaryFile {f.name!r} exists after close")
finally:
os.unlink(f_name)
os.rmdir(dir)

def test_del_by_finalizer(self):
# A NamedTemporaryFile is deleted when finalized in the case of
# delete=True, delete_on_close=False, and no with-statement is used.
def my_func(dir):
f = tempfile.NamedTemporaryFile(dir=dir, delete=True,
delete_on_close=False)
tmp_name = f.name
f.write(b'blat')
# Testing extreme case, where the file is not explicitly closed
# f.close()
return tmp_name
# Make sure that the garbage collector has finalized the file object.
gc.collect()
dir = tempfile.mkdtemp()
try:
tmp_name = my_func(dir)
self.assertFalse(os.path.exists(tmp_name),
f"NamedTemporaryFile {tmp_name!r} "
f"exists after finalizer ")
finally:
os.rmdir(dir)

def test_correct_finalizer_work_if_already_deleted(self):
# There should be no error in the case of delete=True,
# delete_on_close=False, no with-statement is used, and the file is
# deleted manually.
def my_func(dir)->str:
f = tempfile.NamedTemporaryFile(dir=dir, delete=True,
delete_on_close=False)
tmp_name = f.name
f.write(b'blat')
f.close()
os.unlink(tmp_name)
return tmp_name
# Make sure that the garbage collector has finalized the file object.
gc.collect()

def test_bad_mode(self):
dir = tempfile.mkdtemp()
self.addCleanup(os_helper.rmtree, dir)
Expand Down Expand Up @@ -1081,7 +1178,8 @@ def test_iobase_interface(self):
missing_attrs = iobase_attrs - spooledtempfile_attrs
self.assertFalse(
missing_attrs,
'SpooledTemporaryFile missing attributes from IOBase/BufferedIOBase/TextIOBase'
'SpooledTemporaryFile missing attributes from '
'IOBase/BufferedIOBase/TextIOBase'
)

def test_del_on_close(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The :class:`tempfile.NamedTemporaryFile` function has a new optional parameter *delete_on_close*

0 comments on commit 743453a

Please sign in to comment.