Skip to content

Commit e2683f4

Browse files
committed
refactor all collection related logic
- drop all pickling support (for now) - perform collection completely ahead of test running (no iterativity) - introduce new collection related hooks - shift all keyword-selection code to pytest_keyword plugin - simplify session object - besides: fix issue88 --HG-- branch : trunk
1 parent 350ebbd commit e2683f4

30 files changed

+819
-781
lines changed

CHANGELOG

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
2+
Changes between 1.3.4 and 1.4.0a1
3+
==================================================
4+
5+
- major refactoring of internal collection handling
6+
- fix issue88 (finding custom test nodes from command line arg)
7+
18
Changes between 1.3.3 and 1.3.4
29
==================================================
310

py/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
99
(c) Holger Krekel and others, 2004-2010
1010
"""
11-
__version__ = version = "1.3.4"
11+
__version__ = version = "1.4.0a1"
1212

1313
import py.apipkg
1414

@@ -53,7 +53,7 @@
5353
'_fillfuncargs' : '._test.funcargs:fillfuncargs',
5454
},
5555
'cmdline': {
56-
'main' : '._test.cmdline:main', # backward compat
56+
'main' : '._test.session:main', # backward compat
5757
},
5858
},
5959

py/_plugin/hookspec.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,26 @@ def pytest_configure(config):
2020
and all plugins and initial conftest files been loaded.
2121
"""
2222

23+
def pytest_cmdline_main(config):
24+
""" called for performing the main (cmdline) action. """
25+
pytest_cmdline_main.firstresult = True
26+
2327
def pytest_unconfigure(config):
2428
""" called before test process is exited. """
2529

2630
# -------------------------------------------------------------------------
2731
# collection hooks
2832
# -------------------------------------------------------------------------
2933

