simplify/integrate fixturemapper into FixtureManager

also fix jstests test failures
This commit is contained in:
holger krekel 2012-10-17 11:20:45 +02:00
parent dc4e205876
commit 98513b995a
5 changed files with 134 additions and 104 deletions

View File

@ -1,2 +1,2 @@
# #
__version__ = '2.3.0.dev24' __version__ = '2.3.0.dev25'

View File

@ -250,18 +250,6 @@ class PyobjMixin(PyobjContext):
return fspath, lineno, modpath return fspath, lineno, modpath
class PyCollector(PyobjMixin, pytest.Collector): 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): def funcnamefilter(self, name):
for prefix in self.config.getini("python_functions"): for prefix in self.config.getini("python_functions"):
@ -305,9 +293,8 @@ class PyCollector(PyobjMixin, pytest.Collector):
clscol = self.getparent(Class) clscol = self.getparent(Class)
cls = clscol and clscol.obj or None cls = clscol and clscol.obj or None
transfer_markers(funcobj, cls, module) transfer_markers(funcobj, cls, module)
if not hasattr(self, "_fixturemapper_memo"): fm = self.session._fixturemanager
self._fixturemapper = FixtureMapper(self) fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
fixtureinfo = self._fixturemapper.getfixtureinfo(funcobj, cls)
metafunc = Metafunc(funcobj, fixtureinfo, self.config, metafunc = Metafunc(funcobj, fixtureinfo, self.config,
cls=cls, module=module) cls=cls, module=module)
gentesthook = self.config.hook.pytest_generate_tests gentesthook = self.config.hook.pytest_generate_tests
@ -326,64 +313,6 @@ class PyCollector(PyobjMixin, pytest.Collector):
callspec=callspec, callobj=funcobj, callspec=callspec, callobj=funcobj,
keywords={callspec.id:True}) 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: class FuncFixtureInfo:
def __init__(self, argnames, names_closure, name2fixturedefs): def __init__(self, argnames, names_closure, name2fixturedefs):
@ -599,11 +528,22 @@ def fillfixtures(function):
try: try:
request = function._request request = function._request
except AttributeError: except AttributeError:
# the special jstests class with a custom .obj # XXX this special code path is only expected to execute
fi = FixtureMapper(function).getfixtureinfo(function.obj, None) # 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 function._fixtureinfo = fi
request = function._request = FixtureRequest(function) 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() _notexists = object()
@ -953,8 +893,9 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr):
for name, val in keywords.items(): for name, val in keywords.items():
setattr(self.markers, name, val) setattr(self.markers, name, val)
fi = self.parent._fixturemapper.getfixtureinfo(self.obj, self.cls) fm = self.session._fixturemanager
self._fixtureinfo = fi self._fixtureinfo = fi = fm.getfixtureinfo(self.parent,
self.obj, self.cls)
self.fixturenames = fi.names_closure self.fixturenames = fi.names_closure
if self._isyieldedfunction(): if self._isyieldedfunction():
assert not callspec, ( assert not callspec, (
@ -1395,6 +1336,37 @@ class FixtureLookupErrorRepr(TerminalRepr):
tw.line("%s:%d" % (self.filename, self.firstlineno+1)) tw.line("%s:%d" % (self.filename, self.firstlineno+1))
class FixtureManager: 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__" _argprefix = "pytest_funcarg__"
FixtureLookupError = FixtureLookupError FixtureLookupError = FixtureLookupError
FixtureLookupErrorRepr = FixtureLookupErrorRepr FixtureLookupErrorRepr = FixtureLookupErrorRepr
@ -1409,6 +1381,34 @@ class FixtureManager:
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
session.config.pluginmanager.register(self, "funcmanage") 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 ### 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 ### so that we don't have to do the below pytest_configure hook
def pytest_plugin_registered(self, plugin): def pytest_plugin_registered(self, plugin):

View File

@ -20,6 +20,8 @@ def pytest_pycollect_makeitem(collector, name, obj):
return UnitTestCase(name, parent=collector) return UnitTestCase(name, parent=collector)
class UnitTestCase(pytest.Class): class UnitTestCase(pytest.Class):
nofuncargs = True # marker for fixturemanger.getfixtureinfo()
# to declare that our children do not support funcargs
def collect(self): def collect(self):
self.session._fixturemanager.parsefactories(self, unittest=True) self.session._fixturemanager.parsefactories(self, unittest=True)

View File

@ -24,7 +24,7 @@ def main():
name='pytest', name='pytest',
description='py.test: simple powerful testing with Python', description='py.test: simple powerful testing with Python',
long_description = long_description, long_description = long_description,
version='2.3.0.dev24', version='2.3.0.dev25',
url='http://pytest.org', url='http://pytest.org',
license='MIT license', license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],

View File

@ -280,7 +280,6 @@ class TestFunction:
config = testdir.parseconfigure() config = testdir.parseconfigure()
session = testdir.Session(config) session = testdir.Session(config)
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
session._fixturemapper = funcargs.FixtureMapper(session, funcargs=False)
def func1(): def func1():
pass pass
def func2(): def func2():
@ -1440,29 +1439,58 @@ def test_conftest_funcargs_only_available_in_subdir(testdir):
"*2 passed*" "*2 passed*"
]) ])
def test_funcarg_non_pycollectobj(testdir): # rough jstests usage class TestOEJSKITSpecials:
testdir.makeconftest(""" def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage
import pytest testdir.makeconftest("""
def pytest_pycollect_makeitem(collector, name, obj): import pytest
if name == "MyClass": def pytest_pycollect_makeitem(collector, name, obj):
return MyCollector(name, parent=collector) if name == "MyClass":
class MyCollector(pytest.Collector): return MyCollector(name, parent=collector)
def reportinfo(self): class MyCollector(pytest.Collector):
return self.fspath, 3, "xyz" def reportinfo(self):
""") return self.fspath, 3, "xyz"
modcol = testdir.getmodulecol(""" """)
def pytest_funcarg__arg1(request): modcol = testdir.getmodulecol("""
return 42 def pytest_funcarg__arg1(request):
class MyClass: return 42
pass class MyClass:
""") pass
# this hook finds funcarg factories """)
rep = modcol.ihook.pytest_make_collect_report(collector=modcol) # this hook finds funcarg factories
clscol = rep.result[0] rep = modcol.ihook.pytest_make_collect_report(collector=modcol)
clscol.obj = lambda arg1: None clscol = rep.result[0]
clscol.funcargs = {} clscol.obj = lambda arg1: None
funcargs.fillfixtures(clscol) clscol.funcargs = {}
assert clscol.funcargs['arg1'] == 42 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