Bugfix/slot super (#226)

This commit is contained in:
Tin Tvrtković 2017-08-03 17:04:48 +02:00 committed by Hynek Schlawack
parent 7f2490132b
commit 47583a9459
6 changed files with 109 additions and 4 deletions

View File

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

View File

@ -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>`_

View File

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

View File

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

View File

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

View File

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