attrs/tests/test_dark_magic.py

234 lines
6.2 KiB
Python

from __future__ import absolute_import, division, print_function
import pickle
import pytest
from hypothesis import given
from hypothesis.strategies import booleans
import attr
from attr._compat import TYPE
from attr._make import Attribute, NOTHING
from attr.exceptions import FrozenInstanceError
@attr.s
class C1(object):
x = attr.ib(validator=attr.validators.instance_of(int))
y = attr.ib()
@attr.s(slots=True)
class C1Slots(object):
x = attr.ib(validator=attr.validators.instance_of(int))
y = attr.ib()
foo = None
@attr.s()
class C2(object):
x = attr.ib(default=foo)
y = attr.ib(default=attr.Factory(list))
@attr.s(slots=True)
class C2Slots(object):
x = attr.ib(default=foo)
y = attr.ib(default=attr.Factory(list))
@attr.s
class Super(object):
x = attr.ib()
def meth(self):
return self.x
@attr.s(slots=True)
class SuperSlots(object):
x = attr.ib()
def meth(self):
return self.x
@attr.s
class Sub(Super):
y = attr.ib()
@attr.s(slots=True)
class SubSlots(SuperSlots):
y = attr.ib()
@attr.s(frozen=True, slots=True)
class Frozen(object):
x = attr.ib()
@attr.s
class SubFrozen(Frozen):
y = attr.ib()
@attr.s(frozen=True, slots=False)
class FrozenNoSlots(object):
x = attr.ib()
class TestDarkMagic(object):
"""
Integration tests.
"""
@pytest.mark.parametrize("cls", [C2, C2Slots])
def test_fields(self, cls):
"""
`attr.fields` works.
"""
assert (
Attribute(name="x", default=foo, _validator=None,
repr=True, cmp=True, hash=None, init=True),
Attribute(name="y", default=attr.Factory(list), _validator=None,
repr=True, cmp=True, hash=None, init=True),
) == attr.fields(cls)
@pytest.mark.parametrize("cls", [C1, C1Slots])
def test_asdict(self, cls):
"""
`attr.asdict` works.
"""
assert {
"x": 1,
"y": 2,
} == attr.asdict(cls(x=1, y=2))
@pytest.mark.parametrize("cls", [C1, C1Slots])
def test_validator(self, cls):
"""
`instance_of` raises `TypeError` on type mismatch.
"""
with pytest.raises(TypeError) as e:
cls("1", 2)
# Using C1 explicitly, since slot classes don't support this.
assert (
"'x' must be <{type} 'int'> (got '1' that is a <{type} "
"'str'>).".format(type=TYPE),
C1.x, int, "1",
) == e.value.args
@given(booleans())
def test_renaming(self, slots):
"""
Private members are renamed but only in `__init__`.
"""
@attr.s(slots=slots)
class C3(object):
_x = attr.ib()
assert "C3(_x=1)" == repr(C3(x=1))
@given(booleans(), booleans())
def test_programmatic(self, slots, frozen):
"""
`attr.make_class` works.
"""
PC = attr.make_class("PC", ["a", "b"], slots=slots, frozen=frozen)
assert (
Attribute(name="a", default=NOTHING, _validator=None,
repr=True, cmp=True, hash=None, init=True),
Attribute(name="b", default=NOTHING, _validator=None,
repr=True, cmp=True, hash=None, init=True),
) == attr.fields(PC)
@pytest.mark.parametrize("cls", [Sub, SubSlots])
def test_subclassing_with_extra_attrs(self, cls):
"""
Sub-classing (where the subclass has extra attrs) does what you'd hope
for.
"""
obj = object()
i = cls(x=obj, y=2)
assert i.x is i.meth() is obj
assert i.y == 2
if cls is Sub:
assert "Sub(x={obj}, y=2)".format(obj=obj) == repr(i)
else:
assert "SubSlots(x={obj}, y=2)".format(obj=obj) == repr(i)
@pytest.mark.parametrize("base", [Super, SuperSlots])
def test_subclass_without_extra_attrs(self, base):
"""
Sub-classing (where the subclass does not have extra attrs) still
behaves the same as a subclss with extra attrs.
"""
class Sub2(base):
pass
obj = object()
i = Sub2(x=obj)
assert i.x is i.meth() is obj
assert "Sub2(x={obj})".format(obj=obj) == repr(i)
@pytest.mark.parametrize("frozen_class", [
Frozen, # has slots=True
attr.make_class("FrozenToo", ["x"], slots=False, frozen=True),
])
def test_frozen_instance(self, frozen_class):
"""
Frozen instances can't be modified (easily).
"""
frozen = frozen_class(1)
with pytest.raises(FrozenInstanceError) as e:
frozen.x = 2
with pytest.raises(FrozenInstanceError) as e:
del frozen.x
assert e.value.args[0] == "can't set attribute"
assert 1 == frozen.x
@pytest.mark.parametrize("cls",
[C1, C1Slots, C2, C2Slots, Super, SuperSlots,
Sub, SubSlots, Frozen, FrozenNoSlots])
@pytest.mark.parametrize("protocol",
range(2, pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_attributes(self, cls, protocol):
"""
Pickling/un-pickling of Attribute instances works.
"""
for attribute in attr.fields(cls):
assert attribute == pickle.loads(pickle.dumps(attribute, protocol))
@pytest.mark.parametrize("cls",
[C1, C1Slots, C2, C2Slots, Super, SuperSlots,
Sub, SubSlots, Frozen, FrozenNoSlots])
@pytest.mark.parametrize("protocol",
range(2, pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_object(self, cls, protocol):
"""
Pickle object serialization works on all kinds of attrs classes.
"""
if len(attr.fields(cls)) == 2:
obj = cls(123, 456)
else:
obj = cls(123)
assert repr(obj) == repr(pickle.loads(pickle.dumps(obj, protocol)))
def test_subclassing_frozen_gives_frozen(self):
"""
The frozen-ness of classes is inherited. Subclasses of frozen classes
are also frozen and can be instantiated.
"""
i = SubFrozen("foo", "bar")
assert i.x == "foo"
assert i.y == "bar"