424 lines
10 KiB
Python
424 lines
10 KiB
Python
"""
|
|
Unit tests for slot-related functionality.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
import attr
|
|
|
|
from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell
|
|
|
|
|
|
# Pympler doesn't work on PyPy.
|
|
try:
|
|
from pympler.asizeof import asizeof
|
|
has_pympler = True
|
|
except BaseException: # Won't be an import error.
|
|
has_pympler = False
|
|
|
|
|
|
@attr.s
|
|
class C1(object):
|
|
x = attr.ib(validator=attr.validators.instance_of(int))
|
|
y = attr.ib()
|
|
|
|
def method(self):
|
|
return self.x
|
|
|
|
@classmethod
|
|
def classmethod(cls):
|
|
return "clsmethod"
|
|
|
|
@staticmethod
|
|
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):
|
|
x = attr.ib(validator=attr.validators.instance_of(int))
|
|
y = attr.ib()
|
|
|
|
def method(self):
|
|
return self.x
|
|
|
|
@classmethod
|
|
def classmethod(cls):
|
|
return "clsmethod"
|
|
|
|
@staticmethod
|
|
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():
|
|
"""
|
|
The class is really using __slots__.
|
|
"""
|
|
non_slot_instance = C1(x=1, y="test")
|
|
slot_instance = C1Slots(x=1, y="test")
|
|
|
|
assert "__dict__" not in dir(slot_instance)
|
|
assert "__slots__" in dir(slot_instance)
|
|
|
|
assert "__dict__" in dir(non_slot_instance)
|
|
assert "__slots__" not in dir(non_slot_instance)
|
|
|
|
assert set(["x", "y"]) == set(slot_instance.__slots__)
|
|
|
|
if has_pympler:
|
|
assert asizeof(slot_instance) < asizeof(non_slot_instance)
|
|
|
|
non_slot_instance.t = "test"
|
|
with pytest.raises(AttributeError):
|
|
slot_instance.t = "test"
|
|
|
|
assert 1 == non_slot_instance.method()
|
|
assert 1 == slot_instance.method()
|
|
|
|
assert attr.fields(C1Slots) == attr.fields(C1)
|
|
assert attr.asdict(slot_instance) == attr.asdict(non_slot_instance)
|
|
|
|
|
|
def test_basic_attr_funcs():
|
|
"""
|
|
Comparison, `__eq__`, `__hash__`, `__repr__`, `attrs.asdict` work.
|
|
"""
|
|
a = C1Slots(x=1, y=2)
|
|
b = C1Slots(x=1, y=3)
|
|
a_ = C1Slots(x=1, y=2)
|
|
|
|
# Comparison.
|
|
assert b > a
|
|
|
|
assert a_ == a
|
|
|
|
# Hashing.
|
|
hash(b) # Just to assert it doesn't raise.
|
|
|
|
# Repr.
|
|
assert "C1Slots(x=1, y=2)" == repr(a)
|
|
|
|
assert {"x": 1, "y": 2} == attr.asdict(a)
|
|
|
|
|
|
def test_inheritance_from_nonslots():
|
|
"""
|
|
Inheritance from a non-slot class works.
|
|
|
|
Note that a slots class inheriting from an ordinary class loses most of the
|
|
benefits of slots classes, but it should still work.
|
|
"""
|
|
@attr.s(slots=True, hash=True)
|
|
class C2Slots(C1):
|
|
z = attr.ib()
|
|
|
|
c2 = C2Slots(x=1, y=2, z="test")
|
|
|
|
assert 1 == c2.x
|
|
assert 2 == c2.y
|
|
assert "test" == c2.z
|
|
|
|
c2.t = "test" # This will work, using the base class.
|
|
|
|
assert "test" == c2.t
|
|
|
|
assert 1 == c2.method()
|
|
assert "clsmethod" == c2.classmethod()
|
|
assert "staticmethod" == c2.staticmethod()
|
|
|
|
assert set(["z"]) == set(C2Slots.__slots__)
|
|
|
|
c3 = C2Slots(x=1, y=3, z="test")
|
|
|
|
assert c3 > c2
|
|
|
|
c2_ = C2Slots(x=1, y=2, z="test")
|
|
|
|
assert c2 == c2_
|
|
|
|
assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
|
|
|
|
hash(c2) # Just to assert it doesn't raise.
|
|
|
|
assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
|
|
|
|
|
|
def test_nonslots_these():
|
|
"""
|
|
Enhancing a non-slots class using 'these' works.
|
|
|
|
This will actually *replace* the class with another one, using slots.
|
|
"""
|
|
class SimpleOrdinaryClass(object):
|
|
def __init__(self, x, y, z):
|
|
self.x = x
|
|
self.y = y
|
|
self.z = z
|
|
|
|
def method(self):
|
|
return self.x
|
|
|
|
@classmethod
|
|
def classmethod(cls):
|
|
return "clsmethod"
|
|
|
|
@staticmethod
|
|
def staticmethod():
|
|
return "staticmethod"
|
|
|
|
C2Slots = attr.s(these={"x": attr.ib(), "y": attr.ib(), "z": attr.ib()},
|
|
init=False, slots=True, hash=True)(SimpleOrdinaryClass)
|
|
|
|
c2 = C2Slots(x=1, y=2, z="test")
|
|
assert 1 == c2.x
|
|
assert 2 == c2.y
|
|
assert "test" == c2.z
|
|
with pytest.raises(AttributeError):
|
|
c2.t = "test" # We have slots now.
|
|
|
|
assert 1 == c2.method()
|
|
assert "clsmethod" == c2.classmethod()
|
|
assert "staticmethod" == c2.staticmethod()
|
|
|
|
assert set(["x", "y", "z"]) == set(C2Slots.__slots__)
|
|
|
|
c3 = C2Slots(x=1, y=3, z="test")
|
|
assert c3 > c2
|
|
c2_ = C2Slots(x=1, y=2, z="test")
|
|
assert c2 == c2_
|
|
|
|
assert "SimpleOrdinaryClass(x=1, y=2, z='test')" == repr(c2)
|
|
|
|
hash(c2) # Just to assert it doesn't raise.
|
|
|
|
assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
|
|
|
|
|
|
def test_inheritance_from_slots():
|
|
"""
|
|
Inheriting from an attr slot class works.
|
|
"""
|
|
@attr.s(slots=True, hash=True)
|
|
class C2Slots(C1Slots):
|
|
z = attr.ib()
|
|
|
|
@attr.s(slots=True, hash=True)
|
|
class C2(C1):
|
|
z = attr.ib()
|
|
|
|
c2 = C2Slots(x=1, y=2, z="test")
|
|
assert 1 == c2.x
|
|
assert 2 == c2.y
|
|
assert "test" == c2.z
|
|
|
|
assert set(["z"]) == set(C2Slots.__slots__)
|
|
|
|
assert 1 == c2.method()
|
|
assert "clsmethod" == c2.classmethod()
|
|
assert "staticmethod" == c2.staticmethod()
|
|
|
|
with pytest.raises(AttributeError):
|
|
c2.t = "test"
|
|
|
|
non_slot_instance = C2(x=1, y=2, z="test")
|
|
if has_pympler:
|
|
assert asizeof(c2) < asizeof(non_slot_instance)
|
|
|
|
c3 = C2Slots(x=1, y=3, z="test")
|
|
assert c3 > c2
|
|
c2_ = C2Slots(x=1, y=2, z="test")
|
|
assert c2 == c2_
|
|
|
|
assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
|
|
|
|
hash(c2) # Just to assert it doesn't raise.
|
|
|
|
assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
|
|
|
|
|
|
def test_bare_inheritance_from_slots():
|
|
"""
|
|
Inheriting from a bare attr slot class works.
|
|
"""
|
|
@attr.s(init=False, cmp=False, hash=False, repr=False, slots=True)
|
|
class C1BareSlots(object):
|
|
x = attr.ib(validator=attr.validators.instance_of(int))
|
|
y = attr.ib()
|
|
|
|
def method(self):
|
|
return self.x
|
|
|
|
@classmethod
|
|
def classmethod(cls):
|
|
return "clsmethod"
|
|
|
|
@staticmethod
|
|
def staticmethod():
|
|
return "staticmethod"
|
|
|
|
@attr.s(init=False, cmp=False, hash=False, repr=False)
|
|
class C1Bare(object):
|
|
x = attr.ib(validator=attr.validators.instance_of(int))
|
|
y = attr.ib()
|
|
|
|
def method(self):
|
|
return self.x
|
|
|
|
@classmethod
|
|
def classmethod(cls):
|
|
return "clsmethod"
|
|
|
|
@staticmethod
|
|
def staticmethod():
|
|
return "staticmethod"
|
|
|
|
@attr.s(slots=True, hash=True)
|
|
class C2Slots(C1BareSlots):
|
|
z = attr.ib()
|
|
|
|
@attr.s(slots=True, hash=True)
|
|
class C2(C1Bare):
|
|
z = attr.ib()
|
|
|
|
c2 = C2Slots(x=1, y=2, z="test")
|
|
assert 1 == c2.x
|
|
assert 2 == c2.y
|
|
assert "test" == c2.z
|
|
|
|
assert 1 == c2.method()
|
|
assert "clsmethod" == c2.classmethod()
|
|
assert "staticmethod" == c2.staticmethod()
|
|
|
|
with pytest.raises(AttributeError):
|
|
c2.t = "test"
|
|
|
|
non_slot_instance = C2(x=1, y=2, z="test")
|
|
if has_pympler:
|
|
assert asizeof(c2) < asizeof(non_slot_instance)
|
|
|
|
c3 = C2Slots(x=1, y=3, z="test")
|
|
assert c3 > c2
|
|
c2_ = C2Slots(x=1, y=2, z="test")
|
|
assert c2 == c2_
|
|
|
|
assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
|
|
|
|
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.")
|
|
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
|