Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/slot super #226

Merged
merged 14 commits into from Aug 3, 2017
2 changes: 2 additions & 0 deletions .travis.yml
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
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
@@ -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
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
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():

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

"""
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
@@ -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