Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/703.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``raise from`` now works on frozen classes on PyPy.
1 change: 1 addition & 0 deletions changelog.d/712.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``raise from`` now works on frozen classes on PyPy.
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ source = ["src", ".tox/*/site-packages"]

[tool.coverage.report]
show_missing = true
exclude_lines = [
"pragma: no cover",
# PyPy is unacceptably slow under coverage.
"if PYPY:",
]


[tool.black]
Expand Down
4 changes: 2 additions & 2 deletions src/attr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def metadata_proxy(d):
res.data.update(d) # We blocked update, so we have to do it like this.
return res

def just_warn(*args, **kw): # pragma: nocover
def just_warn(*args, **kw): # pragma: no cover
"""
We only warn on Python 3 because we are not aware of any concrete
consequences of not setting the cell on Python 2.
Expand Down Expand Up @@ -132,7 +132,7 @@ def make_set_closure_cell():
"""
# pypy makes this easy. (It also supports the logic below, but
# why not do the easy/fast thing?)
if PYPY: # pragma: no cover
if PYPY:

def set_closure_cell(cell, value):
cell.__setstate__((value,))
Expand Down
29 changes: 24 additions & 5 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from . import _config, setters
from ._compat import (
PY2,
PYPY,
isclass,
iteritems,
metadata_proxy,
Expand Down Expand Up @@ -527,11 +528,29 @@ def _transform_attrs(
return _Attributes((attrs, base_attrs, base_attr_map))


def _frozen_setattrs(self, name, value):
"""
Attached to frozen classes as __setattr__.
"""
raise FrozenInstanceError()
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__.
"""
raise FrozenInstanceError()


def _frozen_delattrs(self, name):
Expand Down
31 changes: 31 additions & 0 deletions tests/test_next_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import re

from functools import partial

import pytest

import attr
Expand Down Expand Up @@ -238,3 +240,32 @@ class B:
@attr.define(on_setattr=attr.setters.validate)
class C(A):
pass

@pytest.mark.parametrize(
"decorator",
[
partial(attr.s, frozen=True, slots=True, auto_exc=True),
attr.frozen,
attr.define,
attr.mutable,
],
)
def test_discard_context(self, decorator):
"""
raise from None works.

Regression test for #703.
"""

@decorator
class MyException(Exception):
x: str = attr.ib()

with pytest.raises(MyException) as ei:
try:
raise ValueError()
except ValueError:
raise MyException("foo") from None

assert "foo" == ei.value.x
assert ei.value.__cause__ is None