[Python-checkins] gh-70363: Implement `io.IOBase` interface for `SpooledTemporaryFile` (GH-29560)

methane webhook-mailer at python.org
Tue May 3 08:18:31 EDT 2022


https://github.com/python/cpython/commit/78e70be3318bc2ca57dac188061ed35017a0867c
commit: 78e70be3318bc2ca57dac188061ed35017a0867c
branch: main
author: Carey Metcalfe <carey at cmetcalfe.ca>
committer: methane <songofacandy at gmail.com>
date: 2022-05-03T21:18:18+09:00
summary:

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 at skyscanner.net>
Co-authored-by: Inada Naoki <songofacandy at gmail.com>

files:
A Misc/NEWS.d/next/Library/2021-11-14-01-35-04.bpo-26175.LNlOfI.rst
M Doc/library/tempfile.rst
M Lib/tempfile.py
M Lib/test/test_tempfile.py
M Misc/ACKS

diff --git a/Doc/library/tempfile.rst b/Doc/library/tempfile.rst
index c97d119fb8b5e..8fe38b5e81c08 100644
--- a/Doc/library/tempfile.rst
+++ b/Doc/library/tempfile.rst
@@ -123,6 +123,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).
+
 
 .. class:: TemporaryDirectory(suffix=None, prefix=None, dir=None, ignore_cleanup_errors=False)
 
diff --git a/Lib/tempfile.py b/Lib/tempfile.py
index 531cbf32f1283..eb870c998c251 100644
--- a/Lib/tempfile.py
+++ b/Lib/tempfile.py
@@ -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.
@@ -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()
 
@@ -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)
 
@@ -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
@@ -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
diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py
index a6847189dca46..07a54028ec697 100644
--- a/Lib/test/test_tempfile.py
+++ b/Lib/test/test_tempfile.py
@@ -1061,6 +1061,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()
@@ -1076,6 +1100,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)
diff --git a/Misc/ACKS b/Misc/ACKS
index 30b698f90e83b..1efc6a07c6cad 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1172,6 +1172,7 @@ Dimitri Merejkowsky
 Brian Merrell
 Bruce Merry
 Alexis Métaireau
+Carey Metcalfe
 Luke Mewburn
 Carl Meyer
 Kyle Meyer
diff --git a/Misc/NEWS.d/next/Library/2021-11-14-01-35-04.bpo-26175.LNlOfI.rst b/Misc/NEWS.d/next/Library/2021-11-14-01-35-04.bpo-26175.LNlOfI.rst
new file mode 100644
index 0000000000000..89072b3c04f39
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-11-14-01-35-04.bpo-26175.LNlOfI.rst
@@ -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.



More information about the Python-checkins mailing list