attrs/tests/test_make.py

427 lines
11 KiB
Python
Raw Normal View History

2015-01-29 19:50:42 +00:00
"""
Tests for `attr._make`.
"""
2015-01-27 16:53:17 +00:00
from __future__ import absolute_import, division, print_function
import pytest
from . import simple_attr
from attr import _config
from attr._compat import PY3
2015-01-27 16:53:17 +00:00
from attr._make import (
Attribute,
NOTHING,
2015-01-27 16:53:17 +00:00
_CountingAttr,
_transform_attrs,
2015-01-30 07:57:33 +00:00
attr,
attributes,
fields,
2015-01-30 07:57:33 +00:00
make_class,
validate,
2015-01-27 16:53:17 +00:00
)
class TestCountingAttr(object):
2015-01-27 16:53:17 +00:00
"""
Tests for `attr`.
2015-01-27 16:53:17 +00:00
"""
def test_returns_Attr(self):
"""
2015-01-29 19:50:42 +00:00
Returns an instance of _CountingAttr.
2015-01-27 16:53:17 +00:00
"""
a = attr()
2015-01-27 16:53:17 +00:00
assert isinstance(a, _CountingAttr)
def make_tc():
class TransformC(object):
z = attr()
y = attr()
x = attr()
a = 42
return TransformC
class TestTransformAttrs(object):
2015-01-27 16:53:17 +00:00
"""
Tests for `_transform_attrs`.
2015-01-27 16:53:17 +00:00
"""
def test_normal(self):
"""
Transforms every `_CountingAttr` and leaves others (a) be.
2015-01-27 16:53:17 +00:00
"""
C = make_tc()
_transform_attrs(C, None)
assert ["z", "y", "x"] == [a.name for a in C.__attrs_attrs__]
2015-01-27 16:53:17 +00:00
def test_empty(self):
"""
No attributes works as expected.
2015-01-27 16:53:17 +00:00
"""
@attributes
2015-01-27 16:53:17 +00:00
class C(object):
pass
_transform_attrs(C, None)
assert [] == C.__attrs_attrs__
@pytest.mark.parametrize("attribute", [
"z",
"y",
"x",
])
def test_transforms_to_attribute(self, attribute):
"""
All `_CountingAttr`s are transformed into `Attribute`s.
"""
C = make_tc()
_transform_attrs(C, None)
assert isinstance(getattr(C, attribute), Attribute)
2015-01-27 16:53:17 +00:00
2015-01-29 19:50:42 +00:00
def test_conflicting_defaults(self):
"""
Raises `ValueError` if attributes with defaults are followed by
mandatory attributes.
"""
class C(object):
2015-01-29 21:32:41 +00:00
x = attr(default=None)
2015-01-29 19:50:42 +00:00
y = attr()
with pytest.raises(ValueError) as e:
_transform_attrs(C, None)
2015-01-29 19:50:42 +00:00
assert (
2015-02-07 20:03:17 +00:00
"No mandatory attributes allowed after an attribute with a "
2015-01-29 19:50:42 +00:00
"default value or factory. Attribute in question: Attribute"
2015-02-20 12:29:47 +00:00
"(name='y', default=NOTHING, validator=None, repr=True, "
"cmp=True, hash=True, init=True)",
2015-01-29 19:50:42 +00:00
) == e.value.args
def test_these(self):
"""
If these is passed, use it and ignore body.
"""
class C(object):
y = attr()
_transform_attrs(C, {"x": attr()})
assert [
simple_attr("x"),
] == C.__attrs_attrs__
assert isinstance(C.y, _CountingAttr)
2015-02-18 20:31:32 +00:00
def test_recurse(self):
"""
Collect attributes from all sub-classes.
"""
class A(object):
pass
class C(A):
x = attr()
_transform_attrs(C, None)
class D(C):
y = attr()
_transform_attrs(D, None)
assert [
simple_attr("x"),
simple_attr("y"),
] == D.__attrs_attrs__
2015-01-27 16:53:17 +00:00
2015-01-29 19:50:42 +00:00
class TestAttributes(object):
2015-01-27 16:53:17 +00:00
"""
Tests for the `attributes` class decorator.
2015-01-27 16:53:17 +00:00
"""
@pytest.mark.skipif(PY3, reason="No old-style classes in Py3")
def test_catches_old_style(self):
"""
Raises TypeError on old-style classes.
"""
with pytest.raises(TypeError) as e:
@attributes
class C:
pass
assert ("attrs only works with new-style classes.",) == e.value.args
2015-01-27 16:53:17 +00:00
def test_sets_attrs(self):
"""
Sets the `__attrs_attrs__` class attribute with a list of `Attribute`s.
"""
@attributes
2015-01-27 16:53:17 +00:00
class C(object):
x = attr()
2015-01-27 16:53:17 +00:00
assert "x" == C.__attrs_attrs__[0].name
assert all(isinstance(a, Attribute) for a in C.__attrs_attrs__)
def test_empty(self):
"""
No attributes, no problems.
"""
@attributes
2015-01-27 16:53:17 +00:00
class C3(object):
pass
2015-01-27 22:08:55 +00:00
assert "C3()" == repr(C3())
2015-01-27 16:53:17 +00:00
assert C3() == C3()
@pytest.mark.parametrize("method_name", [
"__repr__",
"__eq__",
"__hash__",
"__init__",
])
def test_adds_all_by_default(self, method_name):
"""
If no further arguments are supplied, all add_XXX functions are
applied.
"""
# Set the method name to a sentinel and check whether it has been
# overwritten afterwards.
sentinel = object()
class C1(object):
x = attr()
setattr(C1, method_name, sentinel)
C1 = attributes(C1)
class C2(object):
x = attr()
setattr(C2, method_name, sentinel)
C2 = attributes(C2)
assert sentinel != getattr(C1, method_name)
assert sentinel != getattr(C2, method_name)
@pytest.mark.parametrize("arg_name, method_name", [
2015-02-20 12:29:47 +00:00
("repr", "__repr__"),
("cmp", "__eq__"),
("hash", "__hash__"),
("init", "__init__"),
])
def test_respects_add_arguments(self, arg_name, method_name):
"""
If a certain `add_XXX` is `True`, XXX is not added to the class.
"""
# Set the method name to a sentinel and check whether it has been
# overwritten afterwards.
sentinel = object()
am_args = {
2015-02-20 12:29:47 +00:00
"repr": True,
"cmp": True,
"hash": True,
"init": True
}
2015-02-20 12:29:47 +00:00
am_args[arg_name] = False
class C(object):
x = attr()
setattr(C, method_name, sentinel)
C = attributes(**am_args)(C)
assert sentinel == getattr(C, method_name)
@pytest.mark.skipif(not PY3, reason="__qualname__ is PY3-only.")
def test_repr_qualname(self):
"""
On Python 3, the name in repr is the __qualname__.
"""
@attributes
class C(object):
@attributes
class D(object):
pass
assert "C.D()" == repr(C.D())
assert "GC.D()" == repr(GC.D())
def test_repr_fake_qualname(self):
"""
Setting repr_ns overrides a potentially guessed namespace.
"""
@attributes
class C(object):
@attributes(repr_ns="C")
class D(object):
pass
assert "C.D()" == repr(C.D())
@attributes
class GC(object):
@attributes
class D(object):
pass
class TestAttribute(object):
"""
Tests for `Attribute`.
"""
def test_missing_argument(self):
"""
Raises `TypeError` if an Argument is missing.
"""
with pytest.raises(TypeError) as e:
Attribute(default=NOTHING, validator=None)
assert ("Missing argument 'name'.",) == e.value.args
2015-01-30 07:57:33 +00:00
def test_too_many_arguments(self):
"""
Raises `TypeError` if extra arguments are passed.
"""
with pytest.raises(TypeError) as e:
Attribute(name="foo", default=NOTHING,
2015-02-20 12:29:47 +00:00
factory=NOTHING, validator=None,
repr=True, cmp=True, hash=True, init=True)
assert ("Too many arguments.",) == e.value.args
2015-01-30 07:57:33 +00:00
class TestMakeClass(object):
"""
Tests for `make_class`.
"""
@pytest.mark.parametrize("ls", [
list,
tuple
])
def test_simple(self, ls):
"""
Passing a list of strings creates attributes with default args.
"""
C1 = make_class("C1", ls(["a", "b"]))
@attributes
class C2(object):
a = attr()
b = attr()
assert C1.__attrs_attrs__ == C2.__attrs_attrs__
def test_dict(self):
"""
Passing a dict of name: _CountingAttr creates an equivalent class.
"""
C1 = make_class("C1", {"a": attr(default=42), "b": attr(default=None)})
@attributes
class C2(object):
a = attr(default=42)
b = attr(default=None)
assert C1.__attrs_attrs__ == C2.__attrs_attrs__
def test_attr_args(self):
"""
attributes_arguments are passed to attributes
"""
2015-02-20 12:29:47 +00:00
C = make_class("C", ["x"], repr=False)
2015-01-30 07:57:33 +00:00
assert repr(C(1)).startswith("<attr._make.C object at 0x")
def test_catches_wrong_attrs_type(self):
"""
Raise `TypeError` if an invalid type for attrs is passed.
"""
with pytest.raises(TypeError) as e:
make_class("C", object())
assert (
"attrs argument must be a dict or a list.",
) == e.value.args
class TestFields(object):
"""
Tests for `fields`.
"""
def test_instance(self, C):
"""
Raises `TypeError` on non-classes.
"""
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):
"""
Raises `ValueError` if passed a non-``attrs`` instance.
"""
with pytest.raises(ValueError) as e:
fields(object)
assert (
"{o!r} is not an attrs-decorated class.".format(o=object)
) == e.value.args[0]
def test_fields(self, C):
"""
Returns a list of `Attribute`a.
"""
assert all(isinstance(a, Attribute) for a in fields(C))
def test_copies(self, C):
"""
Returns a new list object with new `Attribute` objects.
"""
assert C.__attrs_attrs__ is not fields(C)
assert all(new == original and new is not original
for new, original
in zip(fields(C), C.__attrs_attrs__))
class TestValidate(object):
"""
Tests for `validate`.
"""
def test_success(self):
"""
If the validator suceeds, nothing gets raised.
"""
C = make_class("C", {"x": attr(validator=lambda *a: None),
"y": attr()})
validate(C(1, 2))
def test_propagates(self):
"""
The exception of the validator is handed through.
"""
def raiser(_, __, value):
if value == 42:
raise FloatingPointError
C = make_class("C", {"x": attr(validator=raiser)})
i = C(1)
i.x = 42
with pytest.raises(FloatingPointError):
validate(i)
def test_run_validators(self):
"""
Setting `_run_validators` to False prevents validators from running.
"""
_config._run_validators = False
obj = object()
def raiser(_, __, ___):
raise Exception(obj)
C = make_class("C", {"x": attr(validator=raiser)})
assert 1 == C(1).x
_config._run_validators = True
with pytest.raises(Exception) as e:
C(1)
assert (obj,) == e.value.args