diff --git a/CHANGELOG b/CHANGELOG index 912eab216..74841bfec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,14 @@ Changes between 2.3.5 and 2.4.DEV ----------------------------------- +- fix issue181: --pdb now also works on collect errors (and + on internal errors) . This was implemented by a slight internal + refactoring and the introduction of a new hook + ``pytest_exception_interact`` hook (see below). + +- fix issue341: introduce new experimental hook for IDEs/terminals to + intercept debugging: ``pytest_exception_interact(node, call, report)``. + - PR27: correctly handle nose.SkipTest during collection. Thanks Antonio Cuni, Ronny Pfannschmidt. diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 4a257c400..7d731d0c1 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.4.0.dev11' +__version__ = '2.4.0.dev12' diff --git a/_pytest/core.py b/_pytest/core.py index cc7726cdf..ceafe721c 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -298,7 +298,8 @@ class PluginManager(object): showlocals=getattr(option, 'showlocals', False), style=style, ) - res = self.hook.pytest_internalerror(excrepr=excrepr) + res = self.hook.pytest_internalerror(excrepr=excrepr, + excinfo=excinfo) if not py.builtin.any(res): for line in str(excrepr).split("\n"): sys.stderr.write("INTERNALERROR> %s\n" %line) diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index ab77fbca8..64479f03f 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -241,8 +241,18 @@ def pytest_plugin_registered(plugin, manager): def pytest_plugin_unregistered(plugin): """ a py lib plugin got unregistered. """ -def pytest_internalerror(excrepr): +def pytest_internalerror(excrepr, excinfo): """ called for internal errors. """ def pytest_keyboard_interrupt(excinfo): """ called for keyboard interrupt. """ + +def pytest_exception_interact(node, call, report): + """ (experimental, new in 2.4) called when + an exception was raised which can potentially be + interactively handled. + + This hook is only called if an exception was raised + that is not an internal exception like "skip.Exception". + """ + diff --git a/_pytest/main.py b/_pytest/main.py index 74d90062b..008d39c3a 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -10,7 +10,7 @@ except ImportError: from UserDict import DictMixin as MappingMixin from _pytest.mark import MarkInfo -import _pytest.runner +from _pytest.runner import collect_one_node, Skipped tracebackcutdir = py.path.local(_pytest.__file__).dirpath() @@ -372,7 +372,7 @@ class Collector(Node): # the set of exceptions to interpret as "Skip the whole module" during # collection - skip_exceptions = (_pytest.runner.Skipped,) + skip_exceptions = (Skipped,) class CollectError(Exception): """ an error during collection, contains a custom message. """ @@ -512,8 +512,7 @@ class Session(FSCollector): parts = self._parsearg(arg) self._initialparts.append(parts) self._initialpaths.add(parts[0]) - self.ihook.pytest_collectstart(collector=self) - rep = self.ihook.pytest_make_collect_report(collector=self) + rep = collect_one_node(self) self.ihook.pytest_collectreport(report=rep) self.trace.root.indent -= 1 if self._notfound: @@ -642,8 +641,7 @@ class Session(FSCollector): resultnodes.append(node) continue assert isinstance(node, pytest.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) + rep = collect_one_node(node) if rep.passed: has_matched = False for x in rep.result: @@ -664,8 +662,7 @@ class Session(FSCollector): yield node else: assert isinstance(node, pytest.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) + rep = collect_one_node(node) if rep.passed: for subnode in rep.result: for x in self.genitems(subnode): diff --git a/_pytest/pdb.py b/_pytest/pdb.py index 45487131a..8c243ef9f 100644 --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -52,31 +52,26 @@ def pytest_runtest_makereport(): pytestPDB.item = None class PdbInvoke: - @pytest.mark.tryfirst - def pytest_runtest_makereport(self, item, call, __multicall__): - rep = __multicall__.execute() - if not call.excinfo or \ - call.excinfo.errisinstance(pytest.skip.Exception) or \ - call.excinfo.errisinstance(py.std.bdb.BdbQuit): - return rep - if hasattr(rep, "wasxfail"): - return rep - return _enter_pdb(item, call.excinfo, rep) + def pytest_exception_interact(self, node, call, report): + return _enter_pdb(node, call.excinfo, report) + + def pytest_internalerror(self, excrepr, excinfo): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" %line) + sys.stderr.flush() + tb = _postmortem_traceback(excinfo) + post_mortem(tb) - - -def _enter_pdb(item, excinfo, rep): - # we assume that the above execute() suspended capturing +def _enter_pdb(node, excinfo, rep): # XXX we re-use the TerminalReporter's terminalwriter # because this seems to avoid some encoding related troubles # for not completely clear reasons. - tw = item.config.pluginmanager.getplugin("terminalreporter")._tw + tw = node.config.pluginmanager.getplugin("terminalreporter")._tw tw.line() tw.sep(">", "traceback") rep.toterminal(tw) tw.sep(">", "entering PDB") - tb = _postmortem_traceback(excinfo) post_mortem(tb) rep._pdbshown = True diff --git a/_pytest/runner.py b/_pytest/runner.py index 00a725bb9..9abc9a7fe 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -108,8 +108,16 @@ def call_and_report(item, when, log=True, **kwds): report = hook.pytest_runtest_makereport(item=item, call=call) if log: hook.pytest_runtest_logreport(report=report) + if check_interactive_exception(call, report): + hook.pytest_exception_interact(node=item, call=call, report=report) return report +def check_interactive_exception(call, report): + return call.excinfo and not ( + hasattr(report, "wasxfail") or + call.excinfo.errisinstance(skip.Exception) or + call.excinfo.errisinstance(py.std.bdb.BdbQuit)) + def call_runtest_hook(item, when, **kwds): hookname = "pytest_runtest_" + when ihook = getattr(item.ihook, hookname) @@ -268,8 +276,11 @@ def pytest_make_collect_report(collector): if not hasattr(errorinfo, "toterminal"): errorinfo = CollectErrorRepr(errorinfo) longrepr = errorinfo - return CollectReport(collector.nodeid, outcome, longrepr, + rep = CollectReport(collector.nodeid, outcome, longrepr, getattr(call, 'result', None)) + rep.call = call # see collect_one_node + return rep + class CollectReport(BaseReport): def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): @@ -364,6 +375,16 @@ class SetupState(object): col._prepare_exc = sys.exc_info() raise +def collect_one_node(collector): + ihook = collector.ihook + ihook.pytest_collectstart(collector=collector) + rep = ihook.pytest_make_collect_report(collector=collector) + call = rep.__dict__.pop("call", None) + if call and check_interactive_exception(call, rep): + ihook.pytest_exception_interact(node=collector, call=call, report=rep) + return rep + + # ============================================================= # Test OutcomeExceptions and helpers for creating them. diff --git a/doc/en/plugins.txt b/doc/en/plugins.txt index a86115efb..d6c169ef5 100644 --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -359,6 +359,17 @@ test execution: .. autofunction:: pytest_runtest_logreport + +Debugging/Interaction hooks +-------------------------------------- + +There are few hooks which can be used for special +reporting or interaction with exceptions: + +.. autofunction:: pytest_internalerror +.. autofunction:: pytest_keyboard_interrupt +.. autofunction:: pytest_exception_interact + Reference of objects involved in hooks =========================================================== diff --git a/setup.py b/setup.py index 5f8339f3a..33ef4c60e 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.4.0.dev11', + version='2.4.0.dev12', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/python/integration.py b/testing/python/integration.py index c716c7ceb..1915c763d 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -1,4 +1,5 @@ import pytest, py, sys +from _pytest import runner class TestOEJSKITSpecials: def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage @@ -18,7 +19,7 @@ class TestOEJSKITSpecials: pass """) # this hook finds funcarg factories - rep = modcol.ihook.pytest_make_collect_report(collector=modcol) + rep = runner.collect_one_node(collector=modcol) clscol = rep.result[0] clscol.obj = lambda arg1: None clscol.funcargs = {} @@ -46,7 +47,7 @@ class TestOEJSKITSpecials: pass """) # this hook finds funcarg factories - rep = modcol.ihook.pytest_make_collect_report(collector=modcol) + rep = runner.collect_one_node(modcol) clscol = rep.result[0] clscol.obj = lambda: None clscol.funcargs = {} diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 71781afb4..517b13400 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1,6 +1,8 @@ import py, pytest import sys +from test_doctest import xfail_if_pdbpp_installed + class TestPDB: def pytest_funcarg__pdblist(self, request): monkeypatch = request.getfuncargvalue("monkeypatch") @@ -85,6 +87,32 @@ class TestPDB: if child.isalive(): child.wait() + def test_pdb_interaction_on_collection_issue181(self, testdir): + p1 = testdir.makepyfile(""" + import pytest + xxx + """) + child = testdir.spawn_pytest("--pdb %s" % p1) + #child.expect(".*import pytest.*") + child.expect("(Pdb)") + child.sendeof() + child.expect("1 error") + if child.isalive(): + child.wait() + + def test_pdb_interaction_on_internal_error(self, testdir): + testdir.makeconftest(""" + def pytest_runtest_protocol(): + 0/0 + """) + p1 = testdir.makepyfile("def test_func(): pass") + child = testdir.spawn_pytest("--pdb %s" % p1) + #child.expect(".*import pytest.*") + child.expect("(Pdb)") + child.sendeof() + if child.isalive(): + child.wait() + def test_pdb_interaction_capturing_simple(self, testdir): p1 = testdir.makepyfile(""" import pytest @@ -122,6 +150,7 @@ class TestPDB: if child.isalive(): child.wait() + @xfail_if_pdbpp_installed def test_pdb_interaction_doctest(self, testdir): p1 = testdir.makepyfile(""" import pytest diff --git a/testing/test_runner.py b/testing/test_runner.py index dd5997170..916746be1 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1,5 +1,5 @@ import pytest, py, sys, os -from _pytest import runner +from _pytest import runner, main from py._code.code import ReprExceptionInfo class TestSetupState: @@ -298,7 +298,7 @@ class TestSessionReports: class TestClass: pass """) - rep = runner.pytest_make_collect_report(col) + rep = runner.collect_one_node(col) assert not rep.failed assert not rep.skipped assert rep.passed @@ -318,7 +318,7 @@ class TestSessionReports: def test_func(): pass """) - rep = runner.pytest_make_collect_report(col) + rep = main.collect_one_node(col) assert not rep.failed assert not rep.passed assert rep.skipped