from __future__ import absolute_import, division, print_function
import copy
import inspect
import itertools
import sys
from operator import attrgetter
import pytest
from hypothesis import given
from hypothesis.strategies import booleans, integers, lists, sampled_from, text
import attr
from attr import _config
from attr._compat import PY2, ordered_dict
from attr._make import (
Attribute, Factory, _AndValidator, _Attributes, _ClassBuilder,
_CountingAttr, _transform_attrs, and_, fields, fields_dict, make_class,
validate
)
from attr.exceptions import DefaultAlreadySetError, NotAnAttrsClassError
from .strategies import (
gen_attr_names, list_of_attrs, simple_attrs, simple_attrs_with_metadata,
simple_attrs_without_metadata, simple_classes
)
from .utils import simple_attr
attrs_st = simple_attrs.map(lambda c: Attribute.from_counting_attr("name", c))
class TestCountingAttr(object):
def test_returns_Attr(self):
a = attr.ib()
assert isinstance(a, _CountingAttr)
def test_validators_lists_to_wrapped_tuples(self):
def v1(_, __):
pass
def v2(_, __):
pass
a = attr.ib(validator=[v1, v2])
assert _AndValidator((v1, v2,)) == a._validator
def test_validator_decorator_single(self):
a = attr.ib()
@a.validator
def v():
pass
assert v == a._validator
@pytest.mark.parametrize("wrap", [
lambda v: v,
lambda v: [v],
lambda v: and_(v)
])
def test_validator_decorator(self, wrap):
def v(_, __):
pass
a = attr.ib(validator=wrap(v))
@a.validator
def v2(self, _, __):
pass
assert _AndValidator((v, v2,)) == a._validator
def test_default_decorator_already_set(self):
a = attr.ib(default=42)
with pytest.raises(DefaultAlreadySetError):
@a.default
def f(self):
pass
def test_default_decorator_sets(self):
a = attr.ib()
@a.default
def f(self):
pass
assert Factory(f, True) == a._default
class TestAttribute(object):
def test_deprecated_convert_argument(self):
def conv(v):
return v
with pytest.warns(DeprecationWarning) as wi:
a = Attribute(
"a", True, True, True, True, True, True, convert=conv
)
w = wi.pop()
assert conv == a.converter
assert (
"The `convert` argument is deprecated in favor of `converter`. "
"It will be removed after 2019/01.",
) == w.message.args
assert __file__ == w.filename
def test_deprecated_convert_attribute(self):
def conv(v):
return v
a = simple_attr("a", converter=conv)
with pytest.warns(DeprecationWarning) as wi:
convert = a.convert
w = wi.pop()
assert conv is convert is a.converter
assert (
"The `convert` attribute is deprecated in favor of `converter`. "
"It will be removed after 2019/01.",
) == w.message.args
assert __file__ == w.filename
def test_convert_converter(self):
with pytest.raises(RuntimeError) as ei:
Attribute(
"a", True, True, True, True, True, True,
convert=lambda v: v, converter=lambda v: v,
)
assert (
"Can't pass both `convert` and `converter`. "
"Please use `converter` only.",
) == ei.value.args
def make_tc():
class TransformC(object):
z = attr.ib()
y = attr.ib()
x = attr.ib()
a = 42
return TransformC
class TestTransformAttrs(object):
def test_no_modifications(self):
C = make_tc()
_transform_attrs(C, None, False)
assert None is getattr(C, "__attrs_attrs__", None)
def test_normal(self):
C = make_tc()
attrs, _, _ = _transform_attrs(C, None, False)
assert ["z", "y", "x"] == [a.name for a in attrs]
def test_empty(self):
@attr.s
class C(object):
pass
assert _Attributes(((), [], {})) == _transform_attrs(C, None, False)
def test_transforms_to_attribute(self):
C = make_tc()
attrs, super_attrs, _ = _transform_attrs(C, None, False)
assert [] == super_attrs
assert 3 == len(attrs)
assert all(isinstance(a, Attribute) for a in attrs)
def test_conflicting_defaults(self):
class C(object):
x = attr.ib(default=None)
y = attr.ib()
with pytest.raises(ValueError) as e:
_transform_attrs(C, None, False)
assert (
"No mandatory attributes allowed after an attribute with a "
"default value or factory. Attribute in question: Attribute"
"(name='y', default=NOTHING, validator=None, repr=True, "
"cmp=True, hash=None, init=True, metadata=mappingproxy({}), "
"type=None, converter=None)",
) == e.value.args
def test_these(self):
class Base(object):
z = attr.ib()
class C(Base):
y = attr.ib()
attrs, super_attrs, _ = _transform_attrs(C, {"x": attr.ib()}, False)
assert [] == super_attrs
assert (
simple_attr("x"),
) == attrs
def test_these_leave_body(self):
@attr.s(init=False, these={"x": attr.ib()})
class C(object):
x = 5
assert 5 == C().x
assert "C(x=5)" == repr(C())
def test_these_ordered(self):
b = attr.ib(default=2)
a = attr.ib(default=1)
@attr.s(these=ordered_dict([("a", a), ("b", b)]))
class C(object):
pass
assert "C(a=1, b=2)" == repr(C())
def test_multiple_inheritance(self):
@attr.s
class A(object):
a1 = attr.ib(default="a1")
a2 = attr.ib(default="a2")
@attr.s
class B(A):
b1 = attr.ib(default="b1")
b2 = attr.ib(default="b2")
@attr.s
class C(B, A):
c1 = attr.ib(default="c1")
c2 = attr.ib(default="c2")
@attr.s
class D(A):
d1 = attr.ib(default="d1")
d2 = attr.ib(default="d2")
@attr.s
class E(C, D):
e1 = attr.ib(default="e1")
e2 = attr.ib(default="e2")
assert (
"E(a1='a1', a2='a2', b1='b1', b2='b2', c1='c1', c2='c2', d1='d1', "
"d2='d2', e1='e1', e2='e2')"
) == repr(E())
class TestAttributes(object):
@pytest.mark.skipif(not PY2, reason="No old-style classes in Py3")
def test_catches_old_style(self):
with pytest.raises(TypeError) as e:
@attr.s
class C:
pass
assert ("attrs only works with new-style classes.",) == e.value.args
def test_sets_attrs(self):
@attr.s
class C(object):
x = attr.ib()
assert "x" == C.__attrs_attrs__[0].name
assert all(isinstance(a, Attribute) for a in C.__attrs_attrs__)
def test_empty(self):
@attr.s
class C3(object):
pass
assert "C3()" == repr(C3())
assert C3() == C3()
@given(attr=attrs_st, attr_name=sampled_from(Attribute.__slots__))
def test_immutable(self, attr, attr_name):
with pytest.raises(AttributeError):
setattr(attr, attr_name, 1)
@pytest.mark.parametrize("method_name", [
"__repr__",
"__eq__",
"__hash__",
"__init__",
])
def test_adds_all_by_default(self, method_name):
sentinel = object()
class C(object):
x = attr.ib()
setattr(C, method_name, sentinel)
C = attr.s(C)
meth = getattr(C, method_name)
assert sentinel != meth
if method_name == "__hash__":
assert meth is None
@pytest.mark.parametrize("arg_name, method_name", [
("repr", "__repr__"),
("cmp", "__eq__"),
("hash", "__hash__"),
("init", "__init__"),
])
def test_respects_add_arguments(self, arg_name, method_name):
sentinel = object()
am_args = {
"repr": True,
"cmp": True,
"hash": True,
"init": True
}
am_args[arg_name] = False
class C(object):
x = attr.ib()
setattr(C, method_name, sentinel)
C = attr.s(**am_args)(C)
assert sentinel == getattr(C, method_name)
@pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.")
@given(slots_outer=booleans(), slots_inner=booleans())
def test_repr_qualname(self, slots_outer, slots_inner):
@attr.s(slots=slots_outer)
class C(object):
@attr.s(slots=slots_inner)
class D(object):
pass
assert "C.D()" == repr(C.D())
assert "GC.D()" == repr(GC.D())
@given(slots_outer=booleans(), slots_inner=booleans())
def test_repr_fake_qualname(self, slots_outer, slots_inner):
@attr.s(slots=slots_outer)
class C(object):
@attr.s(repr_ns="C", slots=slots_inner)
class D(object):
pass
assert "C.D()" == repr(C.D())
@pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.")
@given(slots_outer=booleans(), slots_inner=booleans())
def test_name_not_overridden(self, slots_outer, slots_inner):
@attr.s(slots=slots_outer)
class C(object):
@attr.s(slots=slots_inner)
class D(object):
pass
assert C.D.__name__ == "D"
assert C.D.__qualname__ == C.__qualname__ + ".D"
@given(with_validation=booleans())
def test_post_init(self, with_validation, monkeypatch):
monkeypatch.setattr(_config, "_run_validators", with_validation)
@attr.s
class C(object):
x = attr.ib()
y = attr.ib()
def __attrs_post_init__(self2):
self2.z = self2.x + self2.y
c = C(x=10, y=20)
assert 30 == getattr(c, 'z', None)
def test_types(self):
@attr.s
class C(object):
x = attr.ib(type=int)
y = attr.ib(type=str)
z = attr.ib()
assert int is fields(C).x.type
assert str is fields(C).y.type
assert None is fields(C).z.type
@pytest.mark.parametrize("slots", [True, False])
def test_clean_class(self, slots):
@attr.s(slots=slots)
class C(object):
x = attr.ib()
x = getattr(C, "x", None)
assert not isinstance(x, _CountingAttr)
def test_factory_sugar(self):
@attr.s
class C(object):
x = attr.ib(factory=list)
assert Factory(list) == attr.fields(C).x.default
def test_sugar_factory_mutex(self):
with pytest.raises(ValueError, match="mutually exclusive"):
@attr.s
class C(object):
x = attr.ib(factory=list, default=Factory(list))
def test_sugar_callable(self):
with pytest.raises(ValueError, match="must be a callable"):
@attr.s
class C(object):
x = attr.ib(factory=Factory(list))
@attr.s
class GC(object):
@attr.s
class D(object):
pass
class TestMakeClass(object):
@pytest.mark.parametrize("ls", [
list,
tuple
])
def test_simple(self, ls):
C1 = make_class("C1", ls(["a", "b"]))
@attr.s
class C2(object):
a = attr.ib()
b = attr.ib()
assert C1.__attrs_attrs__ == C2.__attrs_attrs__
def test_dict(self):
C1 = make_class("C1", {
"a": attr.ib(default=42),
"b": attr.ib(default=None),
})
@attr.s
class C2(object):
a = attr.ib(default=42)
b = attr.ib(default=None)
assert C1.__attrs_attrs__ == C2.__attrs_attrs__
def test_attr_args(self):
C = make_class("C", ["x"], repr=False)
assert repr(C(1)).startswith("<tests.test_make.C object at 0x")
def test_catches_wrong_attrs_type(self):
with pytest.raises(TypeError) as e:
make_class("C", object())
assert (
"attrs argument must be a dict or a list.",
) == e.value.args
def test_bases(self):
class D(object):
pass
cls = make_class("C", {})
assert cls.__mro__[-1] == object
cls = make_class("C", {}, bases=(D,))
assert D in cls.__mro__
assert isinstance(cls(), D)
@pytest.mark.parametrize("slots", [True, False])
def test_clean_class(self, slots):
C = make_class("C", ["x"], slots=slots)
x = getattr(C, "x", None)
assert not isinstance(x, _CountingAttr)
def test_missing_sys_getframe(self, monkeypatch):
monkeypatch.delattr(sys, '_getframe')
C = make_class("C", ["x"])
assert 1 == len(C.__attrs_attrs__)
def test_make_class_ordered(self):
b = attr.ib(default=2)
a = attr.ib(default=1)
C = attr.make_class("C", ordered_dict([("a", a), ("b", b)]))
assert "C(a=1, b=2)" == repr(C())
class TestFields(object):
def test_instance(self, C):
with pytest.raises(TypeError) as e:
fields(C(1, 2))
assert "Passed object must be a class." == e.value.args[0]
def test_handler_non_attrs_class(self, C):
with pytest.raises(NotAnAttrsClassError) as e:
fields(object)
assert (
"{o!r} is not an attrs-decorated class.".format(o=object)
) == e.value.args[0]
@given(simple_classes())
def test_fields(self, C):
assert all(isinstance(a, Attribute) for a in fields(C))
@given(simple_classes())
def test_fields_properties(self, C):
for attribute in fields(C):
assert getattr(fields(C), attribute.name) is attribute
class TestFieldsDict(object):
def test_instance(self, C):
with pytest.raises(TypeError) as e:
fields_dict(C(1, 2))
assert "Passed object must be a class." == e.value.args[0]
def test_handler_non_attrs_class(self, C):
with pytest.raises(NotAnAttrsClassError) as e:
fields_dict(object)
assert (
"{o!r} is not an attrs-decorated class.".format(o=object)
) == e.value.args[0]
@given(simple_classes())
def test_fields_dict(self, C):
d = fields_dict(C)
assert isinstance(d, ordered_dict)
assert list(fields(C)) == list(d.values())
assert [a.name for a in fields(C)] == [field_name for field_name in d]
class TestConverter(object):
def test_convert(self):
C = make_class("C", {
"x": attr.ib(converter=lambda v: v + 1),
"y": attr.ib(),
})
c = C(1, 2)
assert c.x == 2
assert c.y == 2
@given(integers(), booleans())
def test_convert_property(self, val, init):
C = make_class("C", {
"y": attr.ib(),
"x": attr.ib(init=init, default=val, converter=lambda v: v + 1),
})
c = C(2)
assert c.x == val + 1
assert c.y == 2
@given(integers(), booleans())
def test_convert_factory_property(self, val, init):
C = make_class("C", ordered_dict([
("y", attr.ib()),
("x", attr.ib(
init=init,
default=Factory(lambda: val),
converter=lambda v: v + 1
)),
]))
c = C(2)
assert c.x == val + 1
assert c.y == 2
def test_factory_takes_self(self):
C = make_class("C", {
"x": attr.ib(
default=Factory((lambda self: self), takes_self=True)
),
})
i = C()
assert i is i.x
def test_factory_hashable(self):
assert hash(Factory(None, False)) == hash(Factory(None, False))
def test_convert_before_validate(self):
def validator(inst, attr, val):
raise RuntimeError("foo")
C = make_class(
"C", {
"x": attr.ib(validator=validator, converter=lambda v: 1 / 0),
"y": attr.ib(),
})
with pytest.raises(ZeroDivisionError):
C(1, 2)
def test_frozen(self):
C = make_class("C", {
"x": attr.ib(converter=lambda v: int(v)),
}, frozen=True)
C("1")
def test_deprecated_convert(self):
def conv(v):
return v
with pytest.warns(DeprecationWarning) as wi:
@attr.s
class C(object):
x = attr.ib(convert=conv)
convert = fields(C).x.convert
assert 2 == len(wi.list)
w = wi.pop()
assert conv == fields(C).x.converter == convert
assert (
"The `convert` argument is deprecated in favor of `converter`. "
"It will be removed after 2019/01.",
) == w.message.args
assert __file__ == w.filename
def test_convert_converter(self):
with pytest.raises(RuntimeError) as ei:
@attr.s
class C(object):
x = attr.ib(convert=lambda v: v, converter=lambda v: v)
assert (
"Can't pass both `convert` and `converter`. "
"Please use `converter` only.",
) == ei.value.args
class TestValidate(object):
def test_success(self):
C = make_class("C", {
"x": attr.ib(validator=lambda *a: None),
"y": attr.ib()
})
validate(C(1, 2))
def test_propagates(self):
def raiser(_, __, value):
if value == 42:
raise FloatingPointError
C = make_class("C", {"x": attr.ib(validator=raiser)})
i = C(1)
i.x = 42
with pytest.raises(FloatingPointError):
validate(i)
def test_run_validators(self):
_config._run_validators = False
obj = object()
def raiser(_, __, ___):
raise Exception(obj)
C = make_class("C", {"x": attr.ib(validator=raiser)})
c = C(1)
validate(c)
assert 1 == c.x
_config._run_validators = True
with pytest.raises(Exception):
validate(c)
with pytest.raises(Exception) as e:
C(1)
assert (obj,) == e.value.args
def test_multiple_validators(self):
def v1(_, __, value):
if value == 23:
raise TypeError("omg")
def v2(_, __, value):
if value == 42:
raise ValueError("omg")
C = make_class("C", {"x": attr.ib(validator=[v1, v2])})
validate(C(1))
with pytest.raises(TypeError) as e:
C(23)
assert "omg" == e.value.args[0]
with pytest.raises(ValueError) as e:
C(42)
assert "omg" == e.value.args[0]
def test_multiple_empty(self):
C1 = make_class("C", {"x": attr.ib(validator=[])})
C2 = make_class("C", {"x": attr.ib(validator=None)})
assert inspect.getsource(C1.__init__) == inspect.getsource(C2.__init__)
sorted_lists_of_attrs = list_of_attrs.map(
lambda l: sorted(l, key=attrgetter("counter")))
class TestMetadata(object):
@given(sorted_lists_of_attrs)
def test_metadata_present(self, list_of_attrs):
C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs)))
for hyp_attr, class_attr in zip(list_of_attrs, fields(C)):
if hyp_attr.metadata is None:
assert class_attr.metadata is not None
assert len(class_attr.metadata) == 0
else:
assert hyp_attr.metadata == class_attr.metadata
for k in class_attr.metadata:
assert hyp_attr.metadata[k] == class_attr.metadata[k]
assert (hyp_attr.metadata.get(k) ==
class_attr.metadata.get(k))
@given(simple_classes(), text())
def test_metadata_immutability(self, C, string):
for a in fields(C):
with pytest.raises(TypeError):
a.metadata[string] = string
with pytest.raises(AttributeError):
a.metadata.update({string: string})
with pytest.raises(AttributeError):
a.metadata.clear()
with pytest.raises(AttributeError):
a.metadata.setdefault(string, string)
for k in a.metadata:
with pytest.raises((TypeError, IndexError)):
del a.metadata[k]
with pytest.raises(AttributeError):
a.metadata.pop(k)
with pytest.raises(AttributeError):
a.metadata.popitem()
@given(lists(simple_attrs_without_metadata, min_size=2, max_size=5))
def test_empty_metadata_singleton(self, list_of_attrs):
C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs)))
for a in fields(C)[1:]:
assert a.metadata is fields(C)[0].metadata
@given(lists(simple_attrs_without_metadata, min_size=2, max_size=5))
def test_empty_countingattr_metadata_independent(self, list_of_attrs):
for x, y in itertools.combinations(list_of_attrs, 2):
assert x.metadata is not y.metadata
@given(lists(simple_attrs_with_metadata(), min_size=2, max_size=5))
def test_not_none_metadata(self, list_of_attrs):
C = make_class("C", dict(zip(gen_attr_names(), list_of_attrs)))
assert len(fields(C)) > 0
for cls_a, raw_a in zip(fields(C), list_of_attrs):
assert cls_a.metadata != {}
assert cls_a.metadata == raw_a.metadata
def test_metadata(self):
md = {}
a = attr.ib(metadata=md)
assert md is a.metadata
class TestClassBuilder(object):
def test_repr_str(self):
with pytest.raises(ValueError) as ei:
make_class("C", {}, repr=False, str=True)
assert (
"__str__ can only be generated if a __repr__ exists.",
) == ei.value.args
def test_repr(self):
class C(object):
pass
b = _ClassBuilder(C, None, True, True, False)
assert "<_ClassBuilder(cls=C)>" == repr(b)
def test_returns_self(self):
class C(object):
x = attr.ib()
b = _ClassBuilder(C, None, True, True, False)
cls = b.add_cmp().add_hash().add_init().add_repr("ns").add_str() \
.build_class()
assert "ns.C(x=1)" == repr(cls(1))
@pytest.mark.parametrize("meth_name", [
"__init__", "__hash__", "__repr__", "__str__",
"__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__",
])
def test_attaches_meta_dunders(self, meth_name):
@attr.s(hash=True, str=True)
class C(object):
def organic(self):
pass
meth = getattr(C, meth_name)
assert meth_name == meth.__name__
assert C.organic.__module__ == meth.__module__
if not PY2:
organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0]
assert organic_prefix + "." + meth_name == meth.__qualname__
def test_handles_missing_meta_on_class(self):
class C(object):
pass
b = _ClassBuilder(
C, these=None, slots=False, frozen=False, auto_attribs=False,
)
b._cls = {}
def fake_meth(self):
pass
fake_meth.__module__ = "42"
fake_meth.__qualname__ = "23"
rv = b._add_method_dunders(fake_meth)
assert "42" == rv.__module__ == fake_meth.__module__
assert "23" == rv.__qualname__ == fake_meth.__qualname__
def test_weakref_setstate(self):
@attr.s(slots=True)
class C(object):
__weakref__ = attr.ib(
init=False, hash=False, repr=False, cmp=False
)
assert C() == copy.deepcopy(C())