diff --git a/AUTHORS b/AUTHORS index 1a8c5306f..ce993637d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -285,6 +285,7 @@ Roberto Polli Roland Puntaier Romain Dorgueil Roman Bolshakov +Roni Kishner Ronny Pfannschmidt Ross Lawley Ruaridh Williamson diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index 080138774..22990ea52 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -730,6 +730,11 @@ Here's how the previous example would look using the ``addfinalizer`` method: It's a bit longer than yield fixtures and a bit more complex, but it does offer some nuances for when you're in a pinch. +In addition you can use the remove_finalizer method to remove a finalizer you added +to the teardown stage. +The remove_finalizer method will remove the first finalizer match it finds and return True, +if no match was found the function will return None. + .. code-block:: pytest $ pytest -q test_emaillib.py diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index ee3e93f19..e9f718b86 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -760,6 +760,11 @@ class SubRequest(FixtureRequest): within the requesting test context finished execution.""" self._fixturedef.addfinalizer(finalizer) + def remove_finalizer(self, finalizer: Callable[[], object]) -> None: + """Remove finalizer/teardown function to be called after the last test + within the requesting test context finished execution.""" + return self._fixturedef.remove_finalizer(finalizer) + def _schedule_finalizers( self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" ) -> None: @@ -1003,6 +1008,12 @@ class FixtureDef(Generic[FixtureValue]): def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._finalizers.append(finalizer) + def remove_finalizer(self, finalizer: Callable[[], object]) -> None: + for finalizer_index, finalizer_func in enumerate(self._finalizers): + if finalizer_func.__qualname__ == finalizer.__qualname__: + del self._finalizers[finalizer_index] + return True + def finish(self, request: SubRequest) -> None: exc = None try: diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index df6eecdb1..039c02cfb 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -503,6 +503,20 @@ class SetupState: assert node in self.stack, (node, self.stack) self.stack[node][0].append(finalizer) + def remove_finalizer(self, finalizer: Callable[[], object], node: Node) -> None: + """Remove a finalizer in the given node by name. + + The node must be currently active in the stack. + The first finalizer by name will be removed. + """ + assert node and not isinstance(node, tuple) + assert callable(finalizer) + assert node in self.stack, (node, self.stack) + for finalizer_index, finalizer_func in enumerate(self.stack[node][0]): + if finalizer_func.__qualname__ == finalizer.__qualname__: + del self.stack[node][0][finalizer_index] + return True + def teardown_exact(self, nextitem: Optional[Item]) -> None: """Teardown the current stack up until reaching nodes that nextitem also descends from. diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 3ce5cb34d..98d7aaa91 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -857,11 +857,33 @@ class TestRequestBasic: parent = item.getparent(pytest.Module) assert parent is not None teardownlist = parent.obj.teardownlist + item.session._setupstate.teardown_exact(None) + assert teardownlist == [1] + + def test_request_remove_finalizer(self, pytester: Pytester) -> None: + item = pytester.getitem( + """ + import pytest + teardownlist = [] + @pytest.fixture + def something(request): + request.addfinalizer(lambda: teardownlist.append(1)) + request.remove_finalizer(lambda: teardownlist.append(1)) + def test_func(something): pass + """ + ) + assert isinstance(item, Function) + item.session._setupstate.setup(item) + item._request._fillfixtures() + # successively check finalization calls + parent = item.getparent(pytest.Module) + assert parent is not None + teardownlist = parent.obj.teardownlist ss = item.session._setupstate assert not teardownlist ss.teardown_exact(None) print(ss.stack) - assert teardownlist == [1] + assert teardownlist == [] def test_request_addfinalizer_failing_setup(self, pytester: Pytester) -> None: pytester.makepyfile(