Skip to content

Commit

Permalink
Fix @Frozen exceptions to allow __traceback__ to be set.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Julian committed Jan 6, 2023
1 parent c6668fa commit 6e3a7eb
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 22 deletions.
35 changes: 13 additions & 22 deletions src/attr/_make.py
Expand Up @@ -12,7 +12,7 @@
# We need to import _compat itself in addition to the _compat members to avoid
# having the thread-local in the globals here.
from . import _compat, _config, setters
from ._compat import PY310, PYPY, _AnnotationExtractor, set_closure_cell
from ._compat import PY310, _AnnotationExtractor, set_closure_cell
from .exceptions import (
DefaultAlreadySetError,
FrozenInstanceError,
Expand Down Expand Up @@ -582,28 +582,19 @@ def _transform_attrs(
return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map))


if PYPY:

def _frozen_setattrs(self, name, value):
"""
Attached to frozen classes as __setattr__.
"""
if isinstance(self, BaseException) and name in (
"__cause__",
"__context__",
):
BaseException.__setattr__(self, name, value)
return

raise FrozenInstanceError()

else:
def _frozen_setattrs(self, name, value):
"""
Attached to frozen classes as __setattr__.
"""
if isinstance(self, BaseException) and name in (
"__cause__",
"__context__",
"__traceback__",
):
BaseException.__setattr__(self, name, value)
return

def _frozen_setattrs(self, name, value):
"""
Attached to frozen classes as __setattr__.
"""
raise FrozenInstanceError()
raise FrozenInstanceError()


def _frozen_delattrs(self, name):
Expand Down
33 changes: 33 additions & 0 deletions tests/test_next_gen.py
Expand Up @@ -6,6 +6,7 @@

import re

from contextlib import contextmanager
from functools import partial

import pytest
Expand Down Expand Up @@ -312,6 +313,38 @@ class MyException(Exception):
assert "foo" == ei.value.x
assert ei.value.__cause__ is None

@pytest.mark.parametrize(
"decorator",
[
partial(_attr.s, frozen=True, slots=True, auto_exc=True),
attrs.frozen,
attrs.define,
attrs.mutable,
],
)
def test_setting_traceback_on_exception(self, decorator):
"""
contextlib.contextlib (re-)sets __traceback__ on raised exceptions.
Ensure that works, as well as if done explicitly
"""

@decorator
class MyException(Exception):
pass

@contextmanager
def do_nothing():
yield

with do_nothing(), pytest.raises(MyException) as ei:
raise MyException()

assert isinstance(ei.value, MyException)

# this should not raise an exception either
ei.value.__traceback__ = ei.value.__traceback__

def test_converts_and_validates_by_default(self):
"""
If no on_setattr is set, assume setters.convert, setters.validate.
Expand Down

0 comments on commit 6e3a7eb

Please sign in to comment.