34+
def pytest_log_startcollection(collection):
35+
""" called before collection.perform_collection() is called. """
36+
37+
def pytest_collection_modifyitems(collection):
38+
""" called to allow filtering and selecting of test items (inplace). """
39+
40+
def pytest_log_finishcollection(collection):
41+
""" called after collection has finished. """
42+
3043
def pytest_ignore_collect(path, config):
3144
""" return true value to prevent considering this path for collection.
3245
This hook is consulted for all files and directories prior to considering
@@ -41,9 +54,13 @@ def pytest_collect_directory(path, parent):
4154
def pytest_collect_file(path, parent):
4255
""" return Collection node or None for the given path. """
4356

57+
# logging hooks for collection
4458
def pytest_collectstart(collector):
4559
""" collector starts collecting. """
4660

61+
def pytest_log_itemcollect(item):
62+
""" we just collected a test item. """
63+
4764
def pytest_collectreport(report):
4865
""" collector finished collecting. """
4966

@@ -54,10 +71,6 @@ def pytest_make_collect_report(collector):
5471
""" perform a collection and return a collection. """
5572
pytest_make_collect_report.firstresult = True
5673

57-
# XXX rename to item_collected()? meaning in distribution context?
58-
def pytest_itemstart(item, node=None):
59-
""" test item gets collected. """
60-
6174
# -------------------------------------------------------------------------
6275
# Python test function related hooks
6376
# -------------------------------------------------------------------------
@@ -85,6 +98,9 @@ def pytest_generate_tests(metafunc):
8598
# generic runtest related hooks
8699
# -------------------------------------------------------------------------
87100

101+
def pytest_itemstart(item, node=None):
102+
""" test item starts running. """
103+
88104
def pytest_runtest_protocol(item):
89105
""" implement fixture, run and report about the given test item. """
90106
pytest_runtest_protocol.firstresult = True

py/_plugin/pytest__pytest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,28 @@ def getcalls(self, names):
8787
l.append(call)
8888
return l
8989

90+
def contains(self, entries):
91+
from py.builtin import print_
92+
i = 0
93+
entries = list(entries)
94+
backlocals = py.std.sys._getframe(1).f_locals
95+
while entries:
96+
name, check = entries.pop(0)
97+
for ind, call in enumerate(self.calls[i:]):
98+
if call._name == name:
99+
print_("NAMEMATCH", name, call)
100+
if eval(check, backlocals, call.__dict__):
101+
print_("CHECKERMATCH", repr(check), "->", call)
102+
else:
103+
print_("NOCHECKERMATCH", repr(check), "-", call)
104+
continue
105+
i += ind + 1
106+
break
107+
print_("NONAMEMATCH", name, "with", call)
108+
else:
109+
raise AssertionError("could not find %r in %r" %(
110+
name, self.calls[i:]))
111+
90112
def popcall(self, name):
91113
for i, call in enumerate(self.calls):
92114
if call._name == name:

py/_plugin/pytest_default.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
import sys
44
import py
55

6+
def pytest_cmdline_main(config):
7+
from py._test.session import Session, Collection
8+
exitstatus = 0
9+
if config.option.showfuncargs:
10+
from py._test.funcargs import showfuncargs
11+
session = showfuncargs(config)
12+
else:
13+
collection = Collection(config)
14+
# instantiate session already because it
15+
# records failures and implements maxfail handling
16+
session = Session(config, collection)
17+
exitstatus = collection.do_collection()
18+
if not exitstatus:
19+
exitstatus = session.main()
20+
return exitstatus
21+
622
def pytest_pyfunc_call(__multicall__, pyfuncitem):
723
if not __multicall__.execute():
824
testfunction = pyfuncitem.obj
@@ -16,7 +32,7 @@ def pytest_collect_file(path, parent):
1632
ext = path.ext
1733
pb = path.purebasename
1834
if pb.startswith("test_") or pb.endswith("_test") or \
19-
path in parent.config._argfspaths:
35+
path in parent.collection._argfspaths:
2036
if ext == ".py":
2137
return parent.ihook.pytest_pycollect_makemodule(
2238
path=path, parent=parent)
@@ -49,7 +65,7 @@ def pytest_collect_directory(path, parent):
4965
# define Directory(dir) already
5066
if not parent.recfilter(path): # by default special ".cvs", ...
5167
# check if cmdline specified this dir or a subdir directly
52-
for arg in parent.config._argfspaths:
68+
for arg in parent.collection._argfspaths:
5369
if path == arg or arg.relto(path):
5470
break
5571
else:
@@ -68,12 +84,6 @@ def pytest_addoption(parser):
6884
group._addoption('--maxfail', metavar="num",
6985
action="store", type="int", dest="maxfail", default=0,
7086
help="exit after first num failures or errors.")
71-
group._addoption('-k',
72-
action="store", dest="keyword", default='',
73-
help="only run test items matching the given "
74-
"space separated keywords. precede a keyword with '-' to negate. "
75-
"Terminate the expression with ':' to treat a match as a signal "
76-
"to run all subsequent tests. ")
7787

7888
group = parser.getgroup("collect", "collection")
7989
group.addoption('--collectonly',
@@ -91,17 +101,10 @@ def pytest_addoption(parser):
91101
help="base temporary directory for this test run.")
92102

93103
def pytest_configure(config):
94-
setsession(config)
95104
# compat
96105
if config.getvalue("exitfirst"):
97106
config.option.maxfail = 1
98107

99-
def setsession(config):
100-
val = config.getvalue
101-
if val("collectonly"):
102-
from py._test.session import Session
103-
config.setsessionclass(Session)
104-
105108
# pycollect related hooks and code, should move to pytest_pycollect.py
106109

107110
def pytest_pycollect_makeitem(__multicall__, collector, name, obj):

py/_plugin/pytest_keyword.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
2+
def pytest_addoption(parser):
3+
group = parser.getgroup("general")
4+
group._addoption('-k',
5+
action="store", dest="keyword", default='',
6+
help="only run test items matching the given "
7+
"space separated keywords. precede a keyword with '-' to negate. "
8+
"Terminate the expression with ':' to treat a match as a signal "
9+
"to run all subsequent tests. ")
10+
11+
def pytest_collection_modifyitems(collection):
12+
config = collection.config
13+
keywordexpr = config.option.keyword
14+
if not keywordexpr:
15+
return
16+
selectuntil = False
17+
if keywordexpr[-1] == ":":
18+
selectuntil = True
19+
keywordexpr = keywordexpr[:-1]
20+
21+
remaining = []
22+
deselected = []
23+
for colitem in collection.items:
24+
if keywordexpr and skipbykeyword(colitem, keywordexpr):
25+
deselected.append(colitem)
26+
else:
27+
remaining.append(colitem)
28+
if selectuntil:
29+
keywordexpr = None
30+
31+
if deselected:
32+
config.hook.pytest_deselected(items=deselected)
33+
collection.items[:] = remaining
34+
35+
def skipbykeyword(colitem, keywordexpr):
36+
""" return True if they given keyword expression means to
37+
skip this collector/item.
38+
"""
39+
if not keywordexpr:
40+
return
41+
chain = colitem.listchain()
42+
for key in filter(None, keywordexpr.split()):
43+
eor = key[:1] == '-'
44+
if eor:
45+
key = key[1:]
46+
if not (eor ^ matchonekeyword(key, chain)):
47+
return True
48+
49+
def matchonekeyword(key, chain):
50+
elems = key.split(".")
51+
# XXX O(n^2), anyone cares?
52+
chain = [item.keywords for item in chain if item.keywords]
53+
for start, _ in enumerate(chain):
54+
if start + len(elems) > len(chain):
55+
return False
56+
for num, elem in enumerate(elems):
57+
for keyword in chain[num + start]:
58+
ok = False
59+
if elem in keyword:
60+
ok = True
61+
break
62+
if not ok:
63+
break
64+
if num == len(elems) - 1 and ok:
65+
return True
66+
return False

py/_plugin/pytest_pytester.py

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,8 @@ def __init__(self, request):
7474
def __repr__(self):
7575
return "<TmpTestdir %r>" % (self.tmpdir,)
7676

77-
def Config(self, topdir=None):
78-
if topdir is None:
79-
topdir = self.tmpdir.dirpath()
80-
return pytestConfig(topdir=topdir)
77+
def Config(self):
78+
return pytestConfig()
8179

8280
def finalize(self):
8381
for p in self._syspathremove:
@@ -149,16 +147,23 @@ def mkpydir(self, name):
149147
p.ensure("__init__.py")
150148
return p
151149

150+
def getnode(self, config, arg):
151+
from py._test.session import Collection
152+
collection = Collection(config)
153+
return collection.getbyid(collection._normalizearg(arg))[0]
154+
152155
def genitems(self, colitems):
153-
return list(self.session.genitems(colitems))
156+
collection = colitems[0].collection
157+
result = []
158+
collection.genitems(colitems, (), result)
159+
return result
154160

155161
def inline_genitems(self, *args):
156162
#config = self.parseconfig(*args)
157-
config = self.parseconfig(*args)
158-
session = config.initsession()
163+
from py._test.session import Collection
164+
config = self.parseconfigure(*args)
159165
rec = self.getreportrecorder(config)
160-
colitems = [config.getnode(arg) for arg in config.args]
161-
items = list(session.genitems(colitems))
166+
items = Collection(config).perform_collect()
162167
return items, rec
163168

164169
def runitem(self, source):
@@ -187,11 +192,9 @@ def inline_runsource1(self, *args):
187192
def inline_run(self, *args):
188193
args = ("-s", ) + args # otherwise FD leakage
189194
config = self.parseconfig(*args)
190-
config.pluginmanager.do_configure(config)
191-
session = config.initsession()
192195
reprec = self.getreportrecorder(config)
193-
colitems = config.getinitialnodes()
194-
session.main(colitems)
196+
config.pluginmanager.do_configure(config)
197+
config.hook.pytest_cmdline_main(config=config)
195198
config.pluginmanager.do_unconfigure(config)
196199
return reprec
197200

@@ -245,29 +248,17 @@ def getitem(self, source, funcname="test_func"):
245248

246249
def getitems(self, source):
247250
modcol = self.getmodulecol(source)
248-
return list(modcol.config.initsession().genitems([modcol]))
249-
#assert item is not None, "%r item not found in module:\n%s" %(funcname, source)
250-
#return item
251-
252-
def getfscol(self, path, configargs=()):
253-
self.config = self.parseconfig(path, *configargs)
254-
self.session = self.config.initsession()
255-
return self.config.getnode(path)
251+
return self.genitems([modcol])
256252

257253
def getmodulecol(self, source, configargs=(), withinit=False):
258254
kw = {self.request.function.__name__: py.code.Source(source).strip()}
259255
path = self.makepyfile(**kw)
260256
if withinit:
261257
self.makepyfile(__init__ = "#")
262-
self.config = self.parseconfig(path, *configargs)
263-
self.session = self.config.initsession()
264-
#self.config.pluginmanager.do_configure(config=self.config)
265-
# XXX
266-
self.config.pluginmanager.import_plugin("runner")
267-
plugin = self.config.pluginmanager.getplugin("runner")
268-
plugin.pytest_configure(config=self.config)
269-
270-
return self.config.getnode(path)
258+
self.config = config = self.parseconfigure(path, *configargs)
259+
node = self.getnode(config, path)
260+
#config.pluginmanager.do_unconfigure(config)
261+
return node
271262

272263
def popen(self, cmdargs, stdout, stderr, **kw):
273264
if not hasattr(py.std, 'subprocess'):

0 commit comments

Comments
 (0)