diff --git a/changelog.d/284.change.rst b/changelog.d/284.change.rst new file mode 100644 index 000000000..266599daa --- /dev/null +++ b/changelog.d/284.change.rst @@ -0,0 +1,2 @@ +``ctypes`` is optional now however if it's missing, a bare ``super()`` will not work in slots classes. +This should only happen in special environments like Google App Engine. diff --git a/changelog.d/286.change.rst b/changelog.d/286.change.rst new file mode 100644 index 000000000..266599daa --- /dev/null +++ b/changelog.d/286.change.rst @@ -0,0 +1,2 @@ +``ctypes`` is optional now however if it's missing, a bare ``super()`` will not work in slots classes. +This should only happen in special environments like Google App Engine. diff --git a/src/attr/_compat.py b/src/attr/_compat.py index ed8f65964..8a49341b2 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -3,6 +3,7 @@ import platform import sys import types +import warnings PY2 = sys.version_info[0] == 2 @@ -85,11 +86,54 @@ def iteritems(d): def metadata_proxy(d): return types.MappingProxyType(dict(d)) -if PYPY: # pragma: no cover - def set_closure_cell(cell, value): - cell.__setstate__((value,)) + +def import_ctypes(): # pragma: nocover + """ + Moved into a function for testability. + """ + try: + import ctypes + return ctypes + except ImportError: + return None + + +if not PY2: + def just_warn(*args, **kw): + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + warnings.warn( + "Missing ctypes. Some features like bare super() or accessing " + "__class__ will not work with slots classes.", + RuntimeWarning, + stacklevel=2, + ) else: - import ctypes - set_closure_cell = ctypes.pythonapi.PyCell_Set - set_closure_cell.argtypes = (ctypes.py_object, ctypes.py_object) - set_closure_cell.restype = ctypes.c_int + def just_warn(*args, **kw): # pragma: nocover + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + + +def make_set_closure_cell(): + """ + Moved into a function for testability. + """ + if PYPY: # pragma: no cover + def set_closure_cell(cell, value): + cell.__setstate__((value,)) + else: + ctypes = import_ctypes() + if ctypes is not None: + set_closure_cell = ctypes.pythonapi.PyCell_Set + set_closure_cell.argtypes = (ctypes.py_object, ctypes.py_object) + set_closure_cell.restype = ctypes.c_int + else: + set_closure_cell = just_warn + return set_closure_cell + + +set_closure_cell = make_set_closure_cell() diff --git a/tests/test_slots.py b/tests/test_slots.py index 676bda51e..f44cfc24e 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -13,7 +13,7 @@ import attr -from attr._compat import PY2 +from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell @attr.s @@ -325,76 +325,98 @@ class C2(C1Bare): @pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.") -def test_closure_cell_rewriting(): - """ - Slot classes support proper closure cell rewriting. - - This affects features like `__class__` and the no-arg super(). - """ - non_slot_instance = C1(x=1, y="test") - slot_instance = C1Slots(x=1, y="test") - - assert non_slot_instance.my_class() is C1 - assert slot_instance.my_class() is C1Slots - - # Just assert they return something, and not an exception. - assert non_slot_instance.my_super() - assert slot_instance.my_super() - - -@pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.") -def test_closure_cell_rewriting_inheritance(): - """ - Slot classes support proper closure cell rewriting when inheriting. - - This affects features like `__class__` and the no-arg super(). - """ - @attr.s - class C2(C1): - def my_subclass(self): - return __class__ # NOQA: F821 - - @attr.s - class C2Slots(C1Slots): - def my_subclass(self): - return __class__ # NOQA: F821 - - non_slot_instance = C2(x=1, y="test") - slot_instance = C2Slots(x=1, y="test") - - assert non_slot_instance.my_class() is C1 - assert slot_instance.my_class() is C1Slots - - # Just assert they return something, and not an exception. - assert non_slot_instance.my_super() - assert slot_instance.my_super() - - assert non_slot_instance.my_subclass() is C2 - assert slot_instance.my_subclass() is C2Slots - - -@pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.") -@pytest.mark.parametrize("slots", [True, False]) -def test_closure_cell_rewriting_cls_static(slots): - """ - Slot classes support proper closure cell rewriting for class- and static - methods. - """ - # Python can reuse closure cells, so we create new classes just for - # this test. - - @attr.s(slots=slots) - class C: - @classmethod - def clsmethod(cls): - return __class__ # noqa: F821 - - assert C.clsmethod() is C - - @attr.s(slots=slots) - class D: - @staticmethod - def statmethod(): - return __class__ # noqa: F821 - - assert D.statmethod() is D +class TestClosureCellRewriting(object): + def test_closure_cell_rewriting(self): + """ + Slot classes support proper closure cell rewriting. + + This affects features like `__class__` and the no-arg super(). + """ + non_slot_instance = C1(x=1, y="test") + slot_instance = C1Slots(x=1, y="test") + + assert non_slot_instance.my_class() is C1 + assert slot_instance.my_class() is C1Slots + + # Just assert they return something, and not an exception. + assert non_slot_instance.my_super() + assert slot_instance.my_super() + + def test_inheritance(self): + """ + Slot classes support proper closure cell rewriting when inheriting. + + This affects features like `__class__` and the no-arg super(). + """ + @attr.s + class C2(C1): + def my_subclass(self): + return __class__ # NOQA: F821 + + @attr.s + class C2Slots(C1Slots): + def my_subclass(self): + return __class__ # NOQA: F821 + + non_slot_instance = C2(x=1, y="test") + slot_instance = C2Slots(x=1, y="test") + + assert non_slot_instance.my_class() is C1 + assert slot_instance.my_class() is C1Slots + + # Just assert they return something, and not an exception. + assert non_slot_instance.my_super() + assert slot_instance.my_super() + + assert non_slot_instance.my_subclass() is C2 + assert slot_instance.my_subclass() is C2Slots + + @pytest.mark.parametrize("slots", [True, False]) + def test_cls_static(self, slots): + """ + Slot classes support proper closure cell rewriting for class- and + static methods. + """ + # Python can reuse closure cells, so we create new classes just for + # this test. + + @attr.s(slots=slots) + class C: + @classmethod + def clsmethod(cls): + return __class__ # noqa: F821 + + assert C.clsmethod() is C + + @attr.s(slots=slots) + class D: + @staticmethod + def statmethod(): + return __class__ # noqa: F821 + + assert D.statmethod() is D + + @pytest.mark.skipif( + PYPY, + reason="ctypes are used only on CPython" + ) + def test_missing_ctypes(self, monkeypatch): + """ + Keeps working if ctypes is missing. + + A warning is emitted that points to the actual code. + """ + monkeypatch.setattr(attr._compat, "import_ctypes", lambda: None) + func = make_set_closure_cell() + + with pytest.warns(RuntimeWarning) as wr: + func() + + w = wr.pop() + assert __file__ == w.filename + assert ( + "Missing ctypes. Some features like bare super() or accessing " + "__class__ will not work with slots classes.", + ) == w.message.args + + assert just_warn is func