495 lines
11 KiB
Python
495 lines
11 KiB
Python
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
Integration tests for next-generation APIs.
|
|
"""
|
|
|
|
import re
|
|
|
|
from contextlib import contextmanager
|
|
from functools import partial
|
|
|
|
import pytest
|
|
|
|
import attr as _attr # don't use it by accident
|
|
import attrs
|
|
|
|
from attr._compat import PY_3_11_PLUS
|
|
|
|
|
|
@attrs.define
|
|
class C:
|
|
x: str
|
|
y: int
|
|
|
|
|
|
class TestNextGen:
|
|
def test_simple(self):
|
|
"""
|
|
Instantiation works.
|
|
"""
|
|
C("1", 2)
|
|
|
|
def test_field_type(self):
|
|
"""
|
|
Make class with attrs.field and type parameter.
|
|
"""
|
|
classFields = {"testint": attrs.field(type=int)}
|
|
|
|
A = attrs.make_class("A", classFields)
|
|
|
|
assert int is attrs.fields(A).testint.type
|
|
|
|
def test_no_slots(self):
|
|
"""
|
|
slots can be deactivated.
|
|
"""
|
|
|
|
@attrs.define(slots=False)
|
|
class NoSlots:
|
|
x: int
|
|
|
|
ns = NoSlots(1)
|
|
|
|
assert {"x": 1} == ns.__dict__
|
|
|
|
def test_validates(self):
|
|
"""
|
|
Validators at __init__ and __setattr__ work.
|
|
"""
|
|
|
|
@attrs.define
|
|
class Validated:
|
|
x: int = attrs.field(validator=attrs.validators.instance_of(int))
|
|
|
|
v = Validated(1)
|
|
|
|
with pytest.raises(TypeError):
|
|
Validated(None)
|
|
|
|
with pytest.raises(TypeError):
|
|
v.x = "1"
|
|
|
|
def test_no_order(self):
|
|
"""
|
|
Order is off by default but can be added.
|
|
"""
|
|
with pytest.raises(TypeError):
|
|
C("1", 2) < C("2", 3)
|
|
|
|
@attrs.define(order=True)
|
|
class Ordered:
|
|
x: int
|
|
|
|
assert Ordered(1) < Ordered(2)
|
|
|
|
def test_override_auto_attribs_true(self):
|
|
"""
|
|
Don't guess if auto_attrib is set explicitly.
|
|
|
|
Having an unannotated attrs.ib/attrs.field fails.
|
|
"""
|
|
with pytest.raises(attrs.exceptions.UnannotatedAttributeError):
|
|
|
|
@attrs.define(auto_attribs=True)
|
|
class ThisFails:
|
|
x = attrs.field()
|
|
y: int
|
|
|
|
def test_override_auto_attribs_false(self):
|
|
"""
|
|
Don't guess if auto_attrib is set explicitly.
|
|
|
|
Annotated fields that don't carry an attrs.ib are ignored.
|
|
"""
|
|
|
|
@attrs.define(auto_attribs=False)
|
|
class NoFields:
|
|
x: int
|
|
y: int
|
|
|
|
assert NoFields() == NoFields()
|
|
|
|
def test_auto_attribs_detect(self):
|
|
"""
|
|
define correctly detects if a class lacks type annotations.
|
|
"""
|
|
|
|
@attrs.define
|
|
class OldSchool:
|
|
x = attrs.field()
|
|
|
|
assert OldSchool(1) == OldSchool(1)
|
|
|
|
# Test with maybe_cls = None
|
|
@attrs.define()
|
|
class OldSchool2:
|
|
x = attrs.field()
|
|
|
|
assert OldSchool2(1) == OldSchool2(1)
|
|
|
|
def test_auto_attribs_detect_fields_and_annotations(self):
|
|
"""
|
|
define infers auto_attribs=True if fields have type annotations
|
|
"""
|
|
|
|
@attrs.define
|
|
class NewSchool:
|
|
x: int
|
|
y: list = attrs.field()
|
|
|
|
@y.validator
|
|
def _validate_y(self, attribute, value):
|
|
if value < 0:
|
|
raise ValueError("y must be positive")
|
|
|
|
assert NewSchool(1, 1) == NewSchool(1, 1)
|
|
with pytest.raises(ValueError):
|
|
NewSchool(1, -1)
|
|
assert list(attrs.fields_dict(NewSchool).keys()) == ["x", "y"]
|
|
|
|
def test_auto_attribs_partially_annotated(self):
|
|
"""
|
|
define infers auto_attribs=True if any type annotations are found
|
|
"""
|
|
|
|
@attrs.define
|
|
class NewSchool:
|
|
x: int
|
|
y: list
|
|
z = 10
|
|
|
|
# fields are defined for any annotated attributes
|
|
assert NewSchool(1, []) == NewSchool(1, [])
|
|
assert list(attrs.fields_dict(NewSchool).keys()) == ["x", "y"]
|
|
|
|
# while the unannotated attributes are left as class vars
|
|
assert NewSchool.z == 10
|
|
assert "z" in NewSchool.__dict__
|
|
|
|
def test_auto_attribs_detect_annotations(self):
|
|
"""
|
|
define correctly detects if a class has type annotations.
|
|
"""
|
|
|
|
@attrs.define
|
|
class NewSchool:
|
|
x: int
|
|
|
|
assert NewSchool(1) == NewSchool(1)
|
|
|
|
# Test with maybe_cls = None
|
|
@attrs.define()
|
|
class NewSchool2:
|
|
x: int
|
|
|
|
assert NewSchool2(1) == NewSchool2(1)
|
|
|
|
def test_exception(self):
|
|
"""
|
|
Exceptions are detected and correctly handled.
|
|
"""
|
|
|
|
@attrs.define
|
|
class E(Exception):
|
|
msg: str
|
|
other: int
|
|
|
|
with pytest.raises(E) as ei:
|
|
raise E("yolo", 42)
|
|
|
|
e = ei.value
|
|
|
|
assert ("yolo", 42) == e.args
|
|
assert "yolo" == e.msg
|
|
assert 42 == e.other
|
|
|
|
def test_frozen(self):
|
|
"""
|
|
attrs.frozen freezes classes.
|
|
"""
|
|
|
|
@attrs.frozen
|
|
class F:
|
|
x: str
|
|
|
|
f = F(1)
|
|
|
|
with pytest.raises(attrs.exceptions.FrozenInstanceError):
|
|
f.x = 2
|
|
|
|
def test_auto_detect_eq(self):
|
|
"""
|
|
auto_detect=True works for eq.
|
|
|
|
Regression test for #670.
|
|
"""
|
|
|
|
@attrs.define
|
|
class C:
|
|
def __eq__(self, o):
|
|
raise ValueError
|
|
|
|
with pytest.raises(ValueError):
|
|
C() == C()
|
|
|
|
def test_subclass_frozen(self):
|
|
"""
|
|
It's possible to subclass an `attrs.frozen` class and the frozen-ness
|
|
is inherited.
|
|
"""
|
|
|
|
@attrs.frozen
|
|
class A:
|
|
a: int
|
|
|
|
@attrs.frozen
|
|
class B(A):
|
|
b: int
|
|
|
|
@attrs.define(on_setattr=attrs.setters.NO_OP)
|
|
class C(B):
|
|
c: int
|
|
|
|
assert B(1, 2) == B(1, 2)
|
|
assert C(1, 2, 3) == C(1, 2, 3)
|
|
|
|
with pytest.raises(attrs.exceptions.FrozenInstanceError):
|
|
A(1).a = 1
|
|
|
|
with pytest.raises(attrs.exceptions.FrozenInstanceError):
|
|
B(1, 2).a = 1
|
|
|
|
with pytest.raises(attrs.exceptions.FrozenInstanceError):
|
|
B(1, 2).b = 2
|
|
|
|
with pytest.raises(attrs.exceptions.FrozenInstanceError):
|
|
C(1, 2, 3).c = 3
|
|
|
|
def test_catches_frozen_on_setattr(self):
|
|
"""
|
|
Passing frozen=True and on_setattr hooks is caught, even if the
|
|
immutability is inherited.
|
|
"""
|
|
|
|
@attrs.define(frozen=True)
|
|
class A:
|
|
pass
|
|
|
|
with pytest.raises(
|
|
ValueError, match="Frozen classes can't use on_setattr."
|
|
):
|
|
|
|
@attrs.define(frozen=True, on_setattr=attrs.setters.validate)
|
|
class B:
|
|
pass
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=re.escape(
|
|
"Frozen classes can't use on_setattr "
|
|
"(frozen-ness was inherited)."
|
|
),
|
|
):
|
|
|
|
@attrs.define(on_setattr=attrs.setters.validate)
|
|
class C(A):
|
|
pass
|
|
|
|
@pytest.mark.parametrize(
|
|
"decorator",
|
|
[
|
|
partial(_attr.s, frozen=True, slots=True, auto_exc=True),
|
|
attrs.frozen,
|
|
attrs.define,
|
|
attrs.mutable,
|
|
],
|
|
)
|
|
def test_discard_context(self, decorator):
|
|
"""
|
|
raise from None works.
|
|
|
|
Regression test for #703.
|
|
"""
|
|
|
|
@decorator
|
|
class MyException(Exception):
|
|
x: str = attrs.field()
|
|
|
|
with pytest.raises(MyException) as ei:
|
|
try:
|
|
raise ValueError
|
|
except ValueError:
|
|
raise MyException("foo") from None
|
|
|
|
assert "foo" == ei.value.x
|
|
assert ei.value.__cause__ is None
|
|
|
|
@pytest.mark.parametrize(
|
|
"decorator",
|
|
[
|
|
partial(_attr.s, frozen=True, slots=True, auto_exc=True),
|
|
attrs.frozen,
|
|
attrs.define,
|
|
attrs.mutable,
|
|
],
|
|
)
|
|
def test_setting_exception_mutable_attributes(self, decorator):
|
|
"""
|
|
contextlib.contextlib (re-)sets __traceback__ on raised exceptions.
|
|
|
|
Ensure that works, as well as if done explicitly
|
|
"""
|
|
|
|
@decorator
|
|
class MyException(Exception):
|
|
pass
|
|
|
|
@contextmanager
|
|
def do_nothing():
|
|
yield
|
|
|
|
with do_nothing(), pytest.raises(MyException) as ei:
|
|
raise MyException
|
|
|
|
assert isinstance(ei.value, MyException)
|
|
|
|
# this should not raise an exception either
|
|
ei.value.__traceback__ = ei.value.__traceback__
|
|
ei.value.__cause__ = ValueError("cause")
|
|
ei.value.__context__ = TypeError("context")
|
|
ei.value.__suppress_context__ = True
|
|
ei.value.__suppress_context__ = False
|
|
ei.value.__notes__ = []
|
|
del ei.value.__notes__
|
|
|
|
if PY_3_11_PLUS:
|
|
ei.value.add_note("note")
|
|
del ei.value.__notes__
|
|
|
|
def test_converts_and_validates_by_default(self):
|
|
"""
|
|
If no on_setattr is set, assume setters.convert, setters.validate.
|
|
"""
|
|
|
|
@attrs.define
|
|
class C:
|
|
x: int = attrs.field(converter=int)
|
|
|
|
@x.validator
|
|
def _v(self, _, value):
|
|
if value < 10:
|
|
raise ValueError("must be >=10")
|
|
|
|
inst = C(10)
|
|
|
|
# Converts
|
|
inst.x = "11"
|
|
|
|
assert 11 == inst.x
|
|
|
|
# Validates
|
|
with pytest.raises(ValueError, match="must be >=10"):
|
|
inst.x = "9"
|
|
|
|
def test_mro_ng(self):
|
|
"""
|
|
Attributes and methods are looked up the same way in NG by default.
|
|
|
|
See #428
|
|
"""
|
|
|
|
@attrs.define
|
|
class A:
|
|
x: int = 10
|
|
|
|
def xx(self):
|
|
return 10
|
|
|
|
@attrs.define
|
|
class B(A):
|
|
y: int = 20
|
|
|
|
@attrs.define
|
|
class C(A):
|
|
x: int = 50
|
|
|
|
def xx(self):
|
|
return 50
|
|
|
|
@attrs.define
|
|
class D(B, C):
|
|
pass
|
|
|
|
d = D()
|
|
|
|
assert d.x == d.xx()
|
|
|
|
|
|
class TestAsTuple:
|
|
def test_smoke(self):
|
|
"""
|
|
`attrs.astuple` only changes defaults, so we just call it and compare.
|
|
"""
|
|
inst = C("foo", 42)
|
|
|
|
assert attrs.astuple(inst) == _attr.astuple(inst)
|
|
|
|
|
|
class TestAsDict:
|
|
def test_smoke(self):
|
|
"""
|
|
`attrs.asdict` only changes defaults, so we just call it and compare.
|
|
"""
|
|
inst = C("foo", {(1,): 42})
|
|
|
|
assert attrs.asdict(inst) == _attr.asdict(
|
|
inst, retain_collection_types=True
|
|
)
|
|
|
|
|
|
class TestImports:
|
|
"""
|
|
Verify our re-imports and mirroring works.
|
|
"""
|
|
|
|
def test_converters(self):
|
|
"""
|
|
Importing from attrs.converters works.
|
|
"""
|
|
from attrs.converters import optional
|
|
|
|
assert optional is _attr.converters.optional
|
|
|
|
def test_exceptions(self):
|
|
"""
|
|
Importing from attrs.exceptions works.
|
|
"""
|
|
from attrs.exceptions import FrozenError
|
|
|
|
assert FrozenError is _attr.exceptions.FrozenError
|
|
|
|
def test_filters(self):
|
|
"""
|
|
Importing from attrs.filters works.
|
|
"""
|
|
from attrs.filters import include
|
|
|
|
assert include is _attr.filters.include
|
|
|
|
def test_setters(self):
|
|
"""
|
|
Importing from attrs.setters works.
|
|
"""
|
|
from attrs.setters import pipe
|
|
|
|
assert pipe is _attr.setters.pipe
|
|
|
|
def test_validators(self):
|
|
"""
|
|
Importing from attrs.validators works.
|
|
"""
|
|
from attrs.validators import and_
|
|
|
|
assert and_ is _attr.validators.and_
|