Skip to content

Commit 78e70be

Browse files
pR0PsGFerniemethane
authored
gh-70363: Implement io.IOBase interface for SpooledTemporaryFile (GH-29560)
Since the underlying file-like objects (either `io.BytesIO`, or a true file object) all implement the `io.IOBase` interface, the `SpooledTemporaryFile` should as well. Additionally, since the underlying file object will either be an instance of an `io.BufferedIOBase` (for binary mode) or an `io.TextIOBase` (for text mode), methods for these classes were also implemented. In every case, the required methods and properties are simply delegated to the underlying file object. Co-authored-by: Gary Fernie <Gary.Fernie@skyscanner.net> Co-authored-by: Inada Naoki <songofacandy@gmail.com>
1 parent 52dc9c3 commit 78e70be

File tree

5 files changed

+92
-3
lines changed

5 files changed

+92
-3
lines changed

Doc/library/tempfile.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ The module defines the following user-callable items:
123123
.. versionchanged:: 3.8
124124
Added *errors* parameter.
125125

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

127132
.. class:: TemporaryDirectory(suffix=None, prefix=None, dir=None, ignore_cleanup_errors=False)
128133

Lib/tempfile.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ def TemporaryFile(mode='w+b', buffering=-1, encoding=None,
639639
_os.close(fd)
640640
raise
641641

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

707+
def __del__(self):
708+
if not self.closed:
709+
_warnings.warn(
710+
"Unclosed file {!r}".format(self),
711+
ResourceWarning,
712+
stacklevel=2,
713+
source=self
714+
)
715+
self.close()
716+
707717
def close(self):
708718
self._file.close()
709719

@@ -747,15 +757,30 @@ def name(self):
747757
def newlines(self):
748758
return self._file.newlines
749759

760+
def readable(self):
761+
return self._file.readable()
762+
750763
def read(self, *args):
751764
return self._file.read(*args)
752765

766+
def read1(self, *args):
767+
return self._file.read1(*args)
768+
769+
def readinto(self, b):
770+
return self._file.readinto(b)
771+
772+
def readinto1(self, b):
773+
return self._file.readinto1(b)
774+
753775
def readline(self, *args):
754776
return self._file.readline(*args)
755777

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

781+
def seekable(self):
782+
return self._file.seekable()
783+
759784
def seek(self, *args):
760785
return self._file.seek(*args)
761786

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

765790
def truncate(self, size=None):
766791
if size is None:
767-
self._file.truncate()
792+
return self._file.truncate()
768793
else:
769794
if size > self._max_size:
770795
self.rollover()
771-
self._file.truncate(size)
796+
return self._file.truncate(size)
797+
798+
def writable(self):
799+
return self._file.writable()
772800

773801
def write(self, s):
774802
file = self._file
@@ -782,6 +810,9 @@ def writelines(self, iterable):
782810
self._check(file)
783811
return rv
784812

813+
def detach(self):
814+
return self._file.detach()
815+
785816

786817
class TemporaryDirectory:
787818
"""Create and return a temporary directory. This has the same

Lib/test/test_tempfile.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,30 @@ def test_basic(self):
10611061
f = self.do_create(max_size=100, pre="a", suf=".txt")
10621062
self.assertFalse(f._rolled)
10631063

1064+
def test_is_iobase(self):
1065+
# SpooledTemporaryFile should implement io.IOBase
1066+
self.assertIsInstance(self.do_create(), io.IOBase)
1067+
1068+
def test_iobase_interface(self):
1069+
# SpooledTemporaryFile should implement the io.IOBase interface.
1070+
# Ensure it has all the required methods and properties.
1071+
iobase_attrs = {
1072+
# From IOBase
1073+
'fileno', 'seek', 'truncate', 'close', 'closed', '__enter__',
1074+
'__exit__', 'flush', 'isatty', '__iter__', '__next__', 'readable',
1075+
'readline', 'readlines', 'seekable', 'tell', 'writable',
1076+
'writelines',
1077+
# From BufferedIOBase (binary mode) and TextIOBase (text mode)
1078+
'detach', 'read', 'read1', 'write', 'readinto', 'readinto1',
1079+
'encoding', 'errors', 'newlines',
1080+
}
1081+
spooledtempfile_attrs = set(dir(tempfile.SpooledTemporaryFile))
1082+
missing_attrs = iobase_attrs - spooledtempfile_attrs
1083+
self.assertFalse(
1084+
missing_attrs,
1085+
'SpooledTemporaryFile missing attributes from IOBase/BufferedIOBase/TextIOBase'
1086+
)
1087+
10641088
def test_del_on_close(self):
10651089
# A SpooledTemporaryFile is deleted when closed
10661090
dir = tempfile.mkdtemp()
@@ -1076,6 +1100,30 @@ def test_del_on_close(self):
10761100
finally:
10771101
os.rmdir(dir)
10781102

1103+
def test_del_unrolled_file(self):
1104+
# The unrolled SpooledTemporaryFile should raise a ResourceWarning
1105+
# when deleted since the file was not explicitly closed.
1106+
f = self.do_create(max_size=10)
1107+
f.write(b'foo')
1108+
self.assertEqual(f.name, None) # Unrolled so no filename/fd
1109+
with self.assertWarns(ResourceWarning):
1110+
f.__del__()
1111+
1112+
def test_del_rolled_file(self):
1113+
# The rolled file should be deleted when the SpooledTemporaryFile
1114+
# object is deleted. This should raise a ResourceWarning since the file
1115+
# was not explicitly closed.
1116+
f = self.do_create(max_size=2)
1117+
f.write(b'foo')
1118+
name = f.name # This is a fd on posix+cygwin, a filename everywhere else
1119+
self.assertTrue(os.path.exists(name))
1120+
with self.assertWarns(ResourceWarning):
1121+
f.__del__()
1122+
self.assertFalse(
1123+
os.path.exists(name),
1124+
"Rolled SpooledTemporaryFile (name=%s) exists after delete" % name
1125+
)
1126+
10791127
def test_rewrite_small(self):
10801128
# A SpooledTemporaryFile can be written to multiple within the max_size
10811129
f = self.do_create(max_size=30)

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,7 @@ Dimitri Merejkowsky
11721172
Brian Merrell
11731173
Bruce Merry
11741174
Alexis Métaireau
1175+
Carey Metcalfe
11751176
Luke Mewburn
11761177
Carl Meyer
11771178
Kyle Meyer
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fully implement the :class:`io.BufferedIOBase` or :class:`io.TextIOBase`
2+
interface for :class:`tempfile.SpooledTemporaryFile` objects. This lets them
3+
work correctly with higher-level layers (like compression modules). Patch by
4+
Carey Metcalfe.

0 commit comments

Comments
 (0)