diff --git a/_pytest/config.py b/_pytest/config.py index 450693036..c92374569 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -98,7 +98,7 @@ class PytestPluginManager(PluginManager): err = py.io.dupfile(err, encoding=encoding) except Exception: pass - self.trace.root.setwriter(err.write) + self.set_tracing(err.write) def pytest_configure(self, config): config.addinivalue_line("markers", diff --git a/_pytest/core.py b/_pytest/core.py index d871e7418..937703654 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -70,7 +70,6 @@ class TagTracerSub: class PluginManager(object): def __init__(self, hookspecs=None, prefix="pytest_"): self._name2plugin = {} - self._listattrcache = {} self._plugins = [] self._conftestplugins = [] self._warnings = [] @@ -79,6 +78,26 @@ class PluginManager(object): self._shutdown = [] self.hook = HookRelay(hookspecs or [], pm=self, prefix=prefix) + def set_tracing(self, writer): + self.trace.root.setwriter(writer) + # we reconfigure HookCalling to perform tracing + # and we avoid doing the "do we need to trace" check dynamically + # for speed reasons + assert HookCaller._docall.__name__ == "_docall" + real_docall = HookCaller._docall + def docall_tracing(self, methods, kwargs): + trace = self.hookrelay.trace + trace.root.indent += 1 + trace(self.name, kwargs) + try: + res = real_docall(self, methods, kwargs) + if res: + trace("finish", self.name, "-->", res) + finally: + trace.root.indent -= 1 + return res + HookCaller._docall = docall_tracing + def do_configure(self, config): # backward compatibility config.do_configure() @@ -129,7 +148,6 @@ class PluginManager(object): func() self._plugins = self._conftestplugins = [] self._name2plugin.clear() - self._listattrcache.clear() def isregistered(self, plugin, name=None): if self.getplugin(name) is not None: @@ -261,7 +279,6 @@ class PluginManager(object): l.append(meth) l.extend(last) l.extend(wrappers) - #self._listattrcache[key] = list(l) return l def call_plugin(self, plugin, methname, kwargs): @@ -336,7 +353,7 @@ class MultiCall: "wrapper contain more than one yield") -def varnames(func): +def varnames(func, startindex=None): """ return argument name tuple for a function, method, class or callable. In case of a class, its "__init__" method is considered. @@ -353,14 +370,16 @@ def varnames(func): func = func.__init__ except AttributeError: return () - ismethod = True + startindex = 1 else: if not inspect.isfunction(func) and not inspect.ismethod(func): func = getattr(func, '__call__', func) - ismethod = inspect.ismethod(func) + if startindex is None: + startindex = int(inspect.ismethod(func)) + rawcode = py.code.getrawcode(func) try: - x = rawcode.co_varnames[ismethod:rawcode.co_argcount] + x = rawcode.co_varnames[startindex:rawcode.co_argcount] except AttributeError: x = () else: @@ -388,12 +407,12 @@ class HookRelay: def _addhooks(self, hookspec, prefix): self._hookspecs.append(hookspec) added = False - for name in dir(hookspec): + isclass = int(inspect.isclass(hookspec)) + for name, method in vars(hookspec).items(): if name.startswith(prefix): - method = getattr(hookspec, name) firstresult = getattr(method, 'firstresult', False) hc = HookCaller(self, name, firstresult=firstresult, - argnames=varnames(method)) + argnames=varnames(method, startindex=isclass)) setattr(self, name, hc) added = True #print ("setting new hook", name) @@ -438,11 +457,10 @@ class HookCaller: self.hookrelay = hookrelay self.name = name self.firstresult = firstresult - self.trace = self.hookrelay.trace self.methods = methods self.argnames = ["__multicall__"] self.argnames.extend(argnames) - assert "self" not in argnames + assert "self" not in argnames # prevent oversights def new_cached_caller(self, methods): return HookCaller(self.hookrelay, self.name, self.firstresult, @@ -461,22 +479,11 @@ class HookCaller: return self._docall(methods, kwargs) def callextra(self, methods, **kwargs): - #if self.methods is None: - # self.reload_methods() return self._docall(self.methods + methods, kwargs) def _docall(self, methods, kwargs): - self.trace(self.name, kwargs) - self.trace.root.indent += 1 - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - try: - res = mc.execute() - if res: - self.trace("finish", self.name, "-->", res) - finally: - self.trace.root.indent -= 1 - return res - + return MultiCall(methods, kwargs, + firstresult=self.firstresult).execute() class PluginValidationError(Exception): @@ -486,13 +493,6 @@ def isgenerichook(name): return name == "pytest_plugins" or \ name.startswith("pytest_funcarg__") -def collectattr(obj): - methods = {} - for apiname in dir(obj): - if apiname.startswith("pytest_"): - methods[apiname] = getattr(obj, apiname) - return methods - def formatdef(func): return "%s%s" % ( func.__name__, diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index 028b3e3f1..bffada8d4 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -1,8 +1,7 @@ """ version info, help messages, tracing configuration. """ import py import pytest -import os, inspect, sys -from _pytest.core import varnames +import os, sys def pytest_addoption(parser): group = parser.getgroup('debugconfig') @@ -32,7 +31,7 @@ def pytest_cmdline_parse(__multicall__): f.write("versions pytest-%s, py-%s, python-%s\ncwd=%s\nargs=%s\n\n" %( pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), config._origargs)) - config.trace.root.setwriter(f.write) + config.pluginmanager.set_tracing(f.write) sys.stderr.write("writing pytestdebug information to %s\n" % path) return config diff --git a/testing/test_core.py b/testing/test_core.py index 2fd7ace10..cd8fa8982 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -274,7 +274,7 @@ class TestBootstrapping: saveindent.append(pm.trace.root.indent) raise ValueError(42) l = [] - pm.trace.root.setwriter(l.append) + pm.set_tracing(l.append) indent = pm.trace.root.indent p = api1() pm.register(p) @@ -405,11 +405,7 @@ class TestPytestPluginInteractions: pluginmanager.register(p3) methods = pluginmanager.listattr('m') assert methods == [p2.m, p3.m, p1.m] - # listattr keeps a cache and deleting - # a function attribute requires clearing it - pluginmanager._listattrcache.clear() del P1.m.__dict__['tryfirst'] - pytest.mark.trylast(getattr(P2.m, 'im_func', P2.m)) methods = pluginmanager.listattr('m') assert methods == [p2.m, p1.m, p3.m] diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 7d4c7cab1..30ce9c9f2 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -1,5 +1,4 @@ -import py, pytest -from _pytest.core import collectattr +import pytest def test_version(testdir, pytestconfig): result = testdir.runpytest("--version") @@ -25,18 +24,6 @@ def test_help(testdir): *to see*fixtures*py.test --fixtures* """) -def test_collectattr(): - class A: - def pytest_hello(self): - pass - class B(A): - def pytest_world(self): - pass - methods = py.builtin.sorted(collectattr(B)) - assert list(methods) == ['pytest_hello', 'pytest_world'] - methods = py.builtin.sorted(collectattr(B())) - assert list(methods) == ['pytest_hello', 'pytest_world'] - def test_hookvalidation_unknown(testdir): testdir.makeconftest(""" def pytest_hello(xyz):