Skip to content

Commit 6e3a7eb

Browse files
committed
Fix @Frozen exceptions to allow __traceback__ to be set.
E.g. contextlib.contextmanager does so whenever an exception is raised in its body, and does so even on CPython, so merging the two code paths now seems reasonable.
1 parent c6668fa commit 6e3a7eb

File tree

2 files changed

+46
-22
lines changed

2 files changed

+46
-22
lines changed

src/attr/_make.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# We need to import _compat itself in addition to the _compat members to avoid
1313
# having the thread-local in the globals here.
1414
from . import _compat, _config, setters
15-
from ._compat import PY310, PYPY, _AnnotationExtractor, set_closure_cell
15+
from ._compat import PY310, _AnnotationExtractor, set_closure_cell
1616
from .exceptions import (
1717
DefaultAlreadySetError,
1818
FrozenInstanceError,
@@ -582,28 +582,19 @@ def _transform_attrs(
582582
return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map))
583583

584584

585-
if PYPY:
586-
587-
def _frozen_setattrs(self, name, value):
588-
"""
589-
Attached to frozen classes as __setattr__.
590-
"""
591-
if isinstance(self, BaseException) and name in (
592-
"__cause__",
593-
"__context__",
594-
):
595-
BaseException.__setattr__(self, name, value)
596-
return
597-
598-
raise FrozenInstanceError()
599-
600-
else:
585+
def _frozen_setattrs(self, name, value):
586+
"""
587+
Attached to frozen classes as __setattr__.
588+
"""
589+
if isinstance(self, BaseException) and name in (
590+
"__cause__",
591+
"__context__",
592+
"__traceback__",
593+
):
594+
BaseException.__setattr__(self, name, value)
595+
return
601596

602-
def _frozen_setattrs(self, name, value):
603-
"""
604-
Attached to frozen classes as __setattr__.
605-
"""
606-
raise FrozenInstanceError()
597+
raise FrozenInstanceError()
607598

608599

609600
def _frozen_delattrs(self, name):

tests/test_next_gen.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import re
88

9+
from contextlib import contextmanager
910
from functools import partial
1011

1112
import pytest
@@ -312,6 +313,38 @@ class MyException(Exception):
312313
assert "foo" == ei.value.x
313314
assert ei.value.__cause__ is None
314315

316+
@pytest.mark.parametrize(
317+
"decorator",
318+
[
319+
partial(_attr.s, frozen=True, slots=True, auto_exc=True),
320+
attrs.frozen,
321+
attrs.define,
322+
attrs.mutable,
323+
],
324+
)
325+
def test_setting_traceback_on_exception(self, decorator):
326+
"""
327+
contextlib.contextlib (re-)sets __traceback__ on raised exceptions.
328+
329+
Ensure that works, as well as if done explicitly
330+
"""
331+
332+
@decorator
333+
class MyException(Exception):
334+
pass
335+
336+
@contextmanager
337+
def do_nothing():
338+
yield
339+
340+
with do_nothing(), pytest.raises(MyException) as ei:
341+
raise MyException()
342+
343+
assert isinstance(ei.value, MyException)
344+
345+
# this should not raise an exception either
346+
ei.value.__traceback__ = ei.value.__traceback__
347+
315348
def test_converts_and_validates_by_default(self):
316349
"""
317350
If no on_setattr is set, assume setters.convert, setters.validate.

0 commit comments

Comments
 (0)