Description
Currently for each fixture which depends on another fixture, a "finalizer" is added to the list of the dependent fixture.
For example:
def test(tmpdir):
pass
tmpdir
, as we know, depends on tmpdir_path
to create the temporary directory. Each tmpdir
invocation ends up adding its finalization to the list of finalizers of tmpdir_path
. This is the mechanism used to finalize fixtures in the correct order (as thought we still have bugs in this area, as #1895 shows for example), and ensures that every tmpdir
will be destroyed before the requested tmpdir_path
fixture.
This then means that every high-scoped fixture might contain dozens, hundreds or thousands of "finalizers" attached to them. Fixture finalizers can be called multiple times without problems, but this consumes memory: each finalizer keeps its SubRequest
object alive, containing a number of small variables:
pytest/src/_pytest/fixtures.py
Lines 341 to 359 in ed68fcf
This can easily be demonstrated by applying this patch:
diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py
index 55dcd805..b3a94bc6 100644
--- a/src/_pytest/runner.py
+++ b/src/_pytest/runner.py
@@ -393,6 +393,8 @@ class SetupState(object):
for col in self.stack:
if hasattr(col, "_prepare_exc"):
six.reraise(*col._prepare_exc)
+ if self.stack:
+ print(len(self._finalizers.get(self.stack[0])))
for col in needed_collectors[len(self.stack) :]:
self.stack.append(col)
try:
(this prints the finalizers attached to the "Session" node, where the session fixtures attach their finalization to)
And running this test:
import pytest
@pytest.mark.parametrize('i', range(10))
def test(i, tmpdir):
pass
λ pytest .tmp\test-foo.py -qs
.1
.2
.3
.4
.5
.6
.7
.8
.9
.
10 passed in 0.16 seconds
I believe we can think of ways to refactor the fixture teardown mechanism to avoid this accumulation of finalizers on the objects. Ideally we should build a proper DAG of fixture dependencies which should be destroyed in the proper order. This would also make things more explicit and easier to follow IMHO.