# 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_