Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use session.config.hook instead of ihook. Fixes #2124 #2127

Merged
merged 17 commits into from
Nov 12, 2017

Conversation

malinoff
Copy link
Contributor

@malinoff malinoff commented Dec 10, 2016

  • Make sure to include one or more tests for your change;
  • Add yourself to AUTHORS;
  • Add a new entry to CHANGELOG.rst
    • Choose any open position to avoid merge conflicts with other PRs.
    • Add a link to the issue you are fixing (if any) using RST syntax.
    • The pytest team likes to have people to acknowledged in the CHANGELOG, so please add a thank note to yourself ("Thanks @user for the PR") and a link to your GitHub profile. It may sound weird thanking yourself, but otherwise a maintainer would have to do it manually before or after merging instead of just using GitHub's merge button. This makes it easier on the maintainers to merge PRs.

@coveralls
Copy link

Coverage Status

Coverage remained the same at 92.832% when pulling 522d59e on malinoff:fix-2124 into da40bcf on pytest-dev:master.

@@ -783,8 +783,8 @@ def execute(self, request):
self.finish()
assert not hasattr(self, "cached_result")

ihook = self._fixturemanager.session.ihook
return ihook.pytest_fixture_setup(fixturedef=self, request=request)
hook = self._fixturemanager.session.config.hook
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as i understand, at this place, config.hook is an incorrect choice, as it does not apply fs path filtering

@hpk42 please cross-check my concern

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't then session.gethookproxy(fspath) be used here instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus i think so

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus @RonnyPfannschmidt I disagree - session.gethookproxy(fspath) is specifically what I'm changing in this PR, because session.ihook does precisely session.gethookproxy(self.fspath) (https://github.com/pytest-dev/pytest/blob/master/_pytest/main.py#L267-L270) and it doesn't work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session.fspath is indeeed broken, you need the item fspath, which has a different value

@malinoff
Copy link
Contributor Author

@hpk42 please, review my PR. It blocks me.

@malinoff
Copy link
Contributor Author

malinoff commented Dec 20, 2016

@RonnyPfannschmidt @nicoddemus ok, I've changed pytest_fixture_setup hook call to use an item fspath (please, verify its correctness), but I'm not sure about pytest_fixture_post_finalizer - there is no request in there.

@coveralls
Copy link

Coverage Status

Coverage remained the same at 92.832% when pulling a63b34c on malinoff:fix-2124 into da40bcf on pytest-dev:master.

@RonnyPfannschmidt
Copy link
Member

@malinoff the scope of this one is unfortunately growing

a pr like this where one detail after another creeps up tends to be very frustrating for the contributor and i am very thankful that you patiently stick to it

i believe the request needs to be turned into a argument of the finish() method so that it can use it as needed

@nicoddemus
Copy link
Member

@malinoff @RonnyPfannschmidt gentle ping. 😁

@RonnyPfannschmidt
Copy link
Member

@nicoddemus i reviewed correct passing of the data - and it seems currently impossible, the complete fixture definition work-flow is completely broken as far as i can tell and it only works because it does the wrong thing which is mostly not wrong enough to be a problem

@hpk42 im deferring the complete issue to you

@nicoddemus
Copy link
Member

@RonnyPfannschmidt should we close this? The PR is now stale anyway...

@malinoff
Copy link
Contributor Author

This PR is not stale. I'm waiting for the guidance of how should I proceed.

@nicoddemus
Copy link
Member

This PR is not stale. I'm waiting for the guidance of how should I proceed.

Oh OK thanks for the quick response.

Sorry if my comment implied that you were somehow to blame; it as more of an observation that we have not seen activity on it over some months and I was under the impression that it would not get anymore attention.

@nicoddemus
Copy link
Member

Took the opportunity to take a further look into this, and I managed to pass request objects to finalize which I think is reasonable:

diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py
index f71f357..5ac210c 100644
--- a/_pytest/fixtures.py
+++ b/_pytest/fixtures.py
@@ -4,6 +4,7 @@ import inspect
 import sys
 import warnings

+import functools
 import py
 from py._code.code import FormattedExcinfo

@@ -521,7 +522,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
             val = fixturedef.execute(request=subrequest)
         finally:
             # if fixture function failed it might have registered finalizers
-            self.session._setupstate.addfinalizer(fixturedef.finish,
+            self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest),
                                                   subrequest.node)
         return val

@@ -742,7 +743,7 @@ class FixtureDef:
     def addfinalizer(self, finalizer):
         self._finalizer.append(finalizer)

-    def finish(self):
+    def finish(self, request):
         exceptions = []
         try:
             while self._finalizer:
@@ -757,7 +758,7 @@ class FixtureDef:
                 py.builtin._reraise(*e)

         finally:
-            ihook = self._fixturemanager.session.ihook
+            ihook = self._fixturemanager.session.gethookproxy(request.node.fspath)
             ihook.pytest_fixture_post_finalizer(fixturedef=self)
             # even if finalization fails, we invalidate
             # the cached fixture value
@@ -770,7 +771,7 @@ class FixtureDef:
         for argname in self.argnames:
             fixturedef = request._get_active_fixturedef(argname)
             if argname != "request":
-                fixturedef.addfinalizer(self.finish)
+                fixturedef.addfinalizer(functools.partial(self.finish, request))

         my_cache_key = request.param_index
         cached_result = getattr(self, "cached_result", None)
