Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix @frozen exceptions to allow __traceback__ to be set. #1081

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1081.change.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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