Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add %xmode Doctest mode for doctest friendly tracebacks
  • Loading branch information
Jatin-Shihora committed Apr 10, 2026
commit f1df8bbe8862e0107e0003685f0084eaf8826347
2 changes: 1 addition & 1 deletion IPython/core/interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ def input_transformers_cleanup(self):
separate_out2 = SeparateUnicode('').tag(config=True)
wildcards_case_sensitive = Bool(True).tag(config=True)
xmode = CaselessStrEnum(
("Context", "Plain", "Verbose", "Minimal", "Docs"),
("Context", "Plain", "Verbose", "Minimal", "Docs", "Doctest"),
default_value="Context",
help="Switch modes for the IPython exception handlers.",
).tag(config=True)
Expand Down
4 changes: 3 additions & 1 deletion IPython/core/magics/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def colors(self, parameter_s=''):
def xmode(self, parameter_s=''):
"""Switch modes for the exception handlers.

Valid modes: Plain, Context, Verbose, Minimal, and Docs.
Valid modes: Plain, Context, Verbose, Minimal, Docs, and Doctest.

- ``Plain``: similar to Python's default traceback.
- ``Context``: shows several lines of surrounding context for each
Expand All @@ -355,6 +355,8 @@ def xmode(self, parameter_s=''):
a traceback.
- ``Docs``: a stripped-down version of Verbose, designed for use
when running doctests.
- ``Doctest``: shows only the traceback header, an ellipsis, and the
exception line, for easy copy-paste into Python doctests.

If called without arguments, cycles through the available modes.

Expand Down
46 changes: 44 additions & 2 deletions IPython/core/ultratb.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,46 @@ def get_exception_only(self, etype, value):
"""
return ListTB.structured_traceback(self, etype, value)

def structured_traceback_doctest(
self,
etype,
evalue,
etb=None,
tb_offset=None,
context=5,
):
"""Return a doctest-freindly traceback.

Shows only the header, an ellipsis, and the excepttion line.
"""
# Handle chained exception rule
if isinstance(etb, tuple):
etb, chained_exc_ids = etb
else:
chained_exc_ids = set()

have_traceback = etb is not None

out_list = []
if have_traceback:
out_list.append("Traceback (most recent call last):\n")
out_list.append(" ...\n")

lines = "".join(self._format_exception_only(etype, evalue))
out_list.append(lines)

# Handle chained exceptions
if etb is not None:
exception = self.get_parts_of_chained_exception(evalue)
if exception and (id(exception[1]) not in chained_exc_ids):
chained_exception_message = (self.prepare_chained_exception_message(evalue.__cause__)[0] if evalue is not None else [""])
etype, evalue, etb = exception
chained_exc_ids.add(id(exception[1]))
chained_tb = self.structured_traceback_doctest(etype, evalue, (etb, chained_exc_ids), 0, context)
out_list = chained_tb + chained_exception_message + out_list

return out_list

def show_exception_only(
self, etype: BaseException | None, evalue: TracebackType | None
) -> None:
Expand Down Expand Up @@ -1059,7 +1099,7 @@ def __init__(
debugger_cls=None,
):
# NEVER change the order of this list. Put new modes at the end:
self.valid_modes = ["Plain", "Context", "Verbose", "Minimal", "Docs"]
self.valid_modes = ["Plain", "Context", "Verbose", "Minimal", "Docs", "Doctest"]
self.verbose_modes = self.valid_modes[1:3]

VerboseTB.__init__(
Expand All @@ -1077,7 +1117,7 @@ def __init__(
# Different types of tracebacks are joined with different separators to
# form a single string. They are taken from this dict
self._join_chars = dict(
Plain="", Context="\n", Verbose="\n", Minimal="", Docs=""
Plain="", Context="\n", Verbose="\n", Minimal="", Docs="", Doctest=""
)
# set_mode also sets the tb_join_char attribute
self.set_mode(mode)
Expand Down Expand Up @@ -1114,6 +1154,8 @@ def structured_traceback(

elif mode == "Minimal":
return ListTB.get_exception_only(self, etype, evalue)
elif mode == "Doctest":
return ListTB.structured_traceback_doctest(self, etype, evalue, etb, tb_offset, context)
else:
# We must check the source cache because otherwise we can print
# out-of-date source code.
Expand Down
10 changes: 10 additions & 0 deletions docs/source/whatsnew/pr/xmode-doctest-feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Doctest xmode
=============

Added a new ``%xmode Doctest`` mode that formats tracebacks for easy
copy-paste into Python doctests. The output shows only the traceback
header, a literal ellipsis, and the exception line::

Traceback (most recent call last):
...
ZeroDivisionError: division by zero
2 changes: 1 addition & 1 deletion tests/test_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ def test_cd_force_quiet():
def test_xmode():
# Calling xmode three times should be a no-op
xmode = _ip.InteractiveTB.mode
for i in range(5):
for i in range(len(_ip.InteractiveTB.valid_modes)):
_ip.run_line_magic("xmode", "")
assert _ip.InteractiveTB.mode == xmode

Expand Down
21 changes: 21 additions & 0 deletions tests/test_ultratb.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,24 @@ def testSyntaxError():
expected = "SyntaxError\n"
with tt.AssertPrints(expected):
ip.run_cell(cell)

def test_xmode_doctest():
"""Test that %xmode doctest produces doctest-friendly output."""
ip.run_cell("%xmode doctest")

# Basic exception
with tt.AssertPrints(["Traceback (most recent call last):", " ...", "ZeroDivisionError: division by zero"]):
ip.run_cell("1/0")

# Chained exception
cell = "\n".join([
"try:",
" 1/0",
"except:",
" raise ValueError('bad')",
])
with tt.AssertPrints(["ZeroDivisionError: division by zero", "ValueError: bad"]):
ip.run_cell(cell)

# Restore default mode
ip.run_cell("%xmode context")
Loading