From dfd155bec89bc65c51fe112c88f3765f938b3684 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Thu, 6 Apr 2023 22:32:56 -0400 Subject: [PATCH 1/2] Fix attrs.evolve on bound TypeVar --- mypy/plugins/attrs.py | 22 ++++++----- test-data/unit/check-attr.test | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index f59eb2e36e4c..acc4fbb1e3b5 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -929,13 +929,10 @@ def add_method( add_method(self.ctx, method_name, args, ret_type, self_type, tvd) -def _get_attrs_init_type(typ: Type) -> CallableType | None: +def _get_attrs_init_type(typ: Instance) -> CallableType | None: """ If `typ` refers to an attrs class, gets the type of its initializer method. """ - typ = get_proper_type(typ) - if not isinstance(typ, Instance): - return None magic_attr = typ.type.get(MAGIC_ATTR_NAME) if magic_attr is None or not magic_attr.plugin_generated: return None @@ -967,16 +964,23 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl inst_type = get_proper_type(inst_type) if isinstance(inst_type, AnyType): - return ctx.default_signature + return ctx.default_signature # evolve(Any, ....) -> Any inst_type_str = format_type_bare(inst_type) - - attrs_init_type = _get_attrs_init_type(inst_type) - if not attrs_init_type: + attrs_type = get_proper_type( + inst_type.upper_bound if isinstance(inst_type, TypeVarType) else inst_type + ) + attrs_init_type = None + if isinstance(attrs_type, Instance): + attrs_init_type = _get_attrs_init_type(attrs_type) + if attrs_init_type is None: ctx.api.fail( - f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class', + f'Argument 1 to "evolve" has a variable type "{inst_type_str}" not bound to an attrs class' + if isinstance(inst_type, TypeVarType) + else f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class', ctx.context, ) return ctx.default_signature + assert isinstance(attrs_type, Instance) # AttrClass.__init__ has the following signature (or similar, if having kw-only & defaults): # def __init__(self, attr1: Type1, attr2: Type2) -> None: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 3ca804943010..45c673b269c5 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1970,6 +1970,75 @@ reveal_type(ret) # N: Revealed type is "Any" [typing fixtures/typing-medium.pyi] +[case testEvolveTypeVarBound] +import attrs +from typing import TypeVar + +@attrs.define +class A: + x: int + +@attrs.define +class B(A): + pass + +TA = TypeVar('TA', bound=A) + +def f(t: TA) -> TA: + t2 = attrs.evolve(t, x=42) + reveal_type(t2) # N: Revealed type is "TA`-1" + t3 = attrs.evolve(t, x='42') # E: Argument "x" to "evolve" of "TA" has incompatible type "str"; expected "int" + return t2 + +f(A(x=42)) +f(B(x=42)) + +[builtins fixtures/attr.pyi] + +[case testEvolveTypeVarBoundNonAttrs] +import attrs +from typing import TypeVar + +TInt = TypeVar('TInt', bound=int) +TAny = TypeVar('TAny') +TNone = TypeVar('TNone', bound=None) + +def f(t: TInt) -> None: + _ = attrs.evolve(t, x=42) # E: Argument 1 to "evolve" has a variable type "TInt" not bound to an attrs class + +def g(t: TAny) -> None: + _ = attrs.evolve(t, x=42) # E: Argument 1 to "evolve" has a variable type "TAny" not bound to an attrs class + +def h(t: TNone) -> None: + _ = attrs.evolve(t, x=42) # E: Argument 1 to "evolve" has a variable type "TNone" not bound to an attrs class + +[builtins fixtures/attr.pyi] + +[case testEvolveTypeVarConstrained] +import attrs +from typing import TypeVar + +@attrs.define +class A: + x: int + +@attrs.define +class B: + x: str # conflicting with A.x + +T = TypeVar('T', A, B) + +def f(t: T) -> T: + t2 = attrs.evolve(t, x=42) # E: Argument "x" to "evolve" of "B" has incompatible type "int"; expected "str" + reveal_type(t2) # N: Revealed type is "__main__.A" # N: Revealed type is "__main__.B" + t2 = attrs.evolve(t, x='42') # E: Argument "x" to "evolve" of "A" has incompatible type "str"; expected "int" + return t2 + +f(A(x=42)) +f(B(x='42')) + +[builtins fixtures/attr.pyi] + [case testEvolveVariants] from typing import Any import attr From a3364f1dca8908e4f7ec1bef8b33340625584308 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Sun, 9 Apr 2023 00:03:02 -0400 Subject: [PATCH 2/2] remove extra assert --- mypy/plugins/attrs.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index acc4fbb1e3b5..4a43c2a16d52 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -62,6 +62,7 @@ LiteralType, NoneType, Overloaded, + ProperType, TupleType, Type, TypeOfAny, @@ -942,6 +943,14 @@ def _get_attrs_init_type(typ: Instance) -> CallableType | None: return init_method.type +def _get_attrs_cls_and_init(typ: ProperType) -> tuple[Instance | None, CallableType | None]: + if isinstance(typ, TypeVarType): + typ = get_proper_type(typ.upper_bound) + if not isinstance(typ, Instance): + return None, None + return typ, _get_attrs_init_type(typ) + + def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType: """ Generates a signature for the 'attr.evolve' function that's specific to the call site @@ -966,13 +975,9 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl if isinstance(inst_type, AnyType): return ctx.default_signature # evolve(Any, ....) -> Any inst_type_str = format_type_bare(inst_type) - attrs_type = get_proper_type( - inst_type.upper_bound if isinstance(inst_type, TypeVarType) else inst_type - ) - attrs_init_type = None - if isinstance(attrs_type, Instance): - attrs_init_type = _get_attrs_init_type(attrs_type) - if attrs_init_type is None: + + attrs_type, attrs_init_type = _get_attrs_cls_and_init(inst_type) + if attrs_type is None or attrs_init_type is None: ctx.api.fail( f'Argument 1 to "evolve" has a variable type "{inst_type_str}" not bound to an attrs class' if isinstance(inst_type, TypeVarType) @@ -980,7 +985,6 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl ctx.context, ) return ctx.default_signature - assert isinstance(attrs_type, Instance) # AttrClass.__init__ has the following signature (or similar, if having kw-only & defaults): # def __init__(self, attr1: Type1, attr2: Type2) -> None: