From cd1ead4f7b57071ccb2ed5bbe2cdc24e6ba750ce Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 24 Sep 2012 17:04:34 +0200 Subject: [PATCH] - make request.funcargnames carry the closure of all used funcargs - make metafunc.funcargnames carry the closure of used funcargs --- _pytest/python.py | 106 ++++++++++++++++++++++++++--------------- testing/test_python.py | 79 ++++++++++++++++-------------- 2 files changed, 112 insertions(+), 73 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index b2ad05f8a..7ad1a085c 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -296,7 +296,7 @@ class PyCollector(PyobjMixin, pytest.Collector): clscol = self.getparent(Class) cls = clscol and clscol.obj or None transfer_markers(funcobj, cls, module) - metafunc = Metafunc(funcobj, parentid=self.nodeid, config=self.config, + metafunc = Metafunc(funcobj, parentnode=self, config=self.config, cls=cls, module=module) gentesthook = self.config.hook.pytest_generate_tests extra = [module] @@ -594,13 +594,19 @@ class CallSpec2(object): class Metafunc: def __init__(self, function, config=None, cls=None, module=None, - parentid=""): + parentnode=None): self.config = config self.module = module self.function = function - self.parentid = parentid - self.funcargnames = getfuncargnames(function, - startindex=int(cls is not None)) + self.parentnode = parentnode + self.parentid = getattr(parentnode, "nodeid", "") + argnames = getfuncargnames(function, startindex=int(cls is not None)) + if parentnode is not None: + fm = parentnode.session.funcargmanager + self.funcargnames, self._arg2facdeflist = fm.getallfuncargnames( + argnames, parentnode) + else: + self.funcargnames = argnames self.cls = cls self.module = module self._calls = [] @@ -961,8 +967,11 @@ class FuncargRequest: self._name2factory = {} self.funcargmanager = pyfuncitem.session.funcargmanager self._currentarg = None - self.funcargnames = getfuncargnames(self.function) self.parentid = pyfuncitem.parent.nodeid + self.funcargnames, self._arg2facdeflist_ = \ + self.funcargmanager.getallfuncargnames( + getfuncargnames(self.function), # XXX _pyfuncitem... + pyfuncitem.parent) self._factorystack = [] @property @@ -972,19 +981,20 @@ class FuncargRequest: def _getfaclist(self, argname): facdeflist = self._name2factory.get(argname, None) + getfactb = None + function = None if facdeflist is None: if self._factorystack: function = self._factorystack[-1].func getfactb = lambda: self._factorystack[:-1] else: function = self.function - getfactb = None facdeflist = self.funcargmanager.getfactorylist( - argname, self.parentid, function, getfactb) + argname, self.parentid) self._name2factory[argname] = facdeflist - elif not facdeflist: - self.funcargmanager._raiselookupfailed(argname, self.function, - self.parentid) + if not facdeflist: + self.funcargmanager._raiselookupfailed(argname, function, + self.parentid, getfactb) return facdeflist @property @@ -1068,15 +1078,14 @@ class FuncargRequest: def _fillfuncargs(self): - if self.funcargnames: - assert not getattr(self._pyfuncitem, '_args', None), ( + item = self._pyfuncitem + funcargnames = getattr(item, "funcargnames", self.funcargnames) + if funcargnames: + assert not getattr(item, '_args', None), ( "yielded functions cannot have funcargs") - while self.funcargnames: - argname = self.funcargnames.pop(0) - if argname not in self._pyfuncitem.funcargs: - self._pyfuncitem.funcargs[argname] = \ - self.getfuncargvalue(argname) - + for argname in funcargnames: + if argname not in item.funcargs: + item.funcargs[argname] = self.getfuncargvalue(argname) def _callsetup(self): self.funcargmanager.ensure_setupcalls(self) @@ -1305,26 +1314,47 @@ class FuncargManager: for plugin in plugins: self.pytest_plugin_registered(plugin) + def getallfuncargnames(self, funcargnames, parentnode): + # collect the closure of all funcargs, starting with + # funcargnames as the initial set + # we populate and return a arg2facdeflist mapping + # so that the caller can reuse it and does not have to re-discover + # factories again for each funcargname + parentid = parentnode.nodeid + funcargnames = list(funcargnames) + _, setupargs = self.getsetuplist(parentnode) + def merge(otherlist): + for arg in otherlist: + if arg not in funcargnames: + funcargnames.append(arg) + merge(setupargs) + arg2facdeflist = {} + lastlen = -1 + while lastlen != len(funcargnames): + lastlen = len(funcargnames) + for argname in list(funcargnames): + if argname in arg2facdeflist: + continue + facdeflist = self.getfactorylist(argname, parentid) + arg2facdeflist[argname] = facdeflist + if facdeflist is not None: + for facdef in facdeflist: + merge(facdef.funcargnames) + try: + funcargnames.remove("__request__") + except ValueError: + pass + return funcargnames, arg2facdeflist + def pytest_generate_tests(self, metafunc): - funcargnames = list(metafunc.funcargnames) - _, allargnames = self.getsetuplist(metafunc.parentid) - #print "setuplist, allargnames", setuplist, allargnames - funcargnames.extend(allargnames) - seen = set() - while funcargnames: - argname = funcargnames.pop(0) - if argname in seen or argname == "request": - continue - seen.add(argname) - faclist = self.getfactorylist(argname, metafunc.parentid, - metafunc.function, raising=False) + for argname in metafunc.funcargnames: + faclist = metafunc._arg2facdeflist[argname] if faclist is None: continue # will raise FuncargLookupError at setup time for facdef in faclist: if facdef.params is not None: metafunc.parametrize(argname, facdef.params, indirect=True, - scope=facdef.scope) - funcargnames.extend(facdef.funcargnames) + scope=facdef.scope) def pytest_collection_modifyitems(self, items): # separate parametrized setups @@ -1388,7 +1418,8 @@ class FuncargManager: faclist.append(factorydef) ### check scope/params mismatch? - def getsetuplist(self, nodeid): + def getsetuplist(self, node): + nodeid = node.nodeid l = [] allargnames = set() for setupcall in self.setuplist: @@ -1399,12 +1430,11 @@ class FuncargManager: return l, allargnames - def getfactorylist(self, argname, nodeid, function, getfactb=None, raising=True): + def getfactorylist(self, argname, nodeid): try: factorydeflist = self.arg2facspec[argname] except KeyError: - if raising: - self._raiselookupfailed(argname, function, nodeid, getfactb) + return None else: return self._matchfactories(factorydeflist, nodeid) @@ -1429,7 +1459,7 @@ class FuncargManager: raise FuncargLookupError(function, msg, lines) def ensure_setupcalls(self, request): - setuplist, allnames = self.getsetuplist(request._pyfuncitem.nodeid) + setuplist, allnames = self.getsetuplist(request._pyfuncitem) for setupcall in setuplist: if setupcall.active: continue diff --git a/testing/test_python.py b/testing/test_python.py index 7c382ee3b..2b9e1e65a 100644 --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1340,6 +1340,27 @@ class TestMetafuncFunctional: "*1 passed*" ]) + def test_parametrize_on_setup_arg(self, testdir): + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + assert "arg1" in metafunc.funcargnames + metafunc.parametrize("arg1", [1], indirect=True) + + def pytest_funcarg__arg1(request): + return request.param + + def pytest_funcarg__arg2(request, arg1): + return 10 * arg1 + + def test_func(arg2): + assert arg2 == 10 + """) + result = testdir.runpytest("-v", p) + result.stdout.fnmatch_lines([ + "*test_func*1*PASS*", + "*1 passed*" + ]) + def test_conftest_funcargs_only_available_in_subdir(testdir): sub1 = testdir.mkpydir("sub1") @@ -1648,38 +1669,6 @@ def test_issue117_sessionscopeteardown(testdir): "*ZeroDivisionError*", ]) -class TestRequestAPI: - @pytest.mark.xfail(reason="consider flub feedback") - def test_setup_can_query_funcargs(self, testdir): - testdir.makeconftest(""" - def pytest_runtest_setup(item): - assert not hasattr(item, "_request") - """) - testdir.makepyfile(""" - def pytest_funcarg__a(request): - return 1 - def pytest_funcarg__b(request): - return request.getfuncargvalue("a") + 1 - def test_hello(b): - assert b == 2 - """) - result = testdir.runpytest() - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*1 passed*", - ]) - - result = testdir.makeconftest(""" - import pytest - @pytest.setup() - def mysetup(request): - request.uses_funcarg("db") - """) - result = testdir.runpytest() - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*1 passed*", - ]) class TestFuncargFactory: def test_receives_funcargs(self, testdir): @@ -1828,7 +1817,7 @@ class TestFuncargManager: testdir.makepyfile(""" def test_hello(item, fm): for name in ("fm", "hello", "item"): - faclist = fm.getfactorylist(name, item.nodeid, item.obj) + faclist = fm.getfactorylist(name, item.nodeid) assert len(faclist) == 1 fac = faclist[0] assert fac.func.__name__ == "pytest_funcarg__" + name @@ -1844,7 +1833,7 @@ class TestFuncargManager: def pytest_funcarg__hello(self, request): return "class" def test_hello(self, item, fm): - faclist = fm.getfactorylist("hello", item.nodeid, item.obj) + faclist = fm.getfactorylist("hello", item.nodeid) print (faclist) assert len(faclist) == 3 assert faclist[0].func(item._request) == "conftest" @@ -1880,7 +1869,7 @@ class TestSetupDiscovery: def test_parsefactories_conftest(self, testdir): testdir.makepyfile(""" def test_check_setup(item, fm): - setupcalls, allnames = fm.getsetuplist(item.nodeid) + setupcalls, allnames = fm.getsetuplist(item) assert len(setupcalls) == 2 assert setupcalls[0].func.__name__ == "perfunction" assert "request" in setupcalls[0].funcargnames @@ -2650,3 +2639,23 @@ def test_setup_funcarg_order(testdir): """) reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + + +def test_request_funcargnames(testdir): + testdir.makepyfile(""" + import pytest + @pytest.factory() + def arg1(): + pass + @pytest.factory() + def farg(arg1): + pass + @pytest.setup() + def sarg(tmpdir): + pass + def test_function(request, farg): + assert set(request.funcargnames) == \ + set(["tmpdir", "arg1", "request", "farg"]) + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1)