diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 038a30e37..ec646814a 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev24' +__version__ = '2.3.0.dev25' diff --git a/_pytest/python.py b/_pytest/python.py index 8d4f61f95..1cfd9cf06 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -250,18 +250,6 @@ class PyobjMixin(PyobjContext): return fspath, lineno, modpath class PyCollector(PyobjMixin, pytest.Collector): - def _fixturemapper(): - def get(self): - try: - return self._fixturemapper_memo - except AttributeError: - self._fixturemapper_memo = FixtureMapper(self, funcargs=False) - return self._fixturemapper_memo - def set(self, val): - assert not hasattr(self, "_fixturemapper_memo") - self._fixturemapper_memo = val - return property(get, set) - _fixturemapper = _fixturemapper() def funcnamefilter(self, name): for prefix in self.config.getini("python_functions"): @@ -305,9 +293,8 @@ class PyCollector(PyobjMixin, pytest.Collector): clscol = self.getparent(Class) cls = clscol and clscol.obj or None transfer_markers(funcobj, cls, module) - if not hasattr(self, "_fixturemapper_memo"): - self._fixturemapper = FixtureMapper(self) - fixtureinfo = self._fixturemapper.getfixtureinfo(funcobj, cls) + fm = self.session._fixturemanager + fixtureinfo = fm.getfixtureinfo(self, funcobj, cls) metafunc = Metafunc(funcobj, fixtureinfo, self.config, cls=cls, module=module) gentesthook = self.config.hook.pytest_generate_tests @@ -326,64 +313,6 @@ class PyCollector(PyobjMixin, pytest.Collector): callspec=callspec, callobj=funcobj, keywords={callspec.id:True}) -class FixtureMapper: - """ - pytest fixtures definitions and information is stored and managed - from this class. - - During collection fm.parsefactories() is called multiple times to parse - fixture function definitions into FixtureDef objects and internal - data structures. - - During collection of test functions, metafunc-mechanics instantiate - a FuncFixtureInfo object which is cached in a FixtureMapper instance - which itself lives on the parent collector. This FuncFixtureInfo object - is later retrieved by Function nodes which themselves offer a fixturenames - attribute. - - The FuncFixtureInfo object holds information about fixtures and FixtureDefs - relevant for a particular function. An initial list of fixtures is - assembled like this: - - - ini-defined usefixtures - - autouse-marked fixtures along the collection chain up from the function - - usefixtures markers at module/class/function level - - test function funcargs - - Subsequently the funcfixtureinfo.fixturenames attribute is computed - as the closure of the fixtures needed to setup the initial fixtures, - i. e. fixtures needed by fixture functions themselves are appended - to the fixturenames list. - - Upon the test-setup phases all fixturenames are instantiated, retrieved - by a lookup on a FixtureMapper(). - """ - - def __init__(self, node, funcargs=True): - self.node = node - self._name2fixtureinfo = {} - self.hasfuncargs = funcargs - - def getfixtureinfo(self, func, cls): - try: - return self._name2fixtureinfo[func] - except KeyError: - pass - if self.hasfuncargs: - argnames = getfuncargnames(func, int(cls is not None)) - else: - argnames = () - usefixtures = getattr(func, "usefixtures", None) - initialnames = argnames - if usefixtures is not None: - initialnames = usefixtures.args + initialnames - fm = self.node.session._fixturemanager - names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames, - self.node) - fixtureinfo = FuncFixtureInfo(argnames, names_closure, - arg2fixturedefs) - self._name2fixtureinfo[func] = fixtureinfo - return fixtureinfo class FuncFixtureInfo: def __init__(self, argnames, names_closure, name2fixturedefs): @@ -599,11 +528,22 @@ def fillfixtures(function): try: request = function._request except AttributeError: - # the special jstests class with a custom .obj - fi = FixtureMapper(function).getfixtureinfo(function.obj, None) + # XXX this special code path is only expected to execute + # with the oejskit plugin. It uses classes with funcargs + # and we thus have to work a bit to allow this. + fm = function.session._fixturemanager + fi = fm.getfixtureinfo(function.parent, function.obj, None) function._fixtureinfo = fi request = function._request = FixtureRequest(function) - request._fillfixtures() + request._fillfixtures() + # prune out funcargs for jstests + newfuncargs = {} + for name in fi.argnames: + newfuncargs[name] = function.funcargs[name] + function.funcargs = newfuncargs + else: + request._fillfixtures() + _notexists = object() @@ -953,8 +893,9 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): for name, val in keywords.items(): setattr(self.markers, name, val) - fi = self.parent._fixturemapper.getfixtureinfo(self.obj, self.cls) - self._fixtureinfo = fi + fm = self.session._fixturemanager + self._fixtureinfo = fi = fm.getfixtureinfo(self.parent, + self.obj, self.cls) self.fixturenames = fi.names_closure if self._isyieldedfunction(): assert not callspec, ( @@ -1395,6 +1336,37 @@ class FixtureLookupErrorRepr(TerminalRepr): tw.line("%s:%d" % (self.filename, self.firstlineno+1)) class FixtureManager: + """ + pytest fixtures definitions and information is stored and managed + from this class. + + During collection fm.parsefactories() is called multiple times to parse + fixture function definitions into FixtureDef objects and internal + data structures. + + During collection of test functions, metafunc-mechanics instantiate + a FuncFixtureInfo object which is cached per node/func-name. + This FuncFixtureInfo object is later retrieved by Function nodes + which themselves offer a fixturenames attribute. + + The FuncFixtureInfo object holds information about fixtures and FixtureDefs + relevant for a particular function. An initial list of fixtures is + assembled like this: + + - ini-defined usefixtures + - autouse-marked fixtures along the collection chain up from the function + - usefixtures markers at module/class/function level + - test function funcargs + + Subsequently the funcfixtureinfo.fixturenames attribute is computed + as the closure of the fixtures needed to setup the initial fixtures, + i. e. fixtures needed by fixture functions themselves are appended + to the fixturenames list. + + Upon the test-setup phases all fixturenames are instantiated, retrieved + by a lookup of their FuncFixtureInfo. + """ + _argprefix = "pytest_funcarg__" FixtureLookupError = FixtureLookupError FixtureLookupErrorRepr = FixtureLookupErrorRepr @@ -1409,6 +1381,34 @@ class FixtureManager: self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] session.config.pluginmanager.register(self, "funcmanage") + self._nodename2fixtureinfo = {} + + def getfixtureinfo(self, node, func, cls): + key = (node, func.__name__) + try: + return self._nodename2fixtureinfo[key] + except KeyError: + pass + if not hasattr(node, "nofuncargs"): + if cls is not None: + startindex = 1 + else: + startindex = None + argnames = getfuncargnames(func, startindex) + else: + argnames = () + usefixtures = getattr(func, "usefixtures", None) + initialnames = argnames + if usefixtures is not None: + initialnames = usefixtures.args + initialnames + fm = node.session._fixturemanager + names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames, + node) + fixtureinfo = FuncFixtureInfo(argnames, names_closure, + arg2fixturedefs) + self._nodename2fixtureinfo[key] = fixtureinfo + return fixtureinfo + ### XXX this hook should be called for historic events like pytest_configure ### so that we don't have to do the below pytest_configure hook def pytest_plugin_registered(self, plugin): diff --git a/_pytest/unittest.py b/_pytest/unittest.py index cad8ae3bf..d7a02ccaf 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -20,6 +20,8 @@ def pytest_pycollect_makeitem(collector, name, obj): return UnitTestCase(name, parent=collector) class UnitTestCase(pytest.Class): + nofuncargs = True # marker for fixturemanger.getfixtureinfo() + # to declare that our children do not support funcargs def collect(self): self.session._fixturemanager.parsefactories(self, unittest=True) diff --git a/setup.py b/setup.py index 27dd1ab33..017c0b134 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev24', + version='2.3.0.dev25', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/test_python.py b/testing/test_python.py index f42379fd9..f737063c0 100644 --- a/testing/test_python.py +++ b/testing/test_python.py @@ -280,7 +280,6 @@ class TestFunction: config = testdir.parseconfigure() session = testdir.Session(config) session._fixturemanager = FixtureManager(session) - session._fixturemapper = funcargs.FixtureMapper(session, funcargs=False) def func1(): pass def func2(): @@ -1440,29 +1439,58 @@ def test_conftest_funcargs_only_available_in_subdir(testdir): "*2 passed*" ]) -def test_funcarg_non_pycollectobj(testdir): # rough jstests usage - testdir.makeconftest(""" - import pytest - def pytest_pycollect_makeitem(collector, name, obj): - if name == "MyClass": - return MyCollector(name, parent=collector) - class MyCollector(pytest.Collector): - def reportinfo(self): - return self.fspath, 3, "xyz" - """) - modcol = testdir.getmodulecol(""" - def pytest_funcarg__arg1(request): - return 42 - class MyClass: - pass - """) - # this hook finds funcarg factories - rep = modcol.ihook.pytest_make_collect_report(collector=modcol) - clscol = rep.result[0] - clscol.obj = lambda arg1: None - clscol.funcargs = {} - funcargs.fillfixtures(clscol) - assert clscol.funcargs['arg1'] == 42 +class TestOEJSKITSpecials: + def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage + testdir.makeconftest(""" + import pytest + def pytest_pycollect_makeitem(collector, name, obj): + if name == "MyClass": + return MyCollector(name, parent=collector) + class MyCollector(pytest.Collector): + def reportinfo(self): + return self.fspath, 3, "xyz" + """) + modcol = testdir.getmodulecol(""" + def pytest_funcarg__arg1(request): + return 42 + class MyClass: + pass + """) + # this hook finds funcarg factories + rep = modcol.ihook.pytest_make_collect_report(collector=modcol) + clscol = rep.result[0] + clscol.obj = lambda arg1: None + clscol.funcargs = {} + funcargs.fillfixtures(clscol) + assert clscol.funcargs['arg1'] == 42 + + def test_autouse_fixture(self, testdir): # rough jstests usage + testdir.makeconftest(""" + import pytest + def pytest_pycollect_makeitem(collector, name, obj): + if name == "MyClass": + return MyCollector(name, parent=collector) + class MyCollector(pytest.Collector): + def reportinfo(self): + return self.fspath, 3, "xyz" + """) + modcol = testdir.getmodulecol(""" + import pytest + @pytest.fixture(autouse=True) + def hello(): + pass + def pytest_funcarg__arg1(request): + return 42 + class MyClass: + pass + """) + # this hook finds funcarg factories + rep = modcol.ihook.pytest_make_collect_report(collector=modcol) + clscol = rep.result[0] + clscol.obj = lambda: None + clscol.funcargs = {} + funcargs.fillfixtures(clscol) + assert not clscol.funcargs