From 2d2e0d2f236e21bd0b6722823d95bf0aeaf24316 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 4 Nov 2020 10:04:20 +0100 Subject: [PATCH 1/4] Fix exception chaining on PyPy Fixes #703 --- changelog.d/703.change.rst | 1 + src/attr/_make.py | 29 ++++++++++++++++++++++++----- tests/test_next_gen.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 changelog.d/703.change.rst diff --git a/changelog.d/703.change.rst b/changelog.d/703.change.rst new file mode 100644 index 000000000..c0fde46f6 --- /dev/null +++ b/changelog.d/703.change.rst @@ -0,0 +1 @@ +``raise from`` now works on frozen classes on PyPy. diff --git a/src/attr/_make.py b/src/attr/_make.py index 84b63d55c..49484f935 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -12,6 +12,7 @@ from . import _config, setters from ._compat import ( PY2, + PYPY, isclass, iteritems, metadata_proxy, @@ -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): diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index 941ec0a57..0ebad8d2f 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -4,6 +4,8 @@ import re +from functools import partial + import pytest import attr @@ -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 From e419166305d9e1f0225b58bb37b53a31f94b31cc Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 4 Nov 2020 10:06:11 +0100 Subject: [PATCH 2/4] Add newsfragment --- changelog.d/712.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/712.change.rst diff --git a/changelog.d/712.change.rst b/changelog.d/712.change.rst new file mode 100644 index 000000000..c0fde46f6 --- /dev/null +++ b/changelog.d/712.change.rst @@ -0,0 +1 @@ +``raise from`` now works on frozen classes on PyPy. From 4df8a987beb608af4d3d787abc0ca9010d0e24a5 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 4 Nov 2020 10:13:25 +0100 Subject: [PATCH 3/4] Blankly exclude PyPy from coverage reporting --- pyproject.toml | 4 ++++ src/attr/_compat.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f711b8c06..c41357b30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,10 @@ source = ["src", ".tox/*/site-packages"] [tool.coverage.report] show_missing = true +exclude_lines = [ + # PyPy is unacceptably slow under coverage. + "if PYPY:", +] [tool.black] diff --git a/src/attr/_compat.py b/src/attr/_compat.py index bed5b1357..f55a8265d 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -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,)) From d9370b0e4ad5e7e5f1ee41026d43a5c8ab3ed953 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 4 Nov 2020 10:31:13 +0100 Subject: [PATCH 4/4] Manually add default no cover marker --- pyproject.toml | 1 + src/attr/_compat.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c41357b30..14f65a366 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ source = ["src", ".tox/*/site-packages"] [tool.coverage.report] show_missing = true exclude_lines = [ + "pragma: no cover", # PyPy is unacceptably slow under coverage. "if PYPY:", ] diff --git a/src/attr/_compat.py b/src/attr/_compat.py index f55a8265d..b0ead6e1c 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -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.