[Python-Dev] Fun with ExitStack
Barry Warsaw
barry at python.org
Mon Jul 18 19:40:05 EDT 2016
I was trying to debug a problem in some work code and I ran into some
interesting oddities with contextlib.ExitStack and other context managers in
Python 3.5.
This program creates a temporary directory, and I wanted to give it a --keep
flag to not automatically delete the tempdir at program exit. I was using an
ExitStack to manage a bunch of resources, and the temporary directory is the
first thing pushed into the ExitStack. At that point in the program, I check
the value of --keep and if it's set, I use ExitStack.pop_all() to clear the
stack, and thus, presumably, prevent the temporary directory from being
deleted.
There's this relevant quote in the contextlib documentation:
"""
Each instance [of an ExitStack] maintains a stack of registered callbacks that
are called in reverse order when the instance is closed (either explicitly or
implicitly at the end of a with statement). Note that callbacks are not
invoked implicitly when the context stack instance is garbage collected.
"""
However if I didn't save the reference to the pop_all'd ExitStack, the tempdir
would be immediately deleted. If I did save a reference to the pop_all'd
ExitStack, the tempdir would live until the saved reference went out of scope
and got refcounted away.
As best I can tell this happens because TemporaryDirectory.__init__() creates
a weakref finalizer which ends up calling the _cleanup() function. Although
it's rather difficult to trace, it does appear that when the ExitStack is
gc'd, this finalizer gets triggered (via weakref.detach()), thus causing the
_cleanup() method to be called and the tmpdir to get deleted. I "fix" this by
doing:
def __init__(self):
tmpdir = TemporaryDirectory()
self._tmpdir = (tmpdir.name if keep
else self.resources.enter_context(tmpdir))
There must be more to the story because when __init__() exits in the --keep
case, tmpdir should have gotten refcounted away and the directory deleted, but
it doesn't. I haven't dug down deep enough to figure that out.
Now, while I was debugging that behavior, I ran across more interesting bits.
I put this in a file to drive some tests:
------snip snip-----
with ExitStack() as resources:
print('enter context')
tmpdir = resources.enter_context(X())
resources.pop_all()
print('exit context')
------snip snip-----
Let's say X is:
class X:
def __enter__(self):
print('enter Foo')
return self
def __exit__(self, *args, **kws):
print('exit Foo')
return False
the output is:
enter context
enter Foo
exit context
So far so good. A fairly standard context manager class doesn't get its
__exit__() called even when the program exits. Let's try this:
@contextmanager
def X():
print('enter bar')
yield
print('exit bar')
still good:
enter context
enter bar
exit context
Let's modify X a little bit to be a more common idiom:
@contextmanager
def X():
print('enter foo')
try:
yield
finally:
print('exit foo')
enter context
enter foo
exit foo
exit context
Ah, the try-finally changes the behavior! There's probably some documentation
somewhere that defines how a generator gets finalized, and that triggers the
finally clause, whereas in the previous example, nothing after the yield gets
run. I just can't find that anything that would describe the observed
behavior.
It's all very twisty, and I'm not sure Python is doing anything wrong, but I'm
also not sure it's *not* doing anything wrong. ;)
In any case, the contextlib documentation quoted above should probably be more
liberally sprinkled with salty caveats. Just calling .pop_all() isn't
necessarily enough to ensure that resources managed by an ExitStack will
survive its garbage collection.
Cheers,
-Barry
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 819 bytes
Desc: OpenPGP digital signature
URL: <http://mail.python.org/pipermail/python-dev/attachments/20160718/701ddf41/attachment.sig>
More information about the Python-Dev
mailing list