Clear the copy of the globs dict after running examples. This helps to
break cycles, which are a special problem when running generator tests
that provoke exceptions by invoking the .next() method of a named
generator-iterator: then the iterator is named in globs, and the
iterator's frame gets a tracekback object pointing back to globs, and
gc doesn't chase these types so the cycle leaks.
Also changed _run_examples() to make a copy of globs itself, so its
callers (direct and indirect) don't have to (and changed the callers
to stop making their own copies); *that* much is a change I've been
meaning to make for a long time (it's more robust the new way).
Here's a way to provoke the symptom without doctest; it leaks at a
prodigious rate; if the last two "source" lines are replaced with
g().next()
the iterator isn't named and then there's no leak:
source = """\
def g():
yield 1/0
k = g()
k.next()
"""
code = compile(source, "<source>", "exec")
def f(globs):
try:
exec code in globs
except ZeroDivisionError:
pass
while 1:
f(globals().copy())
After this change, running test_generators in an infinite loop still leaks,
but reduced from a flood to a trickle.
diff --git a/Lib/doctest.py b/Lib/doctest.py
index f83de6c..fb0334c 100644
--- a/Lib/doctest.py
+++ b/Lib/doctest.py
@@ -529,23 +529,34 @@
return failures, len(examples)
-# Run list of examples, in context globs. Return (#failures, #tries).
+# Run list of examples, in a shallow copy of context (dict) globs.
+# Return (#failures, #tries).
+# CAUTION: globs is cleared before returning. This is to help break
+# cycles that may have been created by the examples.
def _run_examples(examples, globs, verbose, name):
import sys
saveout = sys.stdout
+ globs = globs.copy()
try:
sys.stdout = fakeout = _SpoofOut()
x = _run_examples_inner(saveout.write, fakeout, examples,
globs, verbose, name)
finally:
sys.stdout = saveout
+ # While Python gc can clean up most cycles on its own, it doesn't
+ # chase frame objects. This is especially irksome when running
+ # generator tests that raise exceptions, because a named generator-
+ # iterator gets an entry in globs, and the generator-iterator
+ # object's frame's traceback info points back to globs. This is
+ # easy to break just by clearing the namespace.
+ globs.clear()
return x
def run_docstring_examples(f, globs, verbose=0, name="NoName"):
"""f, globs, verbose=0, name="NoName" -> run examples from f.__doc__.
- Use dict globs as the globals for execution.
+ Use (a shallow copy of) dict globs as the globals for execution.
Return (#failures, #tries).
If optional arg verbose is true, print stuff even if there are no
@@ -735,7 +746,7 @@
f = t = 0
e = _extract_examples(s)
if e:
- f, t = _run_examples(e, self.globs.copy(), self.verbose, name)
+ f, t = _run_examples(e, self.globs, self.verbose, name)
if self.verbose:
print f, "of", t, "examples failed in string", name
self.__record_outcome(name, f, t)
@@ -773,8 +784,7 @@
"when object.__name__ doesn't exist; " + `object`)
if self.verbose:
print "Running", name + ".__doc__"
- f, t = run_docstring_examples(object, self.globs.copy(),
- self.verbose, name)
+ f, t = run_docstring_examples(object, self.globs, self.verbose, name)
if self.verbose:
print f, "of", t, "examples failed in", name + ".__doc__"
self.__record_outcome(name, f, t)