vastly simplify and cleanup collection initialization by internally

introducing a RootCollector. Note that the internal node
methods _fromtrail and _totrail are shifted to the still internal
config._rootcol.fromtrail/totrail

--HG--
branch : trunk
This commit is contained in:
holger krekel 2010-01-03 01:02:44 +01:00
parent eebeb1b257
commit 1b34492108
9 changed files with 127 additions and 165 deletions

View File

@ -55,6 +55,7 @@ Changes between 1.X and 1.1.1
which will regularly see e.g. py.test.mark and py.test.importorskip.
- simplify internal plugin manager machinery
- simplify internal collection tree by introducing a RootCollector node
- fix assert reinterpreation that sees a call containing "keyword=..."

View File

@ -115,7 +115,7 @@ class Node(object):
l = [self]
while 1:
x = l[-1]
if x.parent is not None:
if x.parent is not None and x.parent.parent is not None:
l.append(x.parent)
else:
if not rootfirst:
@ -130,62 +130,7 @@ class Node(object):
while current and not isinstance(current, cls):
current = current.parent
return current
def _getitembynames(self, namelist):
cur = self
for name in namelist:
if name:
next = cur.collect_by_name(name)
if next is None:
existingnames = [x.name for x in self._memocollect()]
msg = ("Collector %r does not have name %r "
"existing names are: %s" %
(cur, name, existingnames))
raise AssertionError(msg)
cur = next
return cur
def _getfsnode(self, path):
# this method is usually called from
# config.getfsnode() which returns a colitem
# from filename arguments
#
# pytest's collector tree does not neccessarily
# follow the filesystem and we thus need to do
# some special matching code here because
# _getitembynames() works by colitem names, not
# basenames.
if path == self.fspath:
return self
basenames = path.relto(self.fspath).split(path.sep)
cur = self
while basenames:
basename = basenames.pop(0)
assert basename
fspath = cur.fspath.join(basename)
colitems = cur._memocollect()
l = []
for colitem in colitems:
if colitem.fspath == fspath or colitem.name == basename:
l.append(colitem)
if not l:
raise self.config.Error("can't collect: %s" %(fspath,))
if basenames:
if len(l) > 1:
msg = ("Collector %r has more than one %r colitem "
"existing colitems are: %s" %
(cur, fspath, colitems))
raise self.config.Error("xxx-too many test types for: %s" % (fspath, ))
cur = l[0]
else:
if len(l) > 1:
cur = l
else:
cur = l[0]
break
return cur
def readkeywords(self):
return dict([(x, True) for x in self._keywords()])
@ -228,30 +173,6 @@ class Node(object):
def _prunetraceback(self, traceback):
return traceback
def _totrail(self):
""" provide a trail relative to the topdir,
which can be used to reconstruct the
collector (possibly on a different host
starting from a different topdir).
"""
chain = self.listchain()
topdir = self.config.topdir
relpath = chain[0].fspath.relto(topdir)
if not relpath:
if chain[0].fspath == topdir:
relpath = "."
else:
raise ValueError("%r not relative to topdir %s"
%(chain[0].fspath, topdir))
return relpath, tuple([x.name for x in chain[1:]])
def _fromtrail(trail, config):
relpath, names = trail
fspath = config.topdir.join(relpath)
col = config.getfsnode(fspath)
return col._getitembynames(names)
_fromtrail = staticmethod(_fromtrail)
def _repr_failure_py(self, excinfo):
excinfo.traceback = self._prunetraceback(excinfo.traceback)
# XXX should excinfo.getrepr record all data and toterminal()
@ -347,30 +268,15 @@ class FSCollector(Collector):
self.fspath = fspath
def __getstate__(self):
if self.parent is None:
# the root node needs to pickle more context info
topdir = self.config.topdir
relpath = self.fspath.relto(topdir)
if not relpath:
if self.fspath == topdir:
relpath = "."
else:
raise ValueError("%r not relative to topdir %s"
%(self.fspath, topdir))
return (self.name, self.config, relpath)
if isinstance(self.parent, RootCollector):
relpath = self.parent._getrelpath(self.fspath)
return (relpath, self.parent)
else:
return (self.name, self.parent)
def __setstate__(self, picklestate):
if len(picklestate) == 3:
# root node
name, config, relpath = picklestate
fspath = config.topdir.join(relpath)
fsnode = config.getfsnode(fspath)
self.__dict__.update(fsnode.__dict__)
else:
name, parent = picklestate
self.__init__(parent.fspath.join(name), parent=parent)
name, parent = picklestate
self.__init__(parent.fspath.join(name), parent=parent)
class File(FSCollector):
""" base class for collecting tests from a file. """
@ -421,7 +327,8 @@ class Directory(FSCollector):
l = []
for x in res:
if x not in l:
assert x.parent == self, "wrong collection tree construction"
assert x.parent == self, (x.parent, self)
assert x.fspath == path, (x.fspath, path)
l.append(x)
res = l
return res
@ -468,3 +375,67 @@ def warnoldtestrun(function=None):
"implement item.runtest() instead of "
"item.run() and item.execute()",
stacklevel=2, function=function)
class RootCollector(Directory):
def __init__(self, config):
Directory.__init__(self, config.topdir, parent=None, config=config)
self.name = None
def getfsnode(self, path):
path = py.path.local(path)
if not path.check():
raise self.config.Error("file not found: %s" %(path,))
topdir = self.config.topdir
if path != topdir and not path.relto(topdir):
raise self.config.Error("path %r is not relative to %r" %
(str(path), str(self.fspath)))
# assumtion: pytest's fs-collector tree follows the filesystem tree
basenames = filter(None, path.relto(topdir).split(path.sep))
try:
return self.getbynames(basenames)
except ValueError:
raise self.config.Error("can't collect: %s" % str(path))
def getbynames(self, names):
current = self.consider(self.config.topdir)
for name in names:
if name == ".": # special "identity" name
continue
l = []
for x in current._memocollect():
if x.name == name:
l.append(x)
elif x.fspath == current.fspath.join(name):
l.append(x)
if not l:
raise ValueError("no node named %r in %r" %(name, current))
current = l[0]
return current
def totrail(self, node):
chain = node.listchain()
names = [self._getrelpath(chain[0].fspath)]
names += [x.name for x in chain[1:]]
return names
def fromtrail(self, trail):
return self.config._rootcol.getbynames(trail)
def _getrelpath(self, fspath):
topdir = self.config.topdir
relpath = fspath.relto(topdir)
if not relpath:
if fspath == topdir:
relpath = "."
else:
raise ValueError("%r not relative to topdir %s"
%(self.fspath, topdir))
return relpath
def __getstate__(self):
return self.config
def __setstate__(self, config):
self.__init__(config)

