Skip to content

Commit

Permalink
Fix @Frozen exceptions to allow __traceback__ to be set. (#1081)
Browse files Browse the repository at this point in the history
  • Loading branch information
Julian committed Jan 8, 2023
1 parent c6668fa commit a71fbba
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 22 deletions.
1 change: 1 addition & 0 deletions changelog.d/1081.change.md
@@ -0,0 +1 @@
Fix frozen exception classes when raised within e.g. `contextlib.contextmanager`, which mutates their `__traceback__` attributes.
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 a71fbba

Please sign in to comment.