Repository: python-attrs/attrs
Pull request #226 (2017-08-01 20:06:20)

Bugfix/slot super
May $DEITY have mercy on our souls.

codecov[bot]: # [Codecov](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=h1) Report
> Merging [#226](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=desc) into [master](https://codecov.io/gh/python-attrs/attrs/commit/7f2490132b6bb862483a94f3cb8ee00bef4bf56f?src=pr&el=desc) will **decrease** coverage by `0.49%`.
> The diff coverage is `83.33%`.

[![Impacted file tree graph](https://codecov.io/gh/python-attrs/attrs/pull/226/graphs/tree.svg?height=150&width=650&token=BxTJx0Ibv7&src=pr)](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=tree)

```diff
@@           Coverage Diff            @@
##           master    #226     +/-   ##
========================================
- Coverage     100%   99.5%   -0.5%     
========================================
  Files           9       9             
  Lines         589     606     +17     
  Branches      128     133      +5     
========================================
+ Hits          589     603     +14     
- Misses          0       2      +2     
- Partials        0       1      +1
```


| [Impacted Files](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/attr/\_make.py](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=tree#diff-c3JjL2F0dHIvX21ha2UucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [src/attr/\_compat.py](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=tree#diff-c3JjL2F0dHIvX2NvbXBhdC5weQ==) | `94.23% <66.66%> (-5.77%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=footer). Last update [7f24901...9e79faf](https://codecov.io/gh/python-attrs/attrs/pull/226?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Tinche: Hm, I think this closure cell rewriting stuff is actually Python 3-only (the PEP that started it all seems to be https://www.python.org/dev/peps/pep-3135/, which is a Py3 PEP). So our PyPy implementation is currently superfluous since we don't test on any PyPy3 versions.

So I should either remove the PyPy-specific implementation or add a recent PyPy3 version to our .travis.yml. I think the latter?
hynek: Pretty sure there’s can’t be a $DEITY after this PR, so don’t worry your pretty head about it.

What we’ll have to worry about is this tho:

![2017-08-02 at 09 24](https://user-images.githubusercontent.com/41240/28862186-6c542910-7764-11e7-8ffd-6c083238f081.png)

That’s totally unacceptable so let’s run it normally and put a pragma no cover with an explanation next to it.
hynek: (Also consider yourself scolded for writing code at 2am in the morning. ಠ_ಠ 🤗)
Tinche: Think I got them all except the parametrization comment. I'm leaving tomorrow on a 10 day trip to Scotland (😀) so you'll either have to wait until I return or drag it over the finish line yourself.

Patch:
diff --git a/.travis.yml b/.travis.yml
index d3f21dc..a96af0f 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 05b3977..60c5c8f 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 <https://github.com/python-attrs/attrs/issues/102>`_
+  `#226 <https://github.com/python-attrs/attrs/issues/226>`_
+- The combination of ``str=True`` and ``slots=True`` now works on Python 2.
   `#198 <https://github.com/python-attrs/attrs/issues/198>`_
-- ``attr.Factory`` is now hashable again.
+- ``attr.Factory`` is hashable again.
   `#204 <https://github.com/python-attrs/attrs/issues/204>`_
 
 
diff --git a/src/attr/_compat.py b/src/attr/_compat.py
index 8cbcf16..439d212 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 b22a3f8..e687dfa 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/tox.ini b/tox.ini
index ab2842e..2554660 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]


Test patch:
diff --git a/tests/test_slots.py b/tests/test_slots.py
index a850575..b930b2e 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