@@ -783,10 +784,10 @@ class FixtureDef:
                     return result
             # we have a previous but differently parametrized fixture instance
             # so we need to tear it down before creating a new one
-            self.finish()
+            self.finish(request)
             assert not hasattr(self, "cached_result")

-        ihook = self._fixturemanager.session.ihook
+        ihook = self._fixturemanager.session.gethookproxy(request.node.fspath)
         return ihook.pytest_fixture_setup(fixturedef=self, request=request)

     def __repr__(self):

This passes all tests for me on Python 3.6 and produces correct results on my short test below:

# conftest.py
import pytest

def pytest_fixture_post_finalizer():
    print('ROOT conftest: pytest_fixture_post_finalizer')

def pytest_fixture_setup():
    print('ROOT conftest: pytest_fixture_setup')

@pytest.fixture(scope='module')
def my_fixture():
    yield 'my_fixture value'

# tests/conftest.py
def pytest_fixture_post_finalizer():
    print('SUB conftest: pytest_fixture_post_finalizer')

def pytest_fixture_setup():
    print('SUB conftest: pytest_fixture_setup')

# tests/test_usage.py
def test_usage(my_fixture):
    print('TEST USAGE')

def test_foobar(my_fixture):
    print('TEST FOOBAR')

The hooks are called appropriately in the correct order:

SUB conftest: pytest_fixture_setup
ROOT conftest: pytest_fixture_setup
.TEST USAGE
.TEST FOOBAR
SUB conftest: pytest_fixture_post_finalizer
ROOT conftest: pytest_fixture_post_finalizer
SUB conftest: pytest_fixture_post_finalizer
ROOT conftest: pytest_fixture_post_finalizer

Before the patch:

test_usage.py ROOT conftest: pytest_fixture_setup
.TEST USAGE
.TEST FOOBAR
ROOT conftest: pytest_fixture_post_finalizer
ROOT conftest: pytest_fixture_post_finalizer

Finally, if we are fixing this, we should take the opportunity to pass request to pytest_fixture_post_finalizer as well.


This was not easy, took me a couple of hours of debugging to understand all the moving parts involved. The parts I think were hard for me to make sense while debugging was the relationship between FixtureDef, Request, SubRequest and SetupState: at first glance it seems finalization is centered and handled by the first three, while it is actually deferred by them to SetupState. I believe this is an artifact before there were proper fixtures and the user was required to manage "setup state" by hand (pre 2.0?).

Also surprising why finalize() is called so many times (once for each item which uses it).

I previously thought the pytest_runtest_protocol(item, nextitem) exists because it is responsible for destroying fixtures: knowing next item, the function can know which scopes should be torn down. This is not the case, and the destruction happens somewhat implicitly by stacks of callbacks. It works, but perhaps a more explicit approach would be easier to understand.

This implicit definition really tripped me while debugging pytest-dev#2127,
unfortunately hidden as it was in the middle of all the variable
declarations.

I think the explicit definition is much easier for the eyes and IDEs
to find.
Make SubRequest.addfinalizer an explicit method
@RonnyPfannschmidt
Copy link
Member

@nicoddemus looks good, the failure is unrelated, can you also resolve the conflicts by a merge ?`

@nicoddemus nicoddemus changed the base branch from master to features November 12, 2017 13:36
if hasattr(self, "cached_result"):
del self.cached_result
self._finalizers = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt I realized we should also do this at this point to avoid keeping instances of bound methods alive in case they are registered as finalizers and an exception occurred during teardown, do you agree?

@@ -499,8 +499,16 @@ def __init__(self, fspath, parent=None, config=None, session=None):
super(FSCollector, self).__init__(name, parent, config, session)
self.fspath = fspath

def _check_initialpaths_for_relpath(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I first merged with upstream/master while working here so this PR is bringing #2776 into features as well. Because it is a small patch I don't think this will get into the way of reviewing things so if you guys are OK with it I won't bother trying to fix history in this case.

@nicoddemus
Copy link
Member

nicoddemus commented Nov 12, 2017

Updated the CHANGELOG and changed the target branch to features because pytest_fixture_post_finalizer hook can now receive a request argument.

@coveralls
Copy link

coveralls commented Nov 12, 2017

Coverage Status

Coverage increased (+0.2%) to 92.832% when pulling a6f2d2d on malinoff:fix-2124 into d1af369 on pytest-dev:features.

@nicoddemus
Copy link
Member

Thanks @malinoff for all the work and patience on this. 👍

nicoddemus added a commit to nicoddemus/pytest that referenced this pull request Dec 13, 2017
The leak was caused by the (unused) `FixtureRequest._fixture_values`
cache.

This was introduced because the `partial` object (created to call
FixtureDef.finish() bound with the Request) is kept alive
through the entire session when a function-scoped fixture depends
on a session-scoped (or higher) fixture because of the nested
`addfinalizer` calls.

FixtureDef.finish() started receiving a request object in order to
obtain the proper hook proxy object (pytest-dev#2127), but this does not seem
useful at all in practice because `pytest_fixture_post_finalizer`
will be called with the `request` object of the moment the fixture value
was *created*, not the request object active when the fixture value
is being destroyed. We should probably deprecate/remove the request
parameter from `pytest_fixture_post_finalizer`.

Fix pytest-dev#2981
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants