123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720 |
- from __future__ import absolute_import, division, print_function
- import sys
- import warnings
- import pytest
- from characteristic import (
- Attribute,
- NOTHING,
- _ensure_attributes,
- attributes,
- immutable,
- with_cmp,
- with_init,
- with_repr,
- )
- PY2 = sys.version_info[0] == 2
- warnings.simplefilter("always")
- class TestAttribute(object):
- def test_init_simple(self):
- """
- Instantiating with just the name initializes properly.
- """
- a = Attribute("foo")
- assert "foo" == a.name
- assert NOTHING is a._default
- def test_init_default_factory(self):
- """
- Instantiating with default_factory creates a proper descriptor for
- _default.
- """
- a = Attribute("foo", default_factory=list)
- assert list() == a._default
- def test_init_default_value(self):
- """
- Instantiating with default_value initializes default properly.
- """
- a = Attribute("foo", default_value="bar")
- assert "bar" == a._default
- def test_ambiguous_defaults(self):
- """
- Instantiating with both default_value and default_factory raises
- ValueError.
- """
- with pytest.raises(ValueError):
- Attribute(
- "foo",
- default_value="bar",
- default_factory=lambda: 42
- )
- def test_missing_attr(self):
- """
- Accessing inexistent attributes still raises an AttributeError.
- """
- a = Attribute("foo")
- with pytest.raises(AttributeError):
- a.bar
- def test_alias(self):
- """
- If an attribute with a leading _ is defined, the initializer keyword
- is stripped of it.
- """
- a = Attribute("_private")
- assert "private" == a._kw_name
- def test_non_alias(self):
- """
- The keyword name of a non-private
- """
- a = Attribute("public")
- assert "public" == a._kw_name
- def test_dunder(self):
- """
- Dunder gets all _ stripped.
- """
- a = Attribute("__very_private")
- assert "very_private" == a._kw_name
- def test_init_aliaser_none(self):
- """
- No aliasing if init_aliaser is None.
- """
- a = Attribute("_private", init_aliaser=None)
- assert a.name == a._kw_name
- def test_init_aliaser(self):
- """
- Any callable works for aliasing.
- """
- a = Attribute("a", init_aliaser=lambda _: "foo")
- assert "foo" == a._kw_name
- def test_repr(self):
- """
- repr returns the correct string.
- """
- a = Attribute(
- name="name",
- exclude_from_cmp=True,
- exclude_from_init=True,
- exclude_from_repr=True,
- exclude_from_immutable=True,
- default_value=42,
- instance_of=str,
- init_aliaser=None
- )
- assert (
- "<Attribute(name='name', exclude_from_cmp=True, "
- "exclude_from_init=True, exclude_from_repr=True, "
- "exclude_from_immutable=True, "
- "default_value=42, default_factory=None, instance_of=<{0} 'str'>,"
- " init_aliaser=None)>"
- ).format("type" if PY2 else "class") == repr(a)
- @with_cmp(["a", "b"])
- class CmpC(object):
- def __init__(self, a, b):
- self.a = a
- self.b = b
- class TestWithCmp(object):
- def test_equal(self):
- """
- Equal objects are detected as equal.
- """
- assert CmpC(1, 2) == CmpC(1, 2)
- assert not (CmpC(1, 2) != CmpC(1, 2))
- def test_unequal_same_class(self):
- """
- Unequal objects of correct type are detected as unequal.
- """
- assert CmpC(1, 2) != CmpC(2, 1)
- assert not (CmpC(1, 2) == CmpC(2, 1))
- def test_unequal_different_class(self):
- """
- Unequal objects of differnt type are detected even if their attributes
- match.
- """
- class NotCmpC(object):
- a = 1
- b = 2
- assert CmpC(1, 2) != NotCmpC()
- assert not (CmpC(1, 2) == NotCmpC())
- def test_lt(self):
- """
- __lt__ compares objects as tuples of attribute values.
- """
- for a, b in [
- ((1, 2), (2, 1)),
- ((1, 2), (1, 3)),
- (("a", "b"), ("b", "a")),
- ]:
- assert CmpC(*a) < CmpC(*b)
- def test_lt_unordable(self):
- """
- __lt__ returns NotImplemented if classes differ.
- """
- assert NotImplemented == (CmpC(1, 2).__lt__(42))
- def test_le(self):
- """
- __le__ compares objects as tuples of attribute values.
- """
- for a, b in [
- ((1, 2), (2, 1)),
- ((1, 2), (1, 3)),
- ((1, 1), (1, 1)),
- (("a", "b"), ("b", "a")),
- (("a", "b"), ("a", "b")),
- ]:
- assert CmpC(*a) <= CmpC(*b)
- def test_le_unordable(self):
- """
- __le__ returns NotImplemented if classes differ.
- """
- assert NotImplemented == (CmpC(1, 2).__le__(42))
- def test_gt(self):
- """
- __gt__ compares objects as tuples of attribute values.
- """
- for a, b in [
- ((2, 1), (1, 2)),
- ((1, 3), (1, 2)),
- (("b", "a"), ("a", "b")),
- ]:
- assert CmpC(*a) > CmpC(*b)
- def test_gt_unordable(self):
- """
- __gt__ returns NotImplemented if classes differ.
- """
- assert NotImplemented == (CmpC(1, 2).__gt__(42))
- def test_ge(self):
- """
- __ge__ compares objects as tuples of attribute values.
- """
- for a, b in [
- ((2, 1), (1, 2)),
- ((1, 3), (1, 2)),
- ((1, 1), (1, 1)),
- (("b", "a"), ("a", "b")),
- (("a", "b"), ("a", "b")),
- ]:
- assert CmpC(*a) >= CmpC(*b)
- def test_ge_unordable(self):
- """
- __ge__ returns NotImplemented if classes differ.
- """
- assert NotImplemented == (CmpC(1, 2).__ge__(42))
- def test_hash(self):
- """
- __hash__ returns different hashes for different values.
- """
- assert hash(CmpC(1, 2)) != hash(CmpC(1, 1))
- def test_Attribute_exclude_from_cmp(self):
- """
- Ignores attribute if exclude_from_cmp=True.
- """
- @with_cmp([Attribute("a", exclude_from_cmp=True), "b"])
- class C(object):
- def __init__(self, a, b):
- self.a = a
- self.b = b
- assert C(42, 1) == C(23, 1)
- @with_repr(["a", "b"])
- class ReprC(object):
- def __init__(self, a, b):
- self.a = a
- self.b = b
- class TestReprAttrs(object):
- def test_repr(self):
- """
- Test repr returns a sensible value.
- """
- assert "<ReprC(a=1, b=2)>" == repr(ReprC(1, 2))
- def test_Attribute_exclude_from_repr(self):
- """
- Ignores attribute if exclude_from_repr=True.
- """
- @with_repr([Attribute("a", exclude_from_repr=True), "b"])
- class C(object):
- def __init__(self, a, b):
- self.a = a
- self.b = b
- assert "<C(b=2)>" == repr(C(1, 2))
- @with_init([Attribute("a"), Attribute("b")])
- class InitC(object):
- def __init__(self):
- if self.a == self.b:
- raise ValueError
- class TestWithInit(object):
- def test_sets_attributes(self):
- """
- The attributes are initialized using the passed keywords.
- """
- obj = InitC(a=1, b=2)
- assert 1 == obj.a
- assert 2 == obj.b
- def test_custom_init(self):
- """
- The class initializer is called too.
- """
- with pytest.raises(ValueError):
- InitC(a=1, b=1)
- def test_passes_args(self):
- """
- All positional parameters are passed to the original initializer.
- """
- @with_init(["a"])
- class InitWithArg(object):
- def __init__(self, arg):
- self.arg = arg
- obj = InitWithArg(42, a=1)
- assert 42 == obj.arg
- assert 1 == obj.a
- def test_passes_remaining_kw(self):
- """
- Keyword arguments that aren't used for attributes are passed to the
- original initializer.
- """
- @with_init(["a"])
- class InitWithKWArg(object):
- def __init__(self, kw_arg=None):
- self.kw_arg = kw_arg
- obj = InitWithKWArg(a=1, kw_arg=42)
- assert 42 == obj.kw_arg
- assert 1 == obj.a
- def test_does_not_pass_attrs(self):
- """
- The attributes are removed from the keyword arguments before they are
- passed to the original initializer.
- """
- @with_init(["a"])
- class InitWithKWArgs(object):
- def __init__(self, **kw):
- assert "a" not in kw
- assert "b" in kw
- InitWithKWArgs(a=1, b=42)
- def test_defaults(self):
- """
- If defaults are passed, they are used as fallback.
- """
- @with_init(["a", "b"], defaults={"b": 2})
- class InitWithDefaults(object):
- pass
- obj = InitWithDefaults(a=1)
- assert 2 == obj.b
- def test_missing_arg(self):
- """
- Raises `ValueError` if a value isn't passed.
- """
- with pytest.raises(ValueError) as e:
- InitC(a=1)
- assert "Missing keyword value for 'b'." == e.value.args[0]
- def test_defaults_conflict(self):
- """
- Raises `ValueError` if both defaults and an Attribute are passed.
- """
- with pytest.raises(ValueError) as e:
- @with_init([Attribute("a")], defaults={"a": 42})
- class C(object):
- pass
- assert (
- "Mixing of the 'defaults' keyword argument and passing instances "
- "of Attribute for 'attrs' is prohibited. Please don't use "
- "'defaults' anymore, it has been deprecated in 14.0."
- == e.value.args[0]
- )
- def test_attribute(self):
- """
- String attributes are converted to Attributes and thus work.
- """
- @with_init(["a"])
- class C(object):
- pass
- o = C(a=1)
- assert 1 == o.a
- def test_default_factory(self):
- """
- The default factory is used for each instance of missing keyword
- argument.
- """
- @with_init([Attribute("a", default_factory=list)])
- class C(object):
- pass
- o1 = C()
- o2 = C()
- assert o1.a is not o2.a
- def test_optimizes(self):
- """
- Uses __original_setattr__ if possible.
- """
- @immutable(["a"])
- @with_init(["a"])
- class C(object):
- pass
- c = C(a=42)
- assert c.__original_setattr__ == c.__characteristic_setattr__
- def test_setattr(self):
- """
- Uses setattr by default.
- """
- @with_init(["a"])
- class C(object):
- pass
- c = C(a=42)
- assert c.__setattr__ == c.__characteristic_setattr__
- def test_underscores(self):
- """
- with_init takes keyword aliasing into account.
- """
- @with_init([Attribute("_a")])
- class C(object):
- pass
- c = C(a=1)
- assert 1 == c._a
- def test_plain_no_alias(self):
- """
- str-based attributes don't get aliased for backward-compatibility.
- """
- @with_init(["_a"])
- class C(object):
- pass
- c = C(_a=1)
- assert 1 == c._a
- def test_instance_of_fail(self):
- """
- Raise `TypeError` if an Attribute with an `instance_of` is is attempted
- to be set to a mismatched type.
- """
- @with_init([Attribute("a", instance_of=int)])
- class C(object):
- pass
- with pytest.raises(TypeError) as e:
- C(a="not an int!")
- assert (
- "Attribute 'a' must be an instance of 'int'."
- == e.value.args[0]
- )
- def test_instance_of_success(self):
- """
- Setting an attribute to a value that doesn't conflict with an
- `instance_of` declaration works.
- """
- @with_init([Attribute("a", instance_of=int)])
- class C(object):
- pass
- c = C(a=42)
- assert 42 == c.a
- def test_Attribute_exclude_from_init(self):
- """
- Ignores attribute if exclude_from_init=True.
- """
- @with_init([Attribute("a", exclude_from_init=True), "b"])
- class C(object):
- pass
- C(b=1)
- def test_deprecation_defaults(self):
- """
- Emits a DeprecationWarning if `defaults` is used.
- """
- with warnings.catch_warnings(record=True) as w:
- @with_init(["a"], defaults={"a": 42})
- class C(object):
- pass
- assert (
- '`defaults` has been deprecated in 14.0, please use the '
- '`Attribute` class instead.'
- ) == w[0].message.args[0]
- assert issubclass(w[0].category, DeprecationWarning)
- class TestAttributes(object):
- def test_leaves_init_alone(self):
- """
- If *apply_with_init* or *create_init* is `False`, leave __init__ alone.
- """
- @attributes(["a"], apply_with_init=False)
- class C(object):
- pass
- @attributes(["a"], create_init=False)
- class CDeprecated(object):
- pass
- obj1 = C()
- obj2 = CDeprecated()
- with pytest.raises(AttributeError):
- obj1.a
- with pytest.raises(AttributeError):
- obj2.a
- def test_wraps_init(self):
- """
- If *create_init* is `True`, build initializer.
- """
- @attributes(["a", "b"], apply_with_init=True)
- class C(object):
- pass
- obj = C(a=1, b=2)
- assert 1 == obj.a
- assert 2 == obj.b
- def test_immutable(self):
- """
- If *apply_immutable* is `True`, make class immutable.
- """
- @attributes(["a"], apply_immutable=True)
- class ImmuClass(object):
- pass
- obj = ImmuClass(a=42)
- with pytest.raises(AttributeError):
- obj.a = "23"
- def test_apply_with_cmp(self):
- """
- Don't add cmp methods if *apply_with_cmp* is `False`.
- """
- @attributes(["a"], apply_with_cmp=False)
- class C(object):
- pass
- obj = C(a=1)
- if PY2:
- assert None is getattr(obj, "__eq__", None)
- else:
- assert object.__eq__ == C.__eq__
- def test_apply_with_repr(self):
- """
- Don't add __repr__ if *apply_with_repr* is `False`.
- """
- @attributes(["a"], apply_with_repr=False)
- class C(object):
- pass
- assert repr(C(a=1)).startswith("<test_characteristic.")
- def test_optimizes(self):
- """
- Uses correct order such that with_init can us __original_setattr__.
- """
- @attributes(["a"], apply_immutable=True)
- class C(object):
- __slots__ = ["a"]
- c = C(a=42)
- assert c.__original_setattr__ == c.__characteristic_setattr__
- def test_private(self):
- """
- Integration test for name mangling/aliasing.
- """
- @attributes([Attribute("_a")])
- class C(object):
- pass
- c = C(a=42)
- assert 42 == c._a
- def test_private_no_alias(self):
- """
- Integration test for name mangling/aliasing.
- """
- @attributes([Attribute("_a", init_aliaser=None)])
- class C(object):
- pass
- c = C(_a=42)
- assert 42 == c._a
- def test_deprecation_create_init(self):
- """
- Emits a DeprecationWarning if `create_init` is used.
- """
- with warnings.catch_warnings(record=True) as w:
- @attributes(["a"], create_init=False)
- class C(object):
- pass
- assert (
- '`create_init` has been deprecated in 14.0, please use '
- '`apply_with_init`.'
- ) == w[0].message.args[0]
- assert issubclass(w[0].category, DeprecationWarning)
- def test_deprecation_defaults(self):
- """
- Emits a DeprecationWarning if `defaults` is used.
- """
- with warnings.catch_warnings(record=True) as w:
- @attributes(["a"], defaults={"a": 42})
- class C(object):
- pass
- assert (
- '`defaults` has been deprecated in 14.0, please use the '
- '`Attribute` class instead.'
- ) == w[0].message.args[0]
- assert issubclass(w[0].category, DeprecationWarning)
- class TestEnsureAttributes(object):
- def test_leaves_attribute_alone(self):
- """
- List items that are an Attribute stay an Attribute.
- """
- a = Attribute("a")
- assert a is _ensure_attributes([a], {})[0]
- def test_converts_rest(self):
- """
- Any other item will be transformed into an Attribute.
- """
- l = _ensure_attributes(["a"], {})
- assert isinstance(l[0], Attribute)
- assert "a" == l[0].name
- def test_defaults(self):
- """
- Legacy defaults are translated into default_value attributes.
- """
- l = _ensure_attributes(["a"], {"a": 42})
- assert 42 == l[0].default_value
- def test_defaults_Attribute(self):
- """
- Raises ValueError on defaults != {} and an Attribute within attrs.
- """
- with pytest.raises(ValueError):
- _ensure_attributes([Attribute("a")], defaults={"a": 42})
- class TestImmutable(object):
- def test_bare(self):
- """
- In an immutable class, setting an definition-time attribute raises an
- AttributeError.
- """
- @immutable(["foo"])
- class ImmuClass(object):
- foo = "bar"
- i = ImmuClass()
- with pytest.raises(AttributeError):
- i.foo = "not bar"
- def test_Attribute(self):
- """
- Mutation is caught if user passes an Attribute instance.
- """
- @immutable([Attribute("foo")])
- class ImmuClass(object):
- def __init__(self):
- self.foo = "bar"
- i = ImmuClass()
- with pytest.raises(AttributeError):
- i.foo = "not bar"
- def test_init(self):
- """
- Changes within __init__ are allowed.
- """
- @immutable(["foo"])
- class ImmuClass(object):
- def __init__(self):
- self.foo = "bar"
- i = ImmuClass()
- assert "bar" == i.foo
- def test_with_init(self):
- """
- Changes in with_init's initializer are allowed.
- """
- @immutable(["foo"])
- @with_init(["foo"])
- class ImmuClass(object):
- pass
- i = ImmuClass(foo="qux")
- assert "qux" == i.foo
- def test_Attribute_exclude_from_immutable(self):
- """
- Ignores attribute if exclude_from_immutable=True.
- """
- @immutable([Attribute("a", exclude_from_immutable=True), "b"])
- class C(object):
- def __init__(self, a, b):
- self.a = a
- self.b = b
- c = C(1, 2)
- c.a = 3
- with pytest.raises(AttributeError):
- c.b = 4
- def test_nothing():
- """
- ``NOTHING`` has a sensible repr.
- """
- assert "NOTHING" == repr(NOTHING)
|