Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
5 changes: 5 additions & 0 deletions Doc/library/tempfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ The module defines the following user-callable items:
.. versionchanged:: 3.8
Added *errors* parameter.

.. versionchanged:: 3.11
Fully implements the :class:`io.BufferedIOBase` and
:class:`io.TextIOBase` abstract base classes (depending on whether binary
or text *mode* was specified).


.. function:: TemporaryDirectory(suffix=None, prefix=None, dir=None, ignore_cleanup_errors=False)

Expand Down
37 changes: 34 additions & 3 deletions Lib/tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ def TemporaryFile(mode='w+b', buffering=-1, encoding=None,
_os.close(fd)
raise

class SpooledTemporaryFile:
class SpooledTemporaryFile(_io.IOBase):
"""Temporary file wrapper, specialized to switch from BytesIO
or StringIO to a real file when it exceeds a certain size or
when a fileno is needed.
Expand Down Expand Up @@ -704,6 +704,16 @@ def __exit__(self, exc, value, tb):
def __iter__(self):
return self._file.__iter__()

def __del__(self):
if not self.closed:
_warnings.warn(
"Unclosed file {!r}".format(self),
ResourceWarning,
stacklevel=2,
source=self
)
self.close()

def close(self):
self._file.close()

Expand Down Expand Up @@ -747,15 +757,30 @@ def name(self):
def newlines(self):
return self._file.newlines

def readable(self):
return self._file.readable()

def read(self, *args):
return self._file.read(*args)

def read1(self, *args):
return self._file.read1(*args)

def readinto(self, b):
return self._file.readinto(b)

def readinto1(self, b):
return self._file.readinto1(b)

def readline(self, *args):
return self._file.readline(*args)

def readlines(self, *args):
return self._file.readlines(*args)

def seekable(self):
return self._file.seekable()

def seek(self, *args):
return self._file.seek(*args)

Expand All @@ -764,11 +789,14 @@ def tell(self):

def truncate(self, size=None):
if size is None:
self._file.truncate()
return self._file.truncate()
else:
if size > self._max_size:
self.rollover()
self._file.truncate(size)
return self._file.truncate(size)

def writable(self):
return self._file.writable()

def write(self, s):
file = self._file
Expand All @@ -782,6 +810,9 @@ def writelines(self, iterable):
self._check(file)
return rv

def detach(self):
return self._file.detach()


class TemporaryDirectory:
"""Create and return a temporary directory. This has the same
Expand Down
48 changes: 48 additions & 0 deletions Lib/test/test_tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,30 @@ def test_basic(self):
f = self.do_create(max_size=100, pre="a", suf=".txt")
self.assertFalse(f._rolled)

def test_is_iobase(self):
# SpooledTemporaryFile should implement io.IOBase
self.assertIsInstance(self.do_create(), io.IOBase)

def test_iobase_interface(self):
# SpooledTemporaryFile should implement the io.IOBase interface.
# Ensure it has all the required methods and properties.
iobase_attrs = {
# From IOBase
'fileno', 'seek', 'truncate', 'close', 'closed', '__enter__',
'__exit__', 'flush', 'isatty', '__iter__', '__next__', 'readable',
'readline', 'readlines', 'seekable', 'tell', 'writable',
'writelines',
# From BufferedIOBase (binary mode) and TextIOBase (text mode)
'detach', 'read', 'read1', 'write', 'readinto', 'readinto1',
'encoding', 'errors', 'newlines',
}
spooledtempfile_attrs = set(dir(tempfile.SpooledTemporaryFile))
missing_attrs = iobase_attrs - spooledtempfile_attrs
self.assertFalse(
missing_attrs,
'SpooledTemporaryFile missing attributes from IOBase/BufferedIOBase/TextIOBase'
)

def test_del_on_close(self):
# A SpooledTemporaryFile is deleted when closed
dir = tempfile.mkdtemp()
Expand All @@ -1073,6 +1097,30 @@ def test_del_on_close(self):
finally:
os.rmdir(dir)

def test_del_unrolled_file(self):
# The unrolled SpooledTemporaryFile should raise a ResourceWarning
# when deleted since the file was not explicitly closed.
f = self.do_create(max_size=10)
f.write(b'foo')
self.assertEqual(f.name, None) # Unrolled so no filename/fd
with self.assertWarns(ResourceWarning):
f.__del__()

def test_del_rolled_file(self):
# The rolled file should be deleted when the SpooledTemporaryFile
# object is deleted. This should raise a ResourceWarning since the file
# was not explicitly closed.
f = self.do_create(max_size=2)
f.write(b'foo')
name = f.name # This is a fd on posix+cygwin, a filename everywhere else
self.assertTrue(os.path.exists(name))
with self.assertWarns(ResourceWarning):
f.__del__()
self.assertFalse(
os.path.exists(name),
"Rolled SpooledTemporaryFile (name=%s) exists after delete" % name
)

def test_rewrite_small(self):
# A SpooledTemporaryFile can be written to multiple within the max_size
f = self.do_create(max_size=30)
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,7 @@ Dimitri Merejkowsky
Brian Merrell
Bruce Merry
Alexis Métaireau
Carey Metcalfe
Luke Mewburn
Carl Meyer
Kyle Meyer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fully implement the :class:`io.BufferedIOBase` or :class:`io.TextIOBase`
interface for :class:`tempfile.SpooledTemporaryFile` objects. This lets them
work correctly with higher-level layers (like compression modules). Patch by
Carey Metcalfe.