From 032ce8baf6d0c0425f3949a630ec43fb1c2bce75 Mon Sep 17 00:00:00 2001 From: Danielle Jenkins Date: Sat, 25 Jun 2016 12:19:46 +0200 Subject: [PATCH] Switch setuponly and setupplan options to a hook-based implementation. --- _pytest/config.py | 2 +- _pytest/hookspec.py | 13 ++++++ _pytest/python.py | 109 ++++++++++++++----------------------------- _pytest/setuponly.py | 54 +++++++++++++++++++++ _pytest/setupplan.py | 14 ++++++ 5 files changed, 117 insertions(+), 75 deletions(-) create mode 100644 _pytest/setuponly.py create mode 100644 _pytest/setupplan.py diff --git a/_pytest/config.py b/_pytest/config.py index 9a308df2b..cce159623 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -65,7 +65,7 @@ _preinit = [] default_plugins = ( "mark main terminal runner python pdb unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml resultlog doctest cacheprovider").split() + "junitxml resultlog doctest cacheprovider setuponly setupplan").split() builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 639e316d0..2c6d8ba51 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -218,6 +218,19 @@ def pytest_runtest_logreport(report): """ process a test setup/call/teardown report relating to the respective phase of executing a test. """ +# ------------------------------------------------------------------------- +# Fixture related hooks +# ------------------------------------------------------------------------- + +@hookspec(firstresult=True) +def pytest_fixture_setup(fixturedef, request): + """ performs fixture setup execution. """ + +def pytest_fixture_post_finalizer(fixturedef): + """ called after fixture teardown, but before the cache is cleared so + the fixture result cache ``fixturedef.cached_result`` can + still be accessed.""" + # ------------------------------------------------------------------------- # test session related hooks # ------------------------------------------------------------------------- diff --git a/_pytest/python.py b/_pytest/python.py index edc81e923..0aa8dee0e 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -2475,25 +2475,18 @@ class FixtureDef: func = self._finalizer.pop() func() finally: + ihook = self._fixturemanager.session.ihook + ihook.pytest_fixture_post_finalizer(fixturedef=self) # even if finalization fails, we invalidate # the cached fixture value if hasattr(self, "cached_result"): - config = self._fixturemanager.config - if config.option.setuponly or config.option.setupplan: - self._show_fixture_action('TEARDOWN') - if hasattr(self, "cached_param"): - del self.cached_param del self.cached_result def execute(self, request): # get required arguments and register our own finish() # with their finalization - kwargs = {} for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) - result, arg_cache_key, exc = fixturedef.cached_result - request._check_scope(argname, request.scope, fixturedef.scope) - kwargs[argname] = result if argname != "request": fixturedef.addfinalizer(self.finish) @@ -2511,76 +2504,44 @@ class FixtureDef: self.finish() assert not hasattr(self, "cached_result") - fixturefunc = self.func - - if self.unittest: - if request.instance is not None: - # bind the unbound method to the TestCase instance - fixturefunc = self.func.__get__(request.instance) - else: - # the fixture function needs to be bound to the actual - # request.instance so that code working with "self" behaves - # as expected. - if request.instance is not None: - fixturefunc = getimfunc(self.func) - if fixturefunc != self.func: - fixturefunc = fixturefunc.__get__(request.instance) - - try: - config = request.config - if config.option.setupplan: - result = None - else: - result = call_fixture_func(fixturefunc, request, kwargs) - if config.option.setuponly or config.option.setupplan: - if hasattr(request, 'param'): - # Save the fixture parameter so ._show_fixture_action() can - # display it now and during the teardown (in .finish()). - if self.ids: - if callable(self.ids): - self.cached_param = self.ids(request.param) - else: - self.cached_param = self.ids[request.param_index] - else: - self.cached_param = request.param - self._show_fixture_action('SETUP') - except Exception: - self.cached_result = (None, my_cache_key, sys.exc_info()) - raise - self.cached_result = (result, my_cache_key, None) - return result - - def _show_fixture_action(self, what): - config = self._fixturemanager.config - capman = config.pluginmanager.getplugin('capturemanager') - if capman: - out, err = capman.suspendcapture() - - tw = config.get_terminal_writer() - tw.line() - tw.write(' ' * 2 * self.scopenum) - tw.write('{step} {scope} {fixture}'.format( - step=what.ljust(8), # align the output to TEARDOWN - scope=self.scope[0].upper(), - fixture=self.argname)) - - if what == 'SETUP': - deps = sorted(arg for arg in self.argnames if arg != 'request') - if deps: - tw.write(' (fixtures used: {0})'.format(', '.join(deps))) - - if hasattr(self, 'cached_param'): - tw.write('[{0}]'.format(self.cached_param)) - - if capman: - capman.resumecapture() - sys.stdout.write(out) - sys.stderr.write(err) + ihook = self._fixturemanager.session.ihook + ihook.pytest_fixture_setup(fixturedef=self, request=request) def __repr__(self): return ("" % (self.argname, self.scope, self.baseid)) +def pytest_fixture_setup(fixturedef, request): + """ Execution of fixture setup. """ + kwargs = {} + for argname in fixturedef.argnames: + fixdef = request._get_active_fixturedef(argname) + result, arg_cache_key, exc = fixdef.cached_result + request._check_scope(argname, request.scope, fixdef.scope) + kwargs[argname] = result + + fixturefunc = fixturedef.func + if fixturedef.unittest: + if request.instance is not None: + # bind the unbound method to the TestCase instance + fixturefunc = fixturedef.func.__get__(request.instance) + else: + # the fixture function needs to be bound to the actual + # request.instance so that code working with "fixturedef" behaves + # as expected. + if request.instance is not None: + fixturefunc = getimfunc(fixturedef.func) + if fixturefunc != fixturedef.func: + fixturefunc = fixturefunc.__get__(request.instance) + my_cache_key = request.param_index + try: + result = call_fixture_func(fixturefunc, request, kwargs) + except Exception: + fixturedef.cached_result = (None, my_cache_key, sys.exc_info()) + raise + fixturedef.cached_result = (result, my_cache_key, None) + return result + def num_mock_patch_args(function): """ return number of arguments used up by mock arguments (if any) """ patchings = getattr(function, "patchings", None) diff --git a/_pytest/setuponly.py b/_pytest/setuponly.py new file mode 100644 index 000000000..98830c76b --- /dev/null +++ b/_pytest/setuponly.py @@ -0,0 +1,54 @@ +import pytest +import sys + +@pytest.hookimpl(hookwrapper=True) +def pytest_fixture_setup(fixturedef, request): + yield + config = request.config + if config.option.setuponly: + if hasattr(request, 'param'): + # Save the fixture parameter so ._show_fixture_action() can + # display it now and during the teardown (in .finish()). + if fixturedef.ids: + if callable(fixturedef.ids): + fixturedef.cached_param = fixturedef.ids(request.param) + else: + fixturedef.cached_param = fixturedef.ids[request.param_index] + else: + fixturedef.cached_param = request.param + _show_fixture_action(fixturedef, 'SETUP') + +def pytest_fixture_post_finalizer(fixturedef): + if hasattr(fixturedef, "cached_result"): + config = fixturedef._fixturemanager.config + if config.option.setuponly: + _show_fixture_action(fixturedef, 'TEARDOWN') + if hasattr(fixturedef, "cached_param"): + del fixturedef.cached_param + +def _show_fixture_action(fixturedef, msg): + config = fixturedef._fixturemanager.config + capman = config.pluginmanager.getplugin('capturemanager') + if capman: + out, err = capman.suspendcapture() + + tw = config.get_terminal_writer() + tw.line() + tw.write(' ' * 2 * fixturedef.scopenum) + tw.write('{step} {scope} {fixture}'.format( + step=msg.ljust(8), # align the output to TEARDOWN + scope=fixturedef.scope[0].upper(), + fixture=fixturedef.argname)) + + if msg == 'SETUP': + deps = sorted(arg for arg in fixturedef.argnames if arg != 'request') + if deps: + tw.write(' (fixtures used: {0})'.format(', '.join(deps))) + + if hasattr(fixturedef, 'cached_param'): + tw.write('[{0}]'.format(fixturedef.cached_param)) + + if capman: + capman.resumecapture() + sys.stdout.write(out) + sys.stderr.write(err) diff --git a/_pytest/setupplan.py b/_pytest/setupplan.py new file mode 100644 index 000000000..65df65d18 --- /dev/null +++ b/_pytest/setupplan.py @@ -0,0 +1,14 @@ +import pytest + + +@pytest.hookimpl(tryfirst=True) +def pytest_fixture_setup(fixturedef, request): + # Will return a dummy fixture if the setuponly option is provided. + if request.config.option.setupplan: + fixturedef.cached_result = (None, None, None) + return fixturedef.cached_result + +@pytest.hookimpl(tryfirst=True) +def pytest_cmdline_main(config): + if config.option.setupplan: + config.option.setuponly = True