Skip to content

Commit

Permalink
Make ctypes optional (#286)
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek authored Nov 10, 2017
1 parent 7501cec commit 1e6627c
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 81 deletions.
2 changes: 2 additions & 0 deletions changelog.d/284.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``ctypes`` is optional now however if it's missing, a bare ``super()`` will not work in slots classes.
This should only happen in special environments like Google App Engine.
2 changes: 2 additions & 0 deletions changelog.d/286.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``ctypes`` is optional now however if it's missing, a bare ``super()`` will not work in slots classes.
This should only happen in special environments like Google App Engine.
58 changes: 51 additions & 7 deletions src/attr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import platform
import sys
import types
import warnings


PY2 = sys.version_info[0] == 2
Expand Down Expand Up @@ -85,11 +86,54 @@ def iteritems(d):
def metadata_proxy(d):
return types.MappingProxyType(dict(d))

if PYPY: # pragma: no cover
def set_closure_cell(cell, value):
cell.__setstate__((value,))

def import_ctypes(): # pragma: nocover
"""
Moved into a function for testability.
"""
try:
import ctypes
return ctypes
except ImportError:
return None


if not PY2:
def just_warn(*args, **kw):
"""
We only warn on Python 3 because we are not aware of any concrete
consequences of not setting the cell on Python 2.
"""
warnings.warn(
"Missing ctypes. Some features like bare super() or accessing "
"__class__ will not work with slots classes.",
RuntimeWarning,
stacklevel=2,
)
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
def just_warn(*args, **kw): # pragma: nocover
"""
We only warn on Python 3 because we are not aware of any concrete
consequences of not setting the cell on Python 2.
"""


def make_set_closure_cell():
"""
Moved into a function for testability.
"""
if PYPY: # pragma: no cover
def set_closure_cell(cell, value):
cell.__setstate__((value,))
else:
ctypes = import_ctypes()
if ctypes is not None:
set_closure_cell = ctypes.pythonapi.PyCell_Set
set_closure_cell.argtypes = (ctypes.py_object, ctypes.py_object)
set_closure_cell.restype = ctypes.c_int
else:
set_closure_cell = just_warn
return set_closure_cell


set_closure_cell = make_set_closure_cell()
170 changes: 96 additions & 74 deletions tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import attr

from attr._compat import PY2
from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell


@attr.s
Expand Down Expand Up @@ -325,76 +325,98 @@ class C2(C1Bare):


@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


@pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.")
@pytest.mark.parametrize("slots", [True, False])
def test_closure_cell_rewriting_cls_static(slots):
"""
Slot classes support proper closure cell rewriting for class- and static
methods.
"""
# Python can reuse closure cells, so we create new classes just for
# this test.

@attr.s(slots=slots)
class C:
@classmethod
def clsmethod(cls):
return __class__ # noqa: F821

assert C.clsmethod() is C

@attr.s(slots=slots)
class D:
@staticmethod
def statmethod():
return __class__ # noqa: F821

assert D.statmethod() is D
class TestClosureCellRewriting(object):
def test_closure_cell_rewriting(self):
"""
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()

def test_inheritance(self):
"""
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

@pytest.mark.parametrize("slots", [True, False])
def test_cls_static(self, slots):
"""
Slot classes support proper closure cell rewriting for class- and
static methods.
"""
# Python can reuse closure cells, so we create new classes just for
# this test.

@attr.s(slots=slots)
class C:
@classmethod
def clsmethod(cls):
return __class__ # noqa: F821

assert C.clsmethod() is C

@attr.s(slots=slots)
class D:
@staticmethod
def statmethod():
return __class__ # noqa: F821

assert D.statmethod() is D

@pytest.mark.skipif(
PYPY,
reason="ctypes are used only on CPython"
)
def test_missing_ctypes(self, monkeypatch):
"""
Keeps working if ctypes is missing.
A warning is emitted that points to the actual code.
"""
monkeypatch.setattr(attr._compat, "import_ctypes", lambda: None)
func = make_set_closure_cell()

with pytest.warns(RuntimeWarning) as wr:
func()

w = wr.pop()
assert __file__ == w.filename
assert (
"Missing ctypes. Some features like bare super() or accessing "
"__class__ will not work with slots classes.",
) == w.message.args

assert just_warn is func

0 comments on commit 1e6627c

Please sign in to comment.