diff --git a/.travis.yml b/.travis.yml index d3f21dcd..a96af0f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ matrix: env: TOXENV=py36 - python: "pypy" env: TOXENV=pypy + - python: "pypy3.5-5.8.0" + env: TOXENV=pypy3 # Meta - python: "3.6" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 05b39772..60c5c8fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,9 +24,12 @@ Deprecations: Changes: ^^^^^^^^ -- Fix *str* on Python 2 when ``slots=True``. +- ``super()`` and ``__class__`` now work on Python 3 when ``slots=True``. + `#102 `_ + `#226 `_ +- The combination of ``str=True`` and ``slots=True`` now works on Python 2. `#198 `_ -- ``attr.Factory`` is now hashable again. +- ``attr.Factory`` is hashable again. `#204 `_ diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 8cbcf16f..439d2124 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -1,10 +1,12 @@ from __future__ import absolute_import, division, print_function +import platform import sys import types PY2 = sys.version_info[0] == 2 +PYPY = platform.python_implementation() == "PyPy" if PY2: @@ -88,3 +90,12 @@ else: def metadata_proxy(d): return types.MappingProxyType(dict(d)) + +if PYPY: # pragma: no cover + def set_closure_cell(cell, value): + cell.__setstate__((value,)) +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 diff --git a/src/attr/_make.py b/src/attr/_make.py index b22a3f8d..e687dfad 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -6,7 +6,14 @@ import linecache from operator import itemgetter from . import _config -from ._compat import PY2, iteritems, isclass, iterkeys, metadata_proxy +from ._compat import ( + PY2, + iteritems, + isclass, + iterkeys, + metadata_proxy, + set_closure_cell, +) from .exceptions import ( DefaultAlreadySetError, FrozenInstanceError, @@ -373,12 +380,27 @@ def attributes(maybe_cls=None, these=None, repr_ns=None, # It might not actually be in there, e.g. if using 'these'. cls_dict.pop(ca_name, None) cls_dict.pop("__dict__", None) + old_cls = cls qualname = getattr(cls, "__qualname__", None) cls = type(cls)(cls.__name__, cls.__bases__, cls_dict) if qualname is not None: cls.__qualname__ = qualname + # The following is a fix for + # https://github.com/python-attrs/attrs/issues/102. On Python 3, + # if a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in cls.__dict__.values(): + closure_cells = getattr(item, "__closure__", None) + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + if cell.cell_contents is old_cls: + set_closure_cell(cell, cls) + return cls # attrs_or class type depends on the usage of the decorator. It's a class diff --git a/tests/test_slots.py b/tests/test_slots.py index a850575a..b930b2ef 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -13,6 +13,8 @@ except: # Won't be an import error. import attr +from attr._compat import PY2 + @attr.s class C1(object): @@ -30,6 +32,14 @@ class C1(object): def staticmethod(): return "staticmethod" + if not PY2: + def my_class(self): + return __class__ # NOQA: F821 + + def my_super(self): + """Just to test out the no-arg super.""" + return super().__repr__() + @attr.s(slots=True, hash=True) class C1Slots(object): @@ -47,6 +57,14 @@ class C1Slots(object): def staticmethod(): return "staticmethod" + if not PY2: + def my_class(self): + return __class__ # NOQA: F821 + + def my_super(self): + """Just to test out the no-arg super.""" + return super().__repr__() + def test_slots_being_used(): """ @@ -298,3 +316,52 @@ def test_bare_inheritance_from_slots(): hash(c2) # Just to assert it doesn't raise. assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2) + + +@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 diff --git a/tox.ini b/tox.ini index ab2842e9..2554660b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,pypy,flake8,manifest,docs,readme,coverage-report +envlist = py27,py34,py35,py36,pypy,pypy3,flake8,manifest,docs,readme,coverage-report [testenv]