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

gh-58451: Add optional delete_on_close parameter to NamedTemporaryFile #97015

Merged
merged 26 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3df957c
Brought the same changes as in now closed PR 22431, but to the latest…
Ev2geny Sep 21, 2022
c4cf39f
Based on the review from @eryksun the code and the documentation is u…
Ev2geny Sep 22, 2022
ac0b241
documentation is slightly corrected
Ev2geny Sep 22, 2022
384614f
minor documentation update
Ev2geny Sep 22, 2022
0e0b6ba
Update Doc/whatsnew/3.12.rst
Ev2geny Sep 22, 2022
cc81ab5
Deletion of file upon finalization is moved from _TemporaryFileWrappe…
Ev2geny Sep 23, 2022
5af6150
Some additions to Lib/test/test_tempfile.py
Ev2geny Sep 24, 2022
239354e
clearing of the files in the situation when delete and not delete_on_…
Ev2geny Sep 24, 2022
5de68c0
if statement is added to avoid errors, when testing. Not sure 100% is…
Ev2geny Sep 24, 2022
eef3439
Apply suggestions from code review
Ev2geny Sep 25, 2022
c9801ee
__del__ was defined twice in _TemporaryFileWrapper. Fixed now
Ev2geny Sep 25, 2022
6944ab2
_TemporaryFileCloser is returned to original state and the logic, whi…
Ev2geny Sep 25, 2022
44f0664
All delete operations are moved to the class _TemporaryFileCloser
Ev2geny Sep 25, 2022
1dfaf72
Minor update to comments
Ev2geny Sep 25, 2022
e54feb5
Whitespec fixes by patchcheck.py
Ev2geny Sep 25, 2022
d2d1119
Update Lib/tempfile.py
Ev2geny Sep 26, 2022
bd4b2dd
Update Lib/tempfile.py
Ev2geny Sep 26, 2022
29eeaff
Modified exception handling in NamedTemporaryFile
Ev2geny Sep 26, 2022
5306b6d
Wrapping at 80 characters is fixed
Ev2geny Sep 26, 2022
1c99fb3
Update Lib/tempfile.py
Ev2geny Oct 1, 2022
29b89d3
Documentation update, based on the feedback from @zooba
Ev2geny Oct 1, 2022
e586028
Minor PEP8 update based on the feedback from @zooba
Ev2geny Oct 1, 2022
3eceb45
Improving comments in the init test file, based on the feedback from …
Ev2geny Oct 1, 2022
892e87d
Updating of documentation
Ev2geny Oct 2, 2022
67996b2
Merge branch 'fix-issue-14243' of https://github.com/Ev2geny/cpython …
Ev2geny Oct 2, 2022
07b963e
Apply suggestions from code review
zooba Oct 4, 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
82 changes: 68 additions & 14 deletions Doc/library/tempfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,58 @@ 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:

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this line was here before, but it doesn't make any sense to me. Should we mention this somewhere else?

Copy link
Contributor Author

@Ev2geny Ev2geny Oct 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eryksun , I think zooba talks about the line "(on Unix, the directory entry is not unlinked)." What do you think, does it make scene to move it somewhere else?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On POSIX, a file created by TemoraryFile() does not have a visible name in the filesystem. The implementation tries to use the O_TMPFILE flag if it's supported (Linux 3.11+), which creates an anonymous file. If O_TMPFILE isn't supported, the file is created with a unique name, and then it's immediately unlinked while the file descriptor is still open.

On Windows, TemporaryFile is aliased to NamedTemporaryFile. There isn't a compelling reason for this decision. It's fine to delete a file that's opened with the O_TEMPORARY flag. On Windows 10 and 11, if it's on an NTFS filesystem, the file gets unlinked immediately. Prior to Windows 10, or on filesystems other than NTFS, the file is deleted but not unlinked. In the latter state, a file has a visible entry in the directory, but it can't be opened again for any access, and it gets unlinked as soon as the last handle to it is closed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eryksun , thanks for explanation. So, would you agree, that the statement on Unix, the directory entry is not unlinked does not really make too much scene the way it is written now? Would it be better to either remove it completely or to put a broader explanation, similar to what you provided?

Copy link
Contributor

@eryksun eryksun Oct 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's in the context of clarifying that NamedTemporaryFile() acts "exactly" like TemporaryFile(). In that context it makes sense. But I'd prefer that the first sentence and the two bullet points were replaced with something like the following:

   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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have now placed the statement about unlinking on Unix to a different place and slightly reworked it to make a reference to TemporaryFile(), without which I agree it was a bit confusing.

I have also implemented rewording from @eryksun

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

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.

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 open the closed file
second time, either make sure not to delete the file upon closure (set the
zooba marked this conversation as resolved.
Show resolved Hide resolved
*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.
zooba marked this conversation as resolved.
Show resolved Hide resolved

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 to open the file again while it is open make sure that at least
one of the following conditions are fulfilled:
zooba marked this conversation as resolved.
Show resolved Hide resolved

* *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 +136,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 +387,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 @@ -142,6 +142,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):
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved
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()
Ev2geny marked this conversation as resolved.
Show resolved Hide resolved


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*