attrs/tests/test_functional.py

694 lines
18 KiB
Python

"""
End-to-end tests.
"""
from __future__ import absolute_import, division, print_function
import pickle
from copy import deepcopy
import pytest
import six
from hypothesis import assume, given
from hypothesis.strategies import booleans
import attr
from attr._compat import PY2, TYPE
from attr._make import NOTHING, Attribute
from attr.exceptions import FrozenInstanceError
from .strategies import optional_bool
@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 Base(object):
x = attr.ib()
def meth(self):
return self.x
@attr.s(slots=True)
class BaseSlots(object):
x = attr.ib()
def meth(self):
return self.x
@attr.s
class Sub(Base):
y = attr.ib()
@attr.s(slots=True)
class SubSlots(BaseSlots):
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 Meta(type):
pass
@attr.s
@six.add_metaclass(Meta)
class WithMeta(object):
pass
@attr.s(slots=True)
@six.add_metaclass(Meta)
class WithMetaSlots(object):
pass
FromMakeClass = attr.make_class("FromMakeClass", ["x"])
class TestFunctional(object):
"""
Functional 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=None,
eq=True,
order=True,
hash=None,
init=True,
inherited=False,
),
Attribute(
name="y",
default=attr.Factory(list),
validator=None,
repr=True,
cmp=None,
eq=True,
order=True,
hash=None,
init=True,
inherited=False,
),
) == 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 slotted classes don't support this.
assert (
"'x' must be <{type} 'int'> (got '1' that is a <{type} "
"'str'>).".format(type=TYPE),
attr.fields(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=None,
eq=True,
order=True,
hash=None,
init=True,
inherited=False,
),
Attribute(
name="b",
default=NOTHING,
validator=None,
repr=True,
cmp=None,
eq=True,
order=True,
hash=None,
init=True,
inherited=False,
),
) == attr.fields(PC)
@pytest.mark.parametrize("cls", [Sub, SubSlots])
def test_subclassing_with_extra_attrs(self, cls):
"""
Subclassing (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", [Base, BaseSlots])
def test_subclass_without_extra_attrs(self, base):
"""
Subclassing (where the subclass does not have extra attrs) still
behaves the same as a subclass 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,
Base,
BaseSlots,
Sub,
SubSlots,
Frozen,
FrozenNoSlots,
FromMakeClass,
],
)
@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,
Base,
BaseSlots,
Sub,
SubSlots,
Frozen,
FrozenNoSlots,
FromMakeClass,
],
)
@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"
with pytest.raises(FrozenInstanceError):
i.x = "baz"
@pytest.mark.parametrize("cls", [WithMeta, WithMetaSlots])
def test_metaclass_preserved(self, cls):
"""
Metaclass data is preserved.
"""
assert Meta == type(cls)
def test_default_decorator(self):
"""
Default decorator sets the default and the respective method gets
called.
"""
@attr.s
class C(object):
x = attr.ib(default=1)
y = attr.ib()
@y.default
def compute(self):
return self.x + 1
assert C(1, 2) == C()
@pytest.mark.parametrize("slots", [True, False])
@pytest.mark.parametrize("frozen", [True, False])
@pytest.mark.parametrize("weakref_slot", [True, False])
def test_attrib_overwrite(self, slots, frozen, weakref_slot):
"""
Subclasses can overwrite attributes of their base class.
"""
@attr.s(slots=slots, frozen=frozen, weakref_slot=weakref_slot)
class SubOverwrite(Base):
x = attr.ib(default=attr.Factory(list))
assert SubOverwrite([]) == SubOverwrite()
def test_dict_patch_class(self):
"""
dict-classes are never replaced.
"""
class C(object):
x = attr.ib()
C_new = attr.s(C)
assert C_new is C
def test_hash_by_id(self):
"""
With dict classes, hashing by ID is active for hash=False even on
Python 3. This is incorrect behavior but we have to retain it for
backward compatibility.
"""
@attr.s(hash=False)
class HashByIDBackwardCompat(object):
x = attr.ib()
assert hash(HashByIDBackwardCompat(1)) != hash(
HashByIDBackwardCompat(1)
)
@attr.s(hash=False, eq=False)
class HashByID(object):
x = attr.ib()
assert hash(HashByID(1)) != hash(HashByID(1))
@attr.s(hash=True)
class HashByValues(object):
x = attr.ib()
assert hash(HashByValues(1)) == hash(HashByValues(1))
def test_handles_different_defaults(self):
"""
Unhashable defaults + subclassing values work.
"""
@attr.s
class Unhashable(object):
pass
@attr.s
class C(object):
x = attr.ib(default=Unhashable())
@attr.s
class D(C):
pass
@pytest.mark.parametrize("slots", [True, False])
def test_hash_false_eq_false(self, slots):
"""
hash=False and eq=False make a class hashable by ID.
"""
@attr.s(hash=False, eq=False, slots=slots)
class C(object):
pass
assert hash(C()) != hash(C())
@pytest.mark.parametrize("slots", [True, False])
def test_eq_false(self, slots):
"""
eq=False makes a class hashable by ID.
"""
@attr.s(eq=False, slots=slots)
class C(object):
pass
# Ensure both objects live long enough such that their ids/hashes
# can't be recycled. Thanks to Ask Hjorth Larsen for pointing that
# out.
c1 = C()
c2 = C()
assert hash(c1) != hash(c2)
def test_overwrite_base(self):
"""
Base classes can overwrite each other and the attributes are added
in the order they are defined.
"""
@attr.s
class C(object):
c = attr.ib(default=100)
x = attr.ib(default=1)
b = attr.ib(default=23)
@attr.s
class D(C):
a = attr.ib(default=42)
x = attr.ib(default=2)
d = attr.ib(default=3.14)
@attr.s
class E(D):
y = attr.ib(default=3)
z = attr.ib(default=4)
assert "E(c=100, b=23, a=42, x=2, d=3.14, y=3, z=4)" == repr(E())
@pytest.mark.parametrize("base_slots", [True, False])
@pytest.mark.parametrize("sub_slots", [True, False])
@pytest.mark.parametrize("base_frozen", [True, False])
@pytest.mark.parametrize("sub_frozen", [True, False])
@pytest.mark.parametrize("base_weakref_slot", [True, False])
@pytest.mark.parametrize("sub_weakref_slot", [True, False])
@pytest.mark.parametrize("base_converter", [True, False])
@pytest.mark.parametrize("sub_converter", [True, False])
def test_frozen_slots_combo(
self,
base_slots,
sub_slots,
base_frozen,
sub_frozen,
base_weakref_slot,
sub_weakref_slot,
base_converter,
sub_converter,
):
"""
A class with a single attribute, inheriting from another class
with a single attribute.
"""
@attr.s(
frozen=base_frozen,
slots=base_slots,
weakref_slot=base_weakref_slot,
)
class Base(object):
a = attr.ib(converter=int if base_converter else None)
@attr.s(
frozen=sub_frozen, slots=sub_slots, weakref_slot=sub_weakref_slot
)
class Sub(Base):
b = attr.ib(converter=int if sub_converter else None)
i = Sub("1", "2")
assert i.a == (1 if base_converter else "1")
assert i.b == (2 if sub_converter else "2")
if base_frozen or sub_frozen:
with pytest.raises(FrozenInstanceError):
i.a = "2"
with pytest.raises(FrozenInstanceError):
i.b = "3"
def test_tuple_class_aliasing(self):
"""
itemgetter and property are legal attribute names.
"""
@attr.s
class C(object):
property = attr.ib()
itemgetter = attr.ib()
x = attr.ib()
assert "property" == attr.fields(C).property.name
assert "itemgetter" == attr.fields(C).itemgetter.name
assert "x" == attr.fields(C).x.name
@pytest.mark.parametrize("slots", [True, False])
@pytest.mark.parametrize("frozen", [True, False])
def test_auto_exc(self, slots, frozen):
"""
Classes with auto_exc=True have a Exception-style __str__, compare and
hash by id, and store the fields additionally in self.args.
"""
@attr.s(auto_exc=True, slots=slots, frozen=frozen)
class FooError(Exception):
x = attr.ib()
y = attr.ib(init=False, default=42)
z = attr.ib(init=False)
a = attr.ib()
FooErrorMade = attr.make_class(
"FooErrorMade",
bases=(Exception,),
attrs={
"x": attr.ib(),
"y": attr.ib(init=False, default=42),
"z": attr.ib(init=False),
"a": attr.ib(),
},
auto_exc=True,
slots=slots,
frozen=frozen,
)
assert FooError(1, "foo") != FooError(1, "foo")
assert FooErrorMade(1, "foo") != FooErrorMade(1, "foo")
for cls in (FooError, FooErrorMade):
with pytest.raises(cls) as ei1:
raise cls(1, "foo")
with pytest.raises(cls) as ei2:
raise cls(1, "foo")
e1 = ei1.value
e2 = ei2.value
assert e1 is e1
assert e1 == e1
assert e2 == e2
assert e1 != e2
assert "(1, 'foo')" == str(e1) == str(e2)
assert (1, "foo") == e1.args == e2.args
hash(e1) == hash(e1)
hash(e2) == hash(e2)
if not frozen:
deepcopy(e1)
deepcopy(e2)
@pytest.mark.parametrize("slots", [True, False])
@pytest.mark.parametrize("frozen", [True, False])
def test_auto_exc_one_attrib(self, slots, frozen):
"""
Having one attribute works with auto_exc=True.
Easy to get wrong with tuple literals.
"""
@attr.s(auto_exc=True, slots=slots, frozen=frozen)
class FooError(Exception):
x = attr.ib()
FooError(1)
@pytest.mark.parametrize("slots", [True, False])
@pytest.mark.parametrize("frozen", [True, False])
def test_eq_only(self, slots, frozen):
"""
Classes with order=False cannot be ordered.
Python 3 throws a TypeError, in Python2 we have to check for the
absence.
"""
@attr.s(eq=True, order=False, slots=slots, frozen=frozen)
class C(object):
x = attr.ib()
if not PY2:
possible_errors = (
"unorderable types: C() < C()",
"'<' not supported between instances of 'C' and 'C'",
"unorderable types: C < C", # old PyPy 3
)
with pytest.raises(TypeError) as ei:
C(5) < C(6)
assert ei.value.args[0] in possible_errors
else:
i = C(42)
for m in ("lt", "le", "gt", "ge"):
assert None is getattr(i, "__%s__" % (m,), None)
@given(cmp=optional_bool, eq=optional_bool, order=optional_bool)
def test_cmp_deprecated_attribute(self, cmp, eq, order):
"""
Accessing Attribute.cmp raises a deprecation warning but returns True
if cmp is True, or eq and order are *both* effectively True.
"""
# These cases are invalid and raise a ValueError.
assume(cmp is None or (eq is None and order is None))
assume(not (eq is False and order is True))
if cmp is not None:
rv = cmp
elif eq is True or eq is None:
rv = order is None or order is True
elif cmp is None and eq is None and order is None:
rv = True
elif cmp is None or eq is None:
rv = False
else:
pytest.fail(
"Unexpected state: cmp=%r eq=%r order=%r" % (cmp, eq, order)
)
with pytest.deprecated_call() as dc:
@attr.s
class C(object):
x = attr.ib(cmp=cmp, eq=eq, order=order)
assert rv == attr.fields(C).x.cmp
if cmp is not None:
# Remove warning from creating the attribute if cmp is not None.
dc.pop()
(w,) = dc.list
assert (
"The usage of `cmp` is deprecated and will be removed on or after "
"2021-06-01. Please use `eq` and `order` instead."
== w.message.args[0]
)