769 lines
19 KiB
Python
769 lines
19 KiB
Python
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
End-to-end tests.
|
|
"""
|
|
|
|
import inspect
|
|
import pickle
|
|
|
|
from copy import deepcopy
|
|
|
|
import pytest
|
|
|
|
from hypothesis import given
|
|
from hypothesis.strategies import booleans
|
|
|
|
import attr
|
|
|
|
from attr._make import NOTHING, Attribute
|
|
from attr.exceptions import FrozenInstanceError
|
|
|
|
|
|
@attr.s
|
|
class C1:
|
|
x = attr.ib(validator=attr.validators.instance_of(int))
|
|
y = attr.ib()
|
|
|
|
|
|
@attr.s(slots=True)
|
|
class C1Slots:
|
|
x = attr.ib(validator=attr.validators.instance_of(int))
|
|
y = attr.ib()
|
|
|
|
|
|
foo = None
|
|
|
|
|
|
@attr.s()
|
|
class C2:
|
|
x = attr.ib(default=foo)
|
|
y = attr.ib(default=attr.Factory(list))
|
|
|
|
|
|
@attr.s(slots=True)
|
|
class C2Slots:
|
|
x = attr.ib(default=foo)
|
|
y = attr.ib(default=attr.Factory(list))
|
|
|
|
|
|
@attr.s
|
|
class Base:
|
|
x = attr.ib()
|
|
|
|
def meth(self):
|
|
return self.x
|
|
|
|
|
|
@attr.s(slots=True)
|
|
class BaseSlots:
|
|
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:
|
|
x = attr.ib()
|
|
|
|
|
|
@attr.s
|
|
class SubFrozen(Frozen):
|
|
y = attr.ib()
|
|
|
|
|
|
@attr.s(frozen=True, slots=False)
|
|
class FrozenNoSlots:
|
|
x = attr.ib()
|
|
|
|
|
|
class Meta(type):
|
|
pass
|
|
|
|
|
|
@attr.s
|
|
class WithMeta(metaclass=Meta):
|
|
pass
|
|
|
|
|
|
@attr.s(slots=True)
|
|
class WithMetaSlots(metaclass=Meta):
|
|
pass
|
|
|
|
|
|
FromMakeClass = attr.make_class("FromMakeClass", ["x"])
|
|
|
|
|
|
class TestFunctional:
|
|
"""
|
|
Functional tests.
|
|
"""
|
|
|
|
@pytest.mark.parametrize("cls", [C2, C2Slots])
|
|
def test_fields(self, cls):
|
|
"""
|
|
`attr.fields` works.
|
|
"""
|
|
assert (
|
|
Attribute(
|
|
name="x",
|
|
alias="x",
|
|
default=foo,
|
|
validator=None,
|
|
repr=True,
|
|
cmp=None,
|
|
eq=True,
|
|
order=True,
|
|
hash=None,
|
|
init=True,
|
|
inherited=False,
|
|
),
|
|
Attribute(
|
|
name="y",
|
|
alias="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 <class 'int'> (got '1' that is a <class 'str'>).",
|
|
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:
|
|
_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",
|
|
alias="a",
|
|
default=NOTHING,
|
|
validator=None,
|
|
repr=True,
|
|
cmp=None,
|
|
eq=True,
|
|
order=True,
|
|
hash=None,
|
|
init=True,
|
|
inherited=False,
|
|
),
|
|
Attribute(
|
|
name="b",
|
|
alias="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 f"Sub(x={obj}, y=2)" == repr(i)
|
|
else:
|
|
assert f"SubSlots(x={obj}, y=2)" == 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 f"Sub2(x={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.
|
|
"""
|
|
obj = cls(123, 456) if len(attr.fields(cls)) == 2 else 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 is type(cls)
|
|
|
|
def test_default_decorator(self):
|
|
"""
|
|
Default decorator sets the default and the respective method gets
|
|
called.
|
|
"""
|
|
|
|
@attr.s
|
|
class C:
|
|
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("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:
|
|
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. This is
|
|
incorrect behavior but we have to retain it for
|
|
backwards-compatibility.
|
|
"""
|
|
|
|
@attr.s(unsafe_hash=False)
|
|
class HashByIDBackwardCompat:
|
|
x = attr.ib()
|
|
|
|
assert hash(HashByIDBackwardCompat(1)) != hash(
|
|
HashByIDBackwardCompat(1)
|
|
)
|
|
|
|
@attr.s(unsafe_hash=False, eq=False)
|
|
class HashByID:
|
|
x = attr.ib()
|
|
|
|
assert hash(HashByID(1)) != hash(HashByID(1))
|
|
|
|
@attr.s(unsafe_hash=True)
|
|
class HashByValues:
|
|
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:
|
|
pass
|
|
|
|
@attr.s
|
|
class C:
|
|
x = attr.ib(default=Unhashable())
|
|
|
|
@attr.s
|
|
class D(C):
|
|
pass
|
|
|
|
def test_unsafe_hash_false_eq_false(self, slots):
|
|
"""
|
|
unsafe_hash=False and eq=False make a class hashable by ID.
|
|
"""
|
|
|
|
@attr.s(unsafe_hash=False, eq=False, slots=slots)
|
|
class C:
|
|
pass
|
|
|
|
assert hash(C()) != hash(C())
|
|
|
|
def test_hash_deprecated(self):
|
|
"""
|
|
Using the hash argument is deprecated.
|
|
"""
|
|
|
|
def test_eq_false(self, slots):
|
|
"""
|
|
eq=False makes a class hashable by ID.
|
|
"""
|
|
|
|
@attr.s(eq=False, slots=slots)
|
|
class C:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
def test_eq_only(self, slots, frozen):
|
|
"""
|
|
Classes with order=False cannot be ordered.
|
|
"""
|
|
|
|
@attr.s(eq=True, order=False, slots=slots, frozen=frozen)
|
|
class C:
|
|
x = attr.ib()
|
|
|
|
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
|
|
|
|
@pytest.mark.parametrize("cmp", [True, False])
|
|
def test_attrib_cmp_shortcut(self, slots, cmp):
|
|
"""
|
|
Setting cmp on `attr.ib`s sets both eq and order.
|
|
"""
|
|
|
|
@attr.s(slots=slots)
|
|
class C:
|
|
x = attr.ib(cmp=cmp)
|
|
|
|
assert cmp is attr.fields(C).x.eq
|
|
assert cmp is attr.fields(C).x.order
|
|
|
|
def test_no_setattr_if_validate_without_validators(self, slots):
|
|
"""
|
|
If a class has on_setattr=attr.setters.validate (former default in NG
|
|
APIs) but sets no validators, don't use the (slower) setattr in
|
|
__init__.
|
|
|
|
Regression test for #816.
|
|
"""
|
|
|
|
@attr.s(on_setattr=attr.setters.validate, slots=slots)
|
|
class C:
|
|
x = attr.ib()
|
|
|
|
@attr.s(on_setattr=attr.setters.validate, slots=slots)
|
|
class D(C):
|
|
y = attr.ib()
|
|
|
|
src = inspect.getsource(D.__init__)
|
|
|
|
assert "setattr" not in src
|
|
assert "self.x = x" in src
|
|
assert "self.y = y" in src
|
|
assert object.__setattr__ == D.__setattr__
|
|
|
|
def test_no_setattr_if_convert_without_converters(self, slots):
|
|
"""
|
|
If a class has on_setattr=attr.setters.convert but sets no validators,
|
|
don't use the (slower) setattr in __init__.
|
|
"""
|
|
|
|
@attr.s(on_setattr=attr.setters.convert, slots=slots)
|
|
class C:
|
|
x = attr.ib()
|
|
|
|
@attr.s(on_setattr=attr.setters.convert, slots=slots)
|
|
class D(C):
|
|
y = attr.ib()
|
|
|
|
src = inspect.getsource(D.__init__)
|
|
|
|
assert "setattr" not in src
|
|
assert "self.x = x" in src
|
|
assert "self.y = y" in src
|
|
assert object.__setattr__ == D.__setattr__
|
|
|
|
def test_no_setattr_with_ng_defaults(self, slots):
|
|
"""
|
|
If a class has the NG default on_setattr=[convert, validate] but sets
|
|
no validators or converters, don't use the (slower) setattr in
|
|
__init__.
|
|
"""
|
|
|
|
@attr.define(slots=slots)
|
|
class C:
|
|
x = attr.ib()
|
|
|
|
src = inspect.getsource(C.__init__)
|
|
|
|
assert "setattr" not in src
|
|
assert "self.x = x" in src
|
|
assert object.__setattr__ == C.__setattr__
|
|
|
|
@attr.define(slots=slots)
|
|
class D(C):
|
|
y = attr.ib()
|
|
|
|
src = inspect.getsource(D.__init__)
|
|
|
|
assert "setattr" not in src
|
|
assert "self.x = x" in src
|
|
assert "self.y = y" in src
|
|
assert object.__setattr__ == D.__setattr__
|
|
|
|
def test_on_setattr_detect_inherited_validators(self):
|
|
"""
|
|
_make_init detects the presence of a validator even if the field is
|
|
inherited.
|
|
"""
|
|
|
|
@attr.s(on_setattr=attr.setters.validate)
|
|
class C:
|
|
x = attr.ib(validator=42)
|
|
|
|
@attr.s(on_setattr=attr.setters.validate)
|
|
class D(C):
|
|
y = attr.ib()
|
|
|
|
src = inspect.getsource(D.__init__)
|
|
|
|
assert "_setattr = _cached_setattr_get(self)" in src
|
|
assert "_setattr('x', x)" in src
|
|
assert "_setattr('y', y)" in src
|
|
assert object.__setattr__ != D.__setattr__
|
|
|
|
def test_unsafe_hash(self, slots):
|
|
"""
|
|
attr.s(unsafe_hash=True) makes a class hashable.
|
|
"""
|
|
|
|
@attr.s(slots=slots, unsafe_hash=True)
|
|
class Hashable:
|
|
pass
|
|
|
|
assert hash(Hashable())
|
|
|
|
def test_init_subclass(self, slots):
|
|
"""
|
|
__attrs_init_subclass__ is called on subclasses.
|
|
"""
|
|
REGISTRY = []
|
|
|
|
@attr.s(slots=slots)
|
|
class Base:
|
|
@classmethod
|
|
def __attrs_init_subclass__(cls):
|
|
REGISTRY.append(cls)
|
|
|
|
@attr.s(slots=slots)
|
|
class ToRegister(Base):
|
|
pass
|
|
|
|
assert [ToRegister] == REGISTRY
|