Skip to content

Commit

Permalink
Bugfix/slot super (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche authored and hynek committed Aug 3, 2017
1 parent 7f24901 commit 47583a9
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 5 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`_


Expand Down
11 changes: 11 additions & 0 deletions src/attr/_compat.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -88,3 +90,12 @@ def iterkeys(d):

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
24 changes: 23 additions & 1 deletion src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
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,
Expand Down Expand Up @@ -373,12 +380,27 @@ def wrap(cls):
# 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
Expand Down
67 changes: 67 additions & 0 deletions tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import attr

from attr._compat import PY2


@attr.s
class C1(object):
Expand All @@ -30,6 +32,14 @@ def classmethod(cls):
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):
Expand All @@ -47,6 +57,14 @@ def classmethod(cls):
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():
"""
Expand Down Expand Up @@ -298,3 +316,52 @@ class C2(C1Bare):
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
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -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]
Expand Down

0 comments on commit 47583a9

Please sign in to comment.