diff --git a/CHANGELOG b/CHANGELOG index 9310172e5..ed1b2746e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,8 +18,8 @@ Changes between 1.0.0 and 1.0.1 * terser reporting of collection error tracebacks -* streamlined internal plugin arch code, renamed of internal methods - and argnames (related to py/_com.py multicall/plugin) +* simplified multicall mechanism and plugin architecture, + renamed some internal methods and argnames Changes between 1.0.0b9 and 1.0.0 ===================================== diff --git a/doc/test/extend.txt b/doc/test/extend.txt index bdd87b4a2..b5c9c2a5b 100644 --- a/doc/test/extend.txt +++ b/doc/test/extend.txt @@ -90,13 +90,13 @@ Available py.test hooks ==================================== py.test calls hooks functions to implement its `test collection`_, running and -reporting process. Upon loading of a plugin py.test performs -strict checking on contained hook functions. Function and argument names -need to match exactly one of `hook definition specification`_. It thus -provides useful error reporting on mistyped hook or argument names -and minimizes version incompatibilites. Below you find some introductory -information on particular hooks. It's sensible to look at existing -plugins so see example usages and start off with your own plugin. +reporting process. When py.test loads a plugin it validates that all hook functions +conform to the `hook definition specification`_. The hook function name and its +argument names need to match exactly but it is allowed for an implementation +to accept *less* parameters. You'll get useful errors on mistyped hook or +argument names. Read on for some introductory information on particular +hooks. It's sensible to look at existing plugins so see example usages +and start off with your own plugin. .. _`hook definition specification`: plugin/hookspec.html diff --git a/py/_com.py b/py/_com.py index 9085916c8..43976ad43 100644 --- a/py/_com.py +++ b/py/_com.py @@ -9,7 +9,8 @@ class MultiCall: def __init__(self, methods, kwargs, firstresult=False): self.methods = methods[:] - self.kwargs = kwargs + self.kwargs = kwargs.copy() + self.kwargs['__multicall__'] = self self.results = [] self.firstresult = firstresult @@ -20,28 +21,30 @@ class MultiCall: def execute(self): while self.methods: method = self.methods.pop() - res = self._call1(method) + kwargs = self.getkwargs(method) + res = method(**kwargs) if res is not None: self.results.append(res) if self.firstresult: - break + return res if not self.firstresult: return self.results - if self.results: - return self.results[-1] - def _call1(self, method): - kwargs = self.kwargs - if '__call__' in varnames(method): - kwargs = kwargs.copy() - kwargs['__call__'] = self - return method(**kwargs) + def getkwargs(self, method): + kwargs = {} + for argname in varnames(method): + try: + kwargs[argname] = self.kwargs[argname] + except KeyError: + pass # might be optional param + return kwargs def varnames(rawcode): + ismethod = hasattr(rawcode, 'im_self') rawcode = getattr(rawcode, 'im_func', rawcode) rawcode = getattr(rawcode, 'func_code', rawcode) try: - return rawcode.co_varnames + return rawcode.co_varnames[ismethod:] except AttributeError: return () @@ -101,9 +104,6 @@ class HookRelay: def _performcall(self, name, multicall): return multicall.execute() - def __repr__(self): - return "" %(self._hookspecs, self._registry) - class HookCaller: def __init__(self, hookrelay, name, firstresult, extralookup=()): self.hookrelay = hookrelay @@ -112,12 +112,10 @@ class HookCaller: self.extralookup = extralookup and [extralookup] or () def __repr__(self): - return "" %( - self.name, self.firstresult, self.hookrelay) + return "" %(self.name,) def __call__(self, **kwargs): - methods = self.hookrelay._getmethods(self.name, - extralookup=self.extralookup) + methods = self.hookrelay._getmethods(self.name, self.extralookup) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) return self.hookrelay._performcall(self.name, mc) diff --git a/py/misc/testing/test_com.py b/py/misc/testing/test_com.py index 9bccdc9c0..e4d9ec2a7 100644 --- a/py/misc/testing/test_com.py +++ b/py/misc/testing/test_com.py @@ -1,8 +1,18 @@ import py import os -from py._com import Registry, MultiCall, HookRelay +from py.__._com import Registry, MultiCall, HookRelay, varnames +def test_varnames(): + def f(x): + pass + class A: + def f(self, y): + pass + assert varnames(f) == ("x",) + assert varnames(A.f) == ('y',) + assert varnames(A().f) == ('y',) + class TestMultiCall: def test_uses_copy_of_methods(self): l = [lambda: 42] @@ -14,15 +24,15 @@ class TestMultiCall: def test_call_passing(self): class P1: - def m(self, __call__, x): - assert len(__call__.results) == 1 - assert not __call__.methods + def m(self, __multicall__, x): + assert len(__multicall__.results) == 1 + assert not __multicall__.methods return 17 class P2: - def m(self, __call__, x): - assert __call__.results == [] - assert __call__.methods + def m(self, __multicall__, x): + assert __multicall__.results == [] + assert __multicall__.methods return 23 p1 = P1() @@ -37,24 +47,23 @@ class TestMultiCall: def test_keyword_args(self): def f(x): return x + 1 - multicall = MultiCall([f], dict(x=23)) + class A: + def f(self, x, y): + return x + y + multicall = MultiCall([f, A().f], dict(x=23, y=24)) assert "'x': 23" in repr(multicall) + assert "'y': 24" in repr(multicall) reslist = multicall.execute() - assert reslist == [24] - assert "1 results" in repr(multicall) + assert reslist == [24+23, 24] + assert "2 results" in repr(multicall) + + def test_keywords_call_error(self): + multicall = MultiCall([lambda x: x], {}) + py.test.raises(TypeError, "multicall.execute()") - def test_optionalcallarg(self): - class P1: - def m(self, x): - return x - call = MultiCall([P1().m], dict(x=23)) - assert "23" in repr(call) - assert call.execute() == [23] - call = MultiCall([P1().m], dict(x=23), firstresult=True) - def test_call_subexecute(self): - def m(__call__): - subresult = __call__.execute() + def m(__multicall__): + subresult = __multicall__.execute() return subresult + 1 def n(): diff --git a/py/test/dist/dsession.py b/py/test/dist/dsession.py index 4c000f161..f68ccd4b2 100644 --- a/py/test/dist/dsession.py +++ b/py/test/dist/dsession.py @@ -77,8 +77,8 @@ class DSession(Session): self.item2nodes = {} super(DSession, self).__init__(config=config) - #def pytest_configure(self, __call__, config): - # __call__.execute() + #def pytest_configure(self, __multicall__, config): + # __multicall__.execute() # try: # config.getxspecs() # except config.Error: diff --git a/py/test/plugin/pytest_capture.py b/py/test/plugin/pytest_capture.py index 228b4617b..a391ba828 100644 --- a/py/test/plugin/pytest_capture.py +++ b/py/test/plugin/pytest_capture.py @@ -181,11 +181,11 @@ class CaptureManager: capfuncarg._finalize() del self._capturing_funcargs - def pytest_make_collect_report(self, __call__, collector): + def pytest_make_collect_report(self, __multicall__, collector): method = self._getmethod(collector.config, collector.fspath) self.resumecapture(method) try: - rep = __call__.execute() + rep = __multicall__.execute() finally: outerr = self.suspendcapture() addouterr(rep, outerr) @@ -204,11 +204,11 @@ class CaptureManager: def pytest_runtest_teardown(self, item): self.resumecapture_item(item) - def pytest__teardown_final(self, __call__, session): + def pytest__teardown_final(self, __multicall__, session): method = self._getmethod(session.config, None) self.resumecapture(method) try: - rep = __call__.execute() + rep = __multicall__.execute() finally: outerr = self.suspendcapture() if rep: @@ -219,9 +219,9 @@ class CaptureManager: if hasattr(self, '_capturing'): self.suspendcapture() - def pytest_runtest_makereport(self, __call__, item, call): + def pytest_runtest_makereport(self, __multicall__, item, call): self.deactivate_funcargs() - rep = __call__.execute() + rep = __multicall__.execute() outerr = self.suspendcapture() outerr = (item.outerr[0] + outerr[0], item.outerr[1] + outerr[1]) if not rep.passed: diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py index cc6ff7e0a..684d1b020 100644 --- a/py/test/plugin/pytest_default.py +++ b/py/test/plugin/pytest_default.py @@ -2,8 +2,8 @@ import py -def pytest_pyfunc_call(__call__, pyfuncitem): - if not __call__.execute(): +def pytest_pyfunc_call(__multicall__, pyfuncitem): + if not __multicall__.execute(): testfunction = pyfuncitem.obj if pyfuncitem._isyieldedfunction(): testfunction(*pyfuncitem._args) diff --git a/py/test/plugin/pytest_execnetcleanup.py b/py/test/plugin/pytest_execnetcleanup.py index 52c3fdd89..2c5941430 100644 --- a/py/test/plugin/pytest_execnetcleanup.py +++ b/py/test/plugin/pytest_execnetcleanup.py @@ -32,10 +32,10 @@ class Execnetcleanup: #for gw in l: # gw.join() - def pytest_pyfunc_call(self, __call__, pyfuncitem): + def pytest_pyfunc_call(self, __multicall__, pyfuncitem): if self._gateways is not None: gateways = self._gateways[:] - res = __call__.execute() + res = __multicall__.execute() while len(self._gateways) > len(gateways): self._gateways[-1].exit() return res diff --git a/py/test/plugin/pytest_nose.py b/py/test/plugin/pytest_nose.py index c77b01f08..e78f7598d 100644 --- a/py/test/plugin/pytest_nose.py +++ b/py/test/plugin/pytest_nose.py @@ -47,7 +47,7 @@ import py import inspect import sys -def pytest_runtest_makereport(__call__, item, call): +def pytest_runtest_makereport(__multicall__, item, call): SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None) if SkipTest: if call.excinfo and call.excinfo.errisinstance(SkipTest): diff --git a/py/test/plugin/pytest_pastebin.py b/py/test/plugin/pytest_pastebin.py index 3d2cbc2af..6ad8567fd 100644 --- a/py/test/plugin/pytest_pastebin.py +++ b/py/test/plugin/pytest_pastebin.py @@ -33,9 +33,9 @@ def pytest_addoption(parser): type="choice", choices=['failed', 'all'], help="send failed|all info to Pocoo pastebin service.") -def pytest_configure(__call__, config): +def pytest_configure(__multicall__, config): import tempfile - __call__.execute() + __multicall__.execute() if config.option.pastebin == "all": config._pastebinfile = tempfile.TemporaryFile() tr = config.pluginmanager.impname2plugin['terminalreporter'] diff --git a/py/test/plugin/pytest_terminal.py b/py/test/plugin/pytest_terminal.py index c2f83c124..80f7020ea 100644 --- a/py/test/plugin/pytest_terminal.py +++ b/py/test/plugin/pytest_terminal.py @@ -250,8 +250,8 @@ class TerminalReporter: for i, testarg in py.builtin.enumerate(self.config.args): self.write_line("test object %d: %s" %(i+1, testarg)) - def pytest_sessionfinish(self, __call__, session, exitstatus): - __call__.execute() + def pytest_sessionfinish(self, exitstatus, __multicall__): + __multicall__.execute() self._tw.line("") if exitstatus in (0, 1, 2): self.summary_errors() diff --git a/py/test/plugin/pytest_xfail.py b/py/test/plugin/pytest_xfail.py index 56aac847c..68361b570 100644 --- a/py/test/plugin/pytest_xfail.py +++ b/py/test/plugin/pytest_xfail.py @@ -19,12 +19,12 @@ when it fails. Instead terminal reporting will list it in the import py -def pytest_runtest_makereport(__call__, item, call): +def pytest_runtest_makereport(__multicall__, item, call): if call.when != "call": return if hasattr(item, 'obj') and hasattr(item.obj, 'func_dict'): if 'xfail' in item.obj.func_dict: - res = __call__.execute() + res = __multicall__.execute() if call.excinfo: res.skipped = True res.failed = res.passed = False diff --git a/py/test/pluginmanager.py b/py/test/pluginmanager.py index 621fd5a99..c8868c552 100644 --- a/py/test/pluginmanager.py +++ b/py/test/pluginmanager.py @@ -134,15 +134,16 @@ class PluginManager(object): fail = True else: method_args = getargs(method) - if '__call__' in method_args: - method_args.remove('__call__') + if '__multicall__' in method_args: + method_args.remove('__multicall__') hook = hooks[name] hookargs = getargs(hook) - for arg, hookarg in zip(method_args, hookargs): - if arg != hookarg: - Print("argument mismatch: %r != %r" %(arg, hookarg)) - Print("actual : %s" %(formatdef(method))) - Print("required:", formatdef(hook)) + for arg in method_args: + if arg not in hookargs: + Print("argument %r not available" %(arg, )) + Print("actual definition: %s" %(formatdef(method))) + Print("available hook arguments: %s" % + ", ".join(hookargs)) fail = True break #if not fail: