diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 611799e5929..19b977224cf 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -1,13 +1,15 @@ from __future__ import absolute_import, division, print_function +import functools import inspect import sys import warnings +from collections import OrderedDict +import attr import py from py._code.code import FormattedExcinfo -import attr import _pytest from _pytest import nodes from _pytest._code.code import TerminalRepr @@ -22,9 +24,6 @@ from _pytest.outcomes import fail, TEST_OUTCOME -from collections import OrderedDict - - def pytest_sessionstart(session): import _pytest.python @@ -519,7 +518,7 @@ def _getfixturevalue(self, fixturedef): 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 @@ -573,7 +572,6 @@ def __init__(self, request, scope, param, param_index, fixturedef): self.param_index = param_index self.scope = scope self._fixturedef = fixturedef - self.addfinalizer = fixturedef.addfinalizer self._pyfuncitem = request._pyfuncitem self._fixture_values = request._fixture_values self._fixture_defs = request._fixture_defs @@ -584,6 +582,9 @@ def __init__(self, request, scope, param, param_index, fixturedef): def __repr__(self): return "" % (self.fixturename, self._pyfuncitem) + def addfinalizer(self, finalizer): + self._fixturedef.addfinalizer(finalizer) + class ScopeMismatchError(Exception): """ A fixture function tries to use a different fixture function which @@ -734,17 +735,17 @@ def __init__(self, fixturemanager, baseid, argname, func, scope, params, self.argnames = getfuncargnames(func, is_method=unittest) self.unittest = unittest self.ids = ids - self._finalizer = [] + self._finalizers = [] def addfinalizer(self, finalizer): - self._finalizer.append(finalizer) + self._finalizers.append(finalizer) - def finish(self): + def finish(self, request): exceptions = [] try: - while self._finalizer: + while self._finalizers: try: - func = self._finalizer.pop() + func = self._finalizers.pop() func() except: # noqa exceptions.append(sys.exc_info()) @@ -754,12 +755,15 @@ def finish(self): py.builtin._reraise(*e) finally: - ihook = self._fixturemanager.session.ihook - ihook.pytest_fixture_post_finalizer(fixturedef=self) + hook = self._fixturemanager.session.gethookproxy(request.node.fspath) + hook.pytest_fixture_post_finalizer(fixturedef=self, request=request) # even if finalization fails, we invalidate - # the cached fixture value + # the cached fixture value and remove + # all finalizers because they may be bound methods which will + # keep instances alive if hasattr(self, "cached_result"): del self.cached_result + self._finalizers = [] def execute(self, request): # get required arguments and register our own finish() @@ -767,7 +771,7 @@ def execute(self, request): 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=request)) my_cache_key = request.param_index cached_result = getattr(self, "cached_result", None) @@ -780,11 +784,11 @@ def execute(self, request): 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 - return ihook.pytest_fixture_setup(fixturedef=self, request=request) + hook = self._fixturemanager.session.gethookproxy(request.node.fspath) + return hook.pytest_fixture_setup(fixturedef=self, request=request) def __repr__(self): return ("" % diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 93cf91b7840..440bf99d375 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -296,7 +296,7 @@ def pytest_fixture_setup(fixturedef, request): Stops at first non-None result, see :ref:`firstresult` """ -def pytest_fixture_post_finalizer(fixturedef): +def pytest_fixture_post_finalizer(fixturedef, request): """ called after fixture teardown, but before the cache is cleared so the fixture result cache ``fixturedef.cached_result`` can still be accessed.""" diff --git a/_pytest/main.py b/_pytest/main.py index 08907175bce..25554098dac 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -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): + for initialpath in self.session._initialpaths: + if self.fspath.common(initialpath) == initialpath: + return self.fspath.relto(initialpath.dirname) + def _makeid(self): relpath = self.fspath.relto(self.config.rootdir) + + if not relpath: + relpath = self._check_initialpaths_for_relpath() if os.sep != nodes.SEP: relpath = relpath.replace(os.sep, nodes.SEP) return relpath diff --git a/_pytest/runner.py b/_pytest/runner.py index eda38be0d24..57379fc78da 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -56,11 +56,6 @@ def pytest_sessionfinish(session): session._setupstate.teardown_all() -class NodeInfo: - def __init__(self, location): - self.location = location - - def pytest_runtest_protocol(item, nextitem): item.ihook.pytest_runtest_logstart( nodeid=item.nodeid, location=item.location, diff --git a/changelog/2124.bugfix b/changelog/2124.bugfix new file mode 100644 index 00000000000..e1c5e044c2e --- /dev/null +++ b/changelog/2124.bugfix @@ -0,0 +1 @@ +``pytest_fixture_setup`` and ``pytest_fixture_post_finalizer`` hooks are now called for all ``conftest.py`` files. diff --git a/changelog/2124.feature b/changelog/2124.feature new file mode 100644 index 00000000000..267fdabc972 --- /dev/null +++ b/changelog/2124.feature @@ -0,0 +1 @@ +``pytest_fixture_post_finalizer`` hook can now receive a ``request`` argument. diff --git a/changelog/2775.bugfix b/changelog/2775.bugfix new file mode 100644 index 00000000000..8123522acb8 --- /dev/null +++ b/changelog/2775.bugfix @@ -0,0 +1 @@ +Fix the bug where running pytest with "--pyargs" will result in Items with empty "parent.nodeid" if run from a different root directory. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 903e5d499c6..111be956bd0 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -624,8 +624,10 @@ def join_pythonpath(*dirs): for p in search_path: monkeypatch.syspath_prepend(p) + os.chdir('world') # mixed module and filenames: - result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "world/ns_pkg") + result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world") + testdir.chdir() assert result.ret == 0 result.stdout.fnmatch_lines([ "*test_hello.py::test_hello*PASSED", diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 1ab4feae796..1a0603a8009 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -3121,3 +3121,43 @@ def test_foo(request): E*{1}:5 *1 failed* """.format(fixfile.strpath, testfile.basename)) + + +def test_pytest_fixture_setup_and_post_finalizer_hook(testdir): + testdir.makeconftest(""" + from __future__ import print_function + def pytest_fixture_setup(fixturedef, request): + print('ROOT setup hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) + def pytest_fixture_post_finalizer(fixturedef, request): + print('ROOT finalizer hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) + """) + testdir.makepyfile(**{ + 'tests/conftest.py': """ + from __future__ import print_function + def pytest_fixture_setup(fixturedef, request): + print('TESTS setup hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) + def pytest_fixture_post_finalizer(fixturedef, request): + print('TESTS finalizer hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) + """, + 'tests/test_hooks.py': """ + from __future__ import print_function + import pytest + + @pytest.fixture() + def my_fixture(): + return 'some' + + def test_func(my_fixture): + print('TEST test_func') + assert my_fixture == 'some' + """ + }) + result = testdir.runpytest("-s") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*TESTS setup hook called for my_fixture from test_func*", + "*ROOT setup hook called for my_fixture from test_func*", + "*TEST test_func*", + "*TESTS finalizer hook called for my_fixture from test_func*", + "*ROOT finalizer hook called for my_fixture from test_func*", + ])