diff --git a/Lib/tempfile.py b/Lib/tempfile.py --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -27,6 +27,7 @@ This module also provides some data item # Imports. +import functools as _functools import warnings as _warnings import sys as _sys import io as _io @@ -349,13 +350,10 @@ def mktemp(suffix="", prefix=template, d "No usable temporary filename found") -class _TemporaryFileWrapper: - """Temporary file wrapper - - This class provides a wrapper around files opened for - temporary use. In particular, it seeks to automatically - remove the file when it is no longer needed. - """ +class _TemporaryFileCloser: + """A separate object allowing proper closing of a temporary file's + underlying file object, without adding a __del__ method to the + temporary file.""" def __init__(self, file, name, delete=True): self.file = file @@ -363,26 +361,6 @@ class _TemporaryFileWrapper: self.close_called = False self.delete = delete - def __getattr__(self, name): - # Attribute lookups are delegated to the underlying file - # and cached for non-numeric results - # (i.e. methods are cached, closed and friends are not) - file = self.__dict__['file'] - a = getattr(file, name) - if not isinstance(a, int): - setattr(self, name, a) - return a - - # The underlying __enter__ method returns the wrong object - # (self.file) so override it to return the wrapper - def __enter__(self): - self.file.__enter__() - return self - - # iter() doesn't use __getattr__ to find the __iter__ method - def __iter__(self): - return iter(self.file) - # 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. @@ -401,18 +379,73 @@ class _TemporaryFileWrapper: if self.delete: self.unlink(self.name) + # Need to ensure the file is deleted on __del__ def __del__(self): self.close() - # Need to trap __exit__ as well to ensure the file gets - # deleted when used in a with statement - def __exit__(self, exc, value, tb): - result = self.file.__exit__(exc, value, tb) - self.close() - return result else: - def __exit__(self, exc, value, tb): - self.file.__exit__(exc, value, tb) + def close(self): + if not self.close_called: + self.close_called = True + self.file.close() + + +class _TemporaryFileWrapper: + """Temporary file wrapper + + This class provides a wrapper around files opened for + temporary use. In particular, it seeks to automatically + remove the file when it is no longer needed. + """ + + def __init__(self, file, name, delete=True): + self.file = file + self.name = name + self.delete = delete + self._closer = _TemporaryFileCloser(file, name, delete) + + def __getattr__(self, name): + # Attribute lookups are delegated to the underlying file + # and cached for non-numeric results + # (i.e. methods are cached, closed and friends are not) + file = self.__dict__['file'] + a = getattr(file, name) + if hasattr(a, '__call__'): + func = a + @_functools.wraps(func) + def func_wrapper(*args, **kwargs): + # This is to prevent self getting out of scope too early, + # see issue #18879 + return func(*args, **kwargs) + # Avoid closing the file as long as the wrapper is alive + func_wrapper._closer = self._closer + a = func_wrapper + if not isinstance(a, int): + setattr(self, name, a) + return a + + # The underlying __enter__ method returns the wrong object + # (self.file) so override it to return the wrapper + def __enter__(self): + self.file.__enter__() + return self + + # Need to trap __exit__ as well to ensure the file gets + # deleted when used in a with statement + def __exit__(self, exc, value, tb): + result = self.file.__exit__(exc, value, tb) + self.close() + return result + + def close(self): + """ + Close the temporary file, possibly deleting it. + """ + self._closer.close() + + # iter() doesn't use __getattr__ to find the __iter__ method + def __iter__(self): + return iter(self.file) def NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None, diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -8,6 +8,7 @@ import sys import re import warnings import contextlib +import weakref import unittest from test import support @@ -674,6 +675,22 @@ class TestNamedTemporaryFile(BaseTestCas self.do_create(pre="a", suf="b") self.do_create(pre="aa", suf=".txt") + def test_method_lookup(self): + # Issue #18879: Looking up a temporary file method should keep it + # alive long enough. + f = self.do_create() + wr = weakref.ref(f) + write = f.write + write2 = f.write + del f + write(b'foo') + del write + write2(b'bar') + del write2 + if support.check_impl_detail(cpython=True): + # No reference cycle was created. + self.assertIsNone(wr()) + def test_creates_named(self): # NamedTemporaryFile creates files with names f = tempfile.NamedTemporaryFile()