Make -P (pdb) work better with exceptions triggered from events

Previously, if an exception was raised from an event listener, and the
`-P` option was specified, the debugger would be started not for the
original error but for the `ExtensionError` wrapping it that was
raised by `EventManager.emit`.  That made it difficult to debug the
error.

With this change, when `-P` is specified, wrapping of errors in
`ExtensionError` is disabled, which allows pdb to debug the original
error.
This commit is contained in:
Jeremy Maitin-Shepard 2022-07-03 17:56:14 -07:00
parent 7e76f2c307
commit 66f9ee4afd
5 changed files with 31 additions and 3 deletions

View File

@ -30,6 +30,7 @@ Features added
Bugs fixed
----------
* #10634: Make -P (pdb) option work better with exceptions triggered from events
* #10031: py domain: Fix spurious whitespace in unparsing various operators (``+``,
``-``, ``~``, and ``**``). Patch by Adam Turner.
* #10460: logging: Always show node source locations as absolute paths.

View File

@ -131,7 +131,8 @@ class Sphinx:
buildername: str, confoverrides: Dict = None,
status: Optional[IO] = sys.stdout, warning: Optional[IO] = sys.stderr,
freshenv: bool = False, warningiserror: bool = False, tags: List[str] = None,
verbosity: int = 0, parallel: int = 0, keep_going: bool = False) -> None:
verbosity: int = 0, parallel: int = 0, keep_going: bool = False,
pdb: bool = False) -> None:
self.phase = BuildPhase.INITIALIZATION
self.verbosity = verbosity
self.extensions: Dict[str, Extension] = {}
@ -173,6 +174,7 @@ class Sphinx:
self.warningiserror = False
else:
self.warningiserror = warningiserror
self.pdb = pdb
logging.setup(self, self._status, self._warning)
self.events = EventManager(self)

View File

@ -272,7 +272,8 @@ def build_main(argv: List[str] = sys.argv[1:]) -> int:
app = Sphinx(args.sourcedir, args.confdir, args.outputdir,
args.doctreedir, args.builder, confoverrides, status,
warning, args.freshenv, args.warningiserror,
args.tags, args.verbosity, args.jobs, args.keep_going)
args.tags, args.verbosity, args.jobs, args.keep_going,
args.pdb)
app.build(args.force_all, filenames)
return app.statuscode
except (Exception, KeyboardInterrupt) as exc:

View File

@ -98,6 +98,9 @@ class EventManager:
except SphinxError:
raise
except Exception as exc:
if self.app.pdb:
# Just pass through the error, so that it can be debugged.
raise
modname = safe_getattr(listener.handler, '__module__', None)
raise ExtensionError(__("Handler %r for event %r threw an exception") %
(listener.handler, name), exc, modname=modname) from exc

View File

@ -19,11 +19,16 @@ def test_event_priority():
assert result == [3, 1, 2, 5, 4]
class FakeApp:
def __init__(self, pdb: bool = False):
self.pdb = pdb
def test_event_allowed_exceptions():
def raise_error(app):
raise RuntimeError
events = EventManager(object()) # pass an dummy object as an app
events = EventManager(FakeApp()) # pass an dummy object as an app
events.connect('builder-inited', raise_error, priority=500)
# all errors are converted to ExtensionError
@ -33,3 +38,19 @@ def test_event_allowed_exceptions():
# Allow RuntimeError (pass-through)
with pytest.raises(RuntimeError):
events.emit('builder-inited', allowed_exceptions=(RuntimeError,))
def test_event_pdb():
def raise_error(app):
raise RuntimeError
events = EventManager(FakeApp(pdb=True)) # pass an dummy object as an app
events.connect('builder-inited', raise_error, priority=500)
# errors aren't converted
with pytest.raises(RuntimeError):
events.emit('builder-inited')
# Allow RuntimeError (pass-through)
with pytest.raises(RuntimeError):
events.emit('builder-inited', allowed_exceptions=(RuntimeError,))