diff --git a/IPython/core/tests/test_exceptiongroup_tb.py b/IPython/core/tests/test_exceptiongroup_tb.py new file mode 100644 index 0000000..0dbd241 --- /dev/null +++ b/IPython/core/tests/test_exceptiongroup_tb.py @@ -0,0 +1,109 @@ +import unittest +import re +from IPython.utils.capture import capture_output +import sys +import pytest +from tempfile import TemporaryDirectory +from IPython.testing import tools as tt + +def _exceptiongroup_common( + outer_chain: str, + inner_chain: str, + native: bool, +) -> None: + pre_raise = "exceptiongroup." if not native else "" + pre_catch = pre_raise if sys.version_info < (3, 11) else "" + filestr = f""" + {"import exceptiongroup" if not native else ""} + import pytest + + def f(): raise ValueError("From f()") + def g(): raise BaseException("From g()") + + def inner(inner_chain): + excs = [] + for callback in [f, g]: + try: + callback() + except BaseException as err: + excs.append(err) + if excs: + if inner_chain == "none": + raise {pre_raise}BaseExceptionGroup("Oops", excs) + try: + raise SyntaxError() + except SyntaxError as e: + if inner_chain == "from": + raise {pre_raise}BaseExceptionGroup("Oops", excs) from e + else: + raise {pre_raise}BaseExceptionGroup("Oops", excs) + + def outer(outer_chain, inner_chain): + try: + inner(inner_chain) + except {pre_catch}BaseExceptionGroup as e: + if outer_chain == "none": + raise + if outer_chain == "from": + raise IndexError() from e + else: + raise IndexError + + + outer("{outer_chain}", "{inner_chain}") + """ + with capture_output() as cap: + ip.run_cell(filestr) + + match_lines = [] + if inner_chain == "another": + match_lines += [ + "During handling of the above exception, another exception occurred:", + ] + elif inner_chain == 'from': + match_lines += [ + "The above exception was the direct cause of the following exception:", + ] + + match_lines += [ + " + Exception Group Traceback (most recent call last):", + f" | {pre_catch}BaseExceptionGroup: Oops (2 sub-exceptions)", + " | ValueError: From f()", + " | BaseException: From g()", + ] + + if outer_chain == "another": + match_lines += [ + "During handling of the above exception, another exception occurred:", + "IndexError", + ] + elif outer_chain == "from": + match_lines += [ + "The above exception was the direct cause of the following exception:", + "IndexError", + ] + + error_lines = cap.stderr.split("\n") + + err_index = match_index = 0 + for expected in match_lines: + for i,actual in enumerate(error_lines): + if actual == expected: + error_lines = error_lines[i+1:] + break + else: + assert False, f'{expected} not found in cap.stderr' + +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Native ExceptionGroup not implemented" + ) +@pytest.mark.parametrize("outer_chain", ["none", "from", "another"]) +@pytest.mark.parametrize("inner_chain", ["none", "from", "another"]) +def test_native_exceptiongroup(outer_chain, inner_chain) -> None: + _exceptiongroup_common(outer_chain, inner_chain, native=True) + +@pytest.mark.parametrize("outer_chain", ["none", "from", "another"]) +@pytest.mark.parametrize("inner_chain", ["none", "from", "another"]) +def test_native_exceptiongroup(outer_chain, inner_chain) -> None: + pytest.importorskip("exceptiongroup") + _exceptiongroup_common(outer_chain, inner_chain, native=False) diff --git a/docs/environment.yml b/docs/environment.yml index 9fe7d4a..efc7e57 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -8,6 +8,7 @@ dependencies: - sphinx>=4.2 - sphinx_rtd_theme - numpy + - exceptiongroup - testpath - matplotlib - pip diff --git a/setup.cfg b/setup.cfg index 319fe4f..0ec359a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,7 +33,7 @@ install_requires = backcall colorama; sys_platform == "win32" decorator - exceptiongroup; python_version<'3.10' + exceptiongroup; python_version<'3.11' jedi>=0.16 matplotlib-inline pexpect>4.3; sys_platform != "win32"