agronholm / exceptiongroup

Backport of PEP 654 (exception groups)
Other
42 stars 20 forks source link

Traceback broken for AttributeError with name only #43

Closed sanderr closed 1 year ago

sanderr commented 1 year ago

Observed behavior

Running pytest on the snippet below results in an error while attempting to construct the traceback.

from collections import abc

class NamedAttributeError(AttributeError):
    def __init__(self, name: str) -> None:
        self.name: str = name

def test_exceptiongroup_error() -> None:
    raise NamedAttributeError(name="mykey")
tests/test_exceptiongroup.py 
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/main.py", line 270, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/main.py", line 324, in _main
INTERNALERROR>     config.hook.pytest_runtestloop(session=session)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_callers.py", line 60, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_result.py", line 60, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_callers.py", line 39, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/main.py", line 349, in pytest_runtestloop
INTERNALERROR>     item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_callers.py", line 60, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_result.py", line 60, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_callers.py", line 39, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/runner.py", line 112, in pytest_runtest_protocol
INTERNALERROR>     runtestprotocol(item, nextitem=nextitem)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/runner.py", line 131, in runtestprotocol
INTERNALERROR>     reports.append(call_and_report(item, "call", log))
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/runner.py", line 222, in call_and_report
INTERNALERROR>     report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_callers.py", line 55, in _multicall
INTERNALERROR>     gen.send(outcome)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/skipping.py", line 265, in pytest_runtest_makereport
INTERNALERROR>     rep = outcome.get_result()
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_result.py", line 60, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/pluggy/_callers.py", line 39, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/runner.py", line 366, in pytest_runtest_makereport
INTERNALERROR>     return TestReport.from_item_and_call(item, call)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/reports.py", line 349, in from_item_and_call
INTERNALERROR>     longrepr = item.repr_failure(excinfo)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/python.py", line 1823, in repr_failure
INTERNALERROR>     return self._repr_failure_py(excinfo, style=style)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/nodes.py", line 484, in _repr_failure_py
INTERNALERROR>     return excinfo.getrepr(
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/_code/code.py", line 669, in getrepr
INTERNALERROR>     return fmt.repr_excinfo(self)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/_code/code.py", line 944, in repr_excinfo
INTERNALERROR>     reprtraceback = self.repr_traceback(excinfo_)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/_code/code.py", line 871, in repr_traceback
INTERNALERROR>     reprentry = self.repr_traceback_entry(entry, einfo)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/_code/code.py", line 822, in repr_traceback_entry
INTERNALERROR>     s = self.get_source(source, line_index, excinfo, short=short)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/_code/code.py", line 760, in get_source
INTERNALERROR>     lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/_code/code.py", line 772, in get_exconly
INTERNALERROR>     exlines = excinfo.exconly(tryshort=True).split("\n")
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/_pytest/_code/code.py", line 588, in exconly
INTERNALERROR>     lines = format_exception_only(self.type, self.value)
INTERNALERROR>   File "/usr/lib/python3.9/traceback.py", line 140, in format_exception_only
INTERNALERROR>     return list(TracebackException(etype, value, None).format_exception_only())
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/exceptiongroup/_formatting.py", line 125, in __init__
INTERNALERROR>     suggestion = _compute_suggestion_error(exc_value, exc_traceback)
INTERNALERROR>   File "/home/sander/.virtualenvs/sandbox-39/lib/python3.9/site-packages/exceptiongroup/_formatting.py", line 451, in _compute_suggestion_error
INTERNALERROR>     obj = exc_value.obj
INTERNALERROR> AttributeError: 'NamedAttributeError' object has no attribute 'obj'

==================================== no tests ran in 0.01s ====================================

Details

This behavior was introduced in 1.0.3. It seems to assume that if the name attribute is set, the obj attribute will be set as well. As far as I understand there are two issues with that assumption:

  1. The Python documentation doesn't mention any invariant that either both or neither should be set. It seems valid to me to set only the name and not the object.
  2. The name and obj attributes are only introduced in Python 3.10. In Python 3.9 and below it is perfectly valid to introduce a custom subclass that happens to have one or both of these names, which might have different meanings entirely than the ones defined in 3.10+.

The second issue could be argued to be of lesser importance (and more difficult to address). The first I would call a breaking bug.

Environment

I'm using the latest versions of pytest-7.2.0 and exceptiongroup-1.0.3 at the time of writing, but I believe older pytest versions are also susceptible to this issue.

agronholm commented 1 year ago

@cfbolz comments?

cfbolz commented 1 year ago

right, I see. yes, it should definitely not crash, sorry for not thinking about this case. I'm fixing it.

sanderr commented 1 year ago

No problem, thanks for the quick resolution!

agronholm commented 1 year ago

@sanderr are you able to test against main? I will make a release then.

sanderr commented 1 year ago

It works as expected:

tests/test_exceptiongroup.py F                                                          [100%]

========================================== FAILURES ===========================================
__________________________________ test_exceptiongroup_error __________________________________

    def test_exceptiongroup_error() -> None:
>       raise NamedAttributeError(name="mykey")
E       test_exceptiongroup.NamedAttributeError

tests/test_exceptiongroup.py:10: NamedAttributeError
agronholm commented 1 year ago

Released v1.0.4.

sanderr commented 1 year ago

Thanks a lot! I really hadn't expected this to be resolved today.