View File

@ -2,6 +2,7 @@ import py, os
from py.impl.test.conftesthandle import Conftest
from py.impl.test.pluginmanager import PluginManager
from py.impl.test import parseopt
from py.impl.test.collect import RootCollector
def ensuretemp(string, dir=1):
""" (deprecated) return temporary directory path with
@ -97,6 +98,7 @@ class Config(object):
if not args:
args.append(py.std.os.getcwd())
self.topdir = gettopdir(args)
self._rootcol = RootCollector(config=self)
self.args = [py.path.local(x) for x in args]
# config objects are usually pickled across system
@ -117,6 +119,7 @@ class Config(object):
py.test.config = self
# next line will registers default plugins
self.__init__(topdir=py.path.local())
self._rootcol = RootCollector(config=self)
args, cmdlineopts = repr
args = [self.topdir.join(x) for x in args]
self.option = cmdlineopts
@ -150,17 +153,7 @@ class Config(object):
return [self.getfsnode(arg) for arg in self.args]
def getfsnode(self, path):
path = py.path.local(path)
if not path.check():
raise self.Error("file not found: %s" %(path,))
# we want our possibly custom collection tree to start at pkgroot
pkgpath = path.pypkgpath()
if pkgpath is None:
pkgpath = path.check(file=1) and path.dirpath() or path
tmpcol = py.test.collect.Directory(pkgpath, config=self)
col = tmpcol.ihook.pytest_collect_directory(path=pkgpath, parent=tmpcol)
col.parent = None
return col._getfsnode(path)
return self._rootcol.getfsnode(path)
def _getcollectclass(self, name, path):
try:

View File

@ -136,8 +136,8 @@ def slave_runsession(channel, config, fullwidth, hasmarkup):
colitems = []
for trail in trails:
try:
colitem = py.test.collect.Collector._fromtrail(trail, config)
except AssertionError:
colitem = config._rootcol.fromtrail(trail)
except ValueError:
#XXX send info for "test disappeared" or so
continue
colitems.append(colitem)
@ -159,4 +159,5 @@ def slave_runsession(channel, config, fullwidth, hasmarkup):
session.config.hook.pytest_looponfailinfo(
failreports=list(failreports),
rootdirs=[config.topdir])
channel.send([rep.getnode()._totrail() for rep in failreports])
rootcol = session.config._rootcol
channel.send([rootcol.totrail(rep.getnode()) for rep in failreports])

View File

@ -4,9 +4,9 @@ from py.plugin.pytest_resultlog import generic_path, ResultLog
from py.impl.test.collect import Node, Item, FSCollector
def test_generic_path(testdir):
config = testdir.Config()
p1 = Node('a', config=config)
assert p1.fspath is None
config = testdir.parseconfig()
p1 = Node('a', parent=config._rootcol)
#assert p1.fspath is None
p2 = Node('B', parent=p1)
p3 = Node('()', parent = p2)
item = Item('c', parent = p3)
@ -14,7 +14,7 @@ def test_generic_path(testdir):
res = generic_path(item)
assert res == 'a.B().c'
p0 = FSCollector('proj/test', config=config)
p0 = FSCollector('proj/test', parent=config._rootcol)
p1 = FSCollector('proj/test/a', parent=p0)
p2 = Node('B', parent=p1)
p3 = Node('()', parent = p2)

View File

@ -52,44 +52,8 @@ class TestCollector:
parent = fn.getparent(py.test.collect.Class)
assert parent is cls
def test_totrail_and_back(self, testdir, tmpdir):
a = tmpdir.ensure("a", dir=1)
tmpdir.ensure("a", "__init__.py")
x = tmpdir.ensure("a", "trail.py")
config = testdir.reparseconfig([x])
col = config.getfsnode(x)
trail = col._totrail()
assert len(trail) == 2
assert trail[0] == a.relto(config.topdir)
assert trail[1] == ('trail.py',)
col2 = py.test.collect.Collector._fromtrail(trail, config)
assert col2.listnames() == col.listnames()
def test_totrail_topdir_and_beyond(self, testdir, tmpdir):
config = testdir.reparseconfig()
col = config.getfsnode(config.topdir)
trail = col._totrail()
assert len(trail) == 2
assert trail[0] == '.'
assert trail[1] == ()
col2 = py.test.collect.Collector._fromtrail(trail, config)
assert col2.fspath == config.topdir
assert len(col2.listchain()) == 1
col3 = config.getfsnode(config.topdir.dirpath())
py.test.raises(ValueError,
"col3._totrail()")
def test_listnames_and__getitembynames(self, testdir):
modcol = testdir.getmodulecol("pass", withinit=True)
print(modcol.config.pluginmanager.getplugins())
names = modcol.listnames()
print(names)
dircol = modcol.config.getfsnode(modcol.config.topdir)
x = dircol._getitembynames(names)
assert modcol.name == x.name
def test_listnames_getitembynames_custom(self, testdir):
def test_getcustomfile_roundtrip(self, testdir):
hello = testdir.makefile(".xxx", hello="world")
testdir.makepyfile(conftest="""
import py
@ -98,15 +62,15 @@ class TestCollector:
class MyDirectory(py.test.collect.Directory):
def collect(self):
return [CustomFile(self.fspath.join("hello.xxx"), parent=self)]
Directory = MyDirectory
def pytest_collect_directory(path, parent):
return MyDirectory(path, parent=parent)
""")
config = testdir.parseconfig(hello)
node = config.getfsnode(hello)
assert isinstance(node, py.test.collect.File)
assert node.name == "hello.xxx"
names = node.listnames()[1:]
dircol = config.getfsnode(config.topdir)
node = dircol._getitembynames(names)
names = config._rootcol.totrail(node)
node = config._rootcol.getbynames(names)
assert isinstance(node, py.test.collect.File)
class TestCollectFS:
@ -232,3 +196,27 @@ class TestCustomConftests:
"*MyModule*",
"*test_x*"
])
class TestRootCol:
def test_totrail_and_back(self, testdir, tmpdir):
a = tmpdir.ensure("a", dir=1)
tmpdir.ensure("a", "__init__.py")
x = tmpdir.ensure("a", "trail.py")
config = testdir.reparseconfig([x])
col = config.getfsnode(x)
trail = config._rootcol.totrail(col)
col2 = config._rootcol.fromtrail(trail)
assert col2 == col
def test_totrail_topdir_and_beyond(self, testdir, tmpdir):
config = testdir.reparseconfig()
col = config.getfsnode(config.topdir)
trail = config._rootcol.totrail(col)
col2 = config._rootcol.fromtrail(trail)
assert col2.fspath == config.topdir
assert len(col2.listchain()) == 1
py.test.raises(config.Error, "config.getfsnode(config.topdir.dirpath())")
#col3 = config.getfsnode(config.topdir.dirpath())
#py.test.raises(ValueError,
# "col3._totrail()")

View File

@ -1,4 +1,5 @@
import py
from py.impl.test.collect import RootCollector
class TestConfigCmdlineParsing:
@ -153,7 +154,7 @@ class TestConfigApi_getcolitems:
assert isinstance(col, py.test.collect.Module)
assert col.name == 'x.py'
assert col.parent.name == tmpdir.basename
assert col.parent.parent is None
assert isinstance(col.parent.parent, RootCollector)
for col in col.listchain():
assert col.config is config
@ -164,7 +165,7 @@ class TestConfigApi_getcolitems:
assert isinstance(col, py.test.collect.Directory)
print(col.listchain())
assert col.name == 'a'
assert col.parent is None
assert isinstance(col.parent, RootCollector)
assert col.config is config
def test__getcol_pkgfile(self, testdir, tmpdir):
@ -175,7 +176,7 @@ class TestConfigApi_getcolitems:
assert isinstance(col, py.test.collect.Module)
assert col.name == 'x.py'
assert col.parent.name == x.dirpath().basename
assert col.parent.parent is None
assert isinstance(col.parent.parent.parent, RootCollector)
for col in col.listchain():
assert col.config is config

View File

@ -5,7 +5,7 @@ from py.impl.test.outcome import Skipped
class TestCollectDeprecated:
def test_collect_with_deprecated_run_and_join(self, testdir, recwarn):
testdir.makepyfile(conftest="""
testdir.makeconftest("""
import py
class MyInstance(py.test.collect.Instance):
@ -39,7 +39,8 @@ class TestCollectDeprecated:
return self.Module(self.fspath.join(name), parent=self)
def pytest_collect_directory(path, parent):
return MyDirectory(path, parent)
if path.basename == "subconf":
return MyDirectory(path, parent)
""")
subconf = testdir.mkpydir("subconf")
somefile = subconf.join("somefile.py")

View File

@ -434,3 +434,9 @@ def test_generate_tests_only_done_in_subdir(testdir):
result.stdout.fnmatch_lines([
"*3 passed*"
])
def test_modulecol_roundtrip(testdir):
modcol = testdir.getmodulecol("pass", withinit=True)
trail = modcol.config._rootcol.totrail(modcol)
newcol = modcol.config._rootcol.fromtrail(trail)
assert modcol.name == newcol.name