From 6a7317d89cc7c119ca63df0897462dfaa7094b0b Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Thu, 6 Apr 2023 22:32:56 -0400 Subject: [PATCH 1/8] Fix attrs.evolve with generics --- mypy/plugins/attrs.py | 6 ++++-- test-data/unit/check-attr.test | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index f59eb2e36e4c..b423f5835cfb 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -9,7 +9,7 @@ from mypy.applytype import apply_generic_arguments from mypy.checker import TypeChecker from mypy.errorcodes import LITERAL_REQ -from mypy.expandtype import expand_type +from mypy.expandtype import expand_type, expand_type_by_instance from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type from mypy.messages import format_type_bare from mypy.nodes import ( @@ -966,7 +966,7 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl # inst_type = get_proper_type(inst_type) - if isinstance(inst_type, AnyType): + if not isinstance(inst_type, Instance): return ctx.default_signature inst_type_str = format_type_bare(inst_type) @@ -978,6 +978,8 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl ) return ctx.default_signature + attrs_init_type = expand_type_by_instance(attrs_init_type, inst_type) + # AttrClass.__init__ has the following signature (or similar, if having kw-only & defaults): # def __init__(self, attr1: Type1, attr2: Type2) -> None: # We want to generate a signature for evolve that looks like this: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 3ca804943010..7c4e39b592e8 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1970,6 +1970,27 @@ reveal_type(ret) # N: Revealed type is "Any" [typing fixtures/typing-medium.pyi] +[case testEvolveGeneric] +import attrs +from typing import ClassVar, Generic, TypeVar + +T = TypeVar('T') + +@attrs.define +class A(Generic[T]): + x: T + + +a = A(x=42) +reveal_type(a) # N: Revealed type is "__main__.A[builtins.int]" +a2 = attrs.evolve(a, x=42) +reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]" +a2 = attrs.evolve(a, x='42') # E: Argument "x" to "evolve" of "A[int]" has incompatible type "str"; expected "int" +reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]" + +[builtins fixtures/attr.pyi] +[typing fixtures/typing-medium.pyi] + [case testEvolveVariants] from typing import Any import attr From edbcb217e6acdb1851a18d63a06f756fe28f2aa5 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Thu, 6 Apr 2023 23:23:33 -0400 Subject: [PATCH 2/8] fix "expected an attrs class" error check --- mypy/plugins/attrs.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index b423f5835cfb..98090cbeb168 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 @@ -966,12 +963,10 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl # inst_type = get_proper_type(inst_type) - if not isinstance(inst_type, Instance): - return ctx.default_signature inst_type_str = format_type_bare(inst_type) - - attrs_init_type = _get_attrs_init_type(inst_type) - if not attrs_init_type: + if not ( + isinstance(inst_type, Instance) and (attrs_init_type := _get_attrs_init_type(inst_type)) + ): ctx.api.fail( f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class', ctx.context, From 9bf473352560bb95e1c4c3ab66bd3178bb61be39 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 7 Apr 2023 22:13:47 -0400 Subject: [PATCH 3/8] unbreak 'testEvolveFromAny' --- mypy/plugins/attrs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 98090cbeb168..29883fd9c914 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -963,6 +963,8 @@ 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 # evolve(Any, ....) -> Any inst_type_str = format_type_bare(inst_type) if not ( isinstance(inst_type, Instance) and (attrs_init_type := _get_attrs_init_type(inst_type)) From 2ee391cac222150a4c5179aa703d852a4c24bb3e Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 7 Apr 2023 22:48:01 -0400 Subject: [PATCH 4/8] support for evolving type-vars --- mypy/plugins/attrs.py | 7 ++- test-data/unit/check-attr.test | 84 +++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 29883fd9c914..0f353fad62b7 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -965,9 +965,12 @@ 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 # evolve(Any, ....) -> Any + # We stringify it first, so that TypeVars maintain their name. inst_type_str = format_type_bare(inst_type) + upper_bound = inst_type.upper_bound if isinstance(inst_type, TypeVarType) else inst_type if not ( - isinstance(inst_type, Instance) and (attrs_init_type := _get_attrs_init_type(inst_type)) + isinstance(upper_bound, Instance) + and (attrs_init_type := _get_attrs_init_type(upper_bound)) ): ctx.api.fail( f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class', @@ -975,7 +978,7 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl ) return ctx.default_signature - attrs_init_type = expand_type_by_instance(attrs_init_type, inst_type) + attrs_init_type = expand_type_by_instance(attrs_init_type, upper_bound) # 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 7c4e39b592e8..61f0f14fd53a 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1972,7 +1972,7 @@ reveal_type(ret) # N: Revealed type is "Any" [case testEvolveGeneric] import attrs -from typing import ClassVar, Generic, TypeVar +from typing import Generic, TypeVar T = TypeVar('T') @@ -1991,6 +1991,88 @@ reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]" [builtins fixtures/attr.pyi] [typing fixtures/typing-medium.pyi] +[case testEvolveTypeVarWithAttrsUpperBound] +import attrs +from typing import TypeVar + + +@attrs.define +class A: + x: int + + +@attrs.define +class B(A): + pass + + +T = TypeVar('T', bound=A) + + +def f(t: T) -> T: + t2 = attrs.evolve(t, x=42) + reveal_type(t2) # N: Revealed type is "T`-1" + t3 = attrs.evolve(t, x='42') # E: Argument "x" to "evolve" of "T" has incompatible type "str"; expected "int" + return t2 + +f(A(x=42)) +f(B(x=42)) + +[builtins fixtures/attr.pyi] +[typing fixtures/typing-medium.pyi] + +[case testEvolveTypeVarWithAttrsGenericUpperBound] +import attrs +from typing import Generic, TypeVar + +Q = TypeVar('Q', bound=str) + +@attrs.define +class A(Generic[Q]): + x: Q + + +T = TypeVar('T', bound=A[str]) + + +def f(t: T) -> T: + t = attrs.evolve(t, x=42) # E: Argument "x" to "evolve" of "T" has incompatible type "int"; expected "str" + return t + +f(A(x='42')) + +[builtins fixtures/attr.pyi] +[typing fixtures/typing-medium.pyi] + +[case testEvolveTypeVarWithAttrsValueRestrictions] +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] +[typing fixtures/typing-medium.pyi] + [case testEvolveVariants] from typing import Any import attr From 8a1a54cd8c1db978a5fa710e0d3823c6d16df896 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 7 Apr 2023 23:13:39 -0400 Subject: [PATCH 5/8] remove walrus operator, more tests, more verbose messages --- mypy/plugins/attrs.py | 41 +++++++++++++++++++++++++--------- test-data/unit/check-attr.test | 16 +++++++++---- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 0f353fad62b7..23497a133ede 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -967,18 +967,37 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl return ctx.default_signature # evolve(Any, ....) -> Any # We stringify it first, so that TypeVars maintain their name. inst_type_str = format_type_bare(inst_type) - upper_bound = inst_type.upper_bound if isinstance(inst_type, TypeVarType) else inst_type - if not ( - isinstance(upper_bound, Instance) - and (attrs_init_type := _get_attrs_init_type(upper_bound)) - ): - ctx.api.fail( - f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class', - ctx.context, - ) - return ctx.default_signature + if isinstance(inst_type, TypeVarType): + attrs_type = inst_type.upper_bound + if not isinstance(attrs_type, Instance): + ctx.api.fail( + f'Argument 1 to "evolve" has a variable type "{inst_type_str}" with unexpected upper bounds', + ctx.context, + ) + return ctx.default_signature # TODO: is this possible? + attrs_init_type = _get_attrs_init_type(attrs_type) + if 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', + ctx.context, + ) + return ctx.default_signature + else: + attrs_type = inst_type + if not isinstance(attrs_type, Instance): + ctx.api.fail( + f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"', ctx.context + ) + return ctx.default_signature # TODO: is this possible? + 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', + ctx.context, + ) + return ctx.default_signature # TODO: is this possible? - attrs_init_type = expand_type_by_instance(attrs_init_type, upper_bound) + attrs_init_type = expand_type_by_instance(attrs_init_type, attrs_type) # 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 61f0f14fd53a..0ec5a61fdfe2 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -2006,18 +2006,26 @@ class B(A): pass -T = TypeVar('T', bound=A) +TA = TypeVar('TA', bound=A) +TInt = TypeVar('TInt', bound=int) +TAny = TypeVar('TAny') -def f(t: T) -> T: +def f(t: TA) -> TA: t2 = attrs.evolve(t, x=42) - reveal_type(t2) # N: Revealed type is "T`-1" - t3 = attrs.evolve(t, x='42') # E: Argument "x" to "evolve" of "T" has incompatible type "str"; expected "int" + 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)) +def g(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 h(t: TAny) -> None: + _ = attrs.evolve(t, x=42) # E: Argument 1 to "evolve" has a variable type "TAny" not bound to an attrs class + [builtins fixtures/attr.pyi] [typing fixtures/typing-medium.pyi] From cbd71ee8cd816e8a0d3d227768b7b7f54d801f94 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 7 Apr 2023 23:42:44 -0400 Subject: [PATCH 6/8] streamline --- mypy/plugins/attrs.py | 39 ++++++++++------------------------ test-data/unit/check-attr.test | 4 ++++ 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 23497a133ede..36073ec95281 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -967,35 +967,18 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl return ctx.default_signature # evolve(Any, ....) -> Any # We stringify it first, so that TypeVars maintain their name. inst_type_str = format_type_bare(inst_type) - if isinstance(inst_type, TypeVarType): - attrs_type = inst_type.upper_bound - if not isinstance(attrs_type, Instance): - ctx.api.fail( - f'Argument 1 to "evolve" has a variable type "{inst_type_str}" with unexpected upper bounds', - ctx.context, - ) - return ctx.default_signature # TODO: is this possible? + attrs_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 a variable type "{inst_type_str}" not bound to an attrs class', - ctx.context, - ) - return ctx.default_signature - else: - attrs_type = inst_type - if not isinstance(attrs_type, Instance): - ctx.api.fail( - f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"', ctx.context - ) - return ctx.default_signature # TODO: is this possible? - 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', - ctx.context, - ) - return ctx.default_signature # TODO: is this possible? + if 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) + else f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class', + ctx.context, + ) + return ctx.default_signature attrs_init_type = expand_type_by_instance(attrs_init_type, attrs_type) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 0ec5a61fdfe2..61517b6917f6 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -2009,6 +2009,7 @@ class B(A): TA = TypeVar('TA', bound=A) TInt = TypeVar('TInt', bound=int) TAny = TypeVar('TAny') +TNone = TypeVar('TNone', bound=None) def f(t: TA) -> TA: @@ -2026,6 +2027,9 @@ def g(t: TInt) -> None: def h(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 q(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] [typing fixtures/typing-medium.pyi] From e9f876ee1af25e3da000a63c1ca1569ded36c7f5 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 7 Apr 2023 23:57:20 -0400 Subject: [PATCH 7/8] fix self-check --- mypy/plugins/attrs.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 36073ec95281..ed9b80603ba0 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -965,9 +965,10 @@ 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 # evolve(Any, ....) -> Any - # We stringify it first, so that TypeVars maintain their name. inst_type_str = format_type_bare(inst_type) - attrs_type = inst_type.upper_bound if isinstance(inst_type, TypeVarType) else 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) @@ -979,6 +980,7 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl ctx.context, ) return ctx.default_signature + assert isinstance(attrs_type, Instance) attrs_init_type = expand_type_by_instance(attrs_init_type, attrs_type) From 469646bd421a223dfad025dd36818618121d7d09 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 12 Apr 2023 10:15:52 -0400 Subject: [PATCH 8/8] remove unneeded type assertion --- mypy/plugins/attrs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 2a5a0712d855..4fecbfdcbbfd 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -985,7 +985,6 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl ctx.context, ) return ctx.default_signature - assert isinstance(attrs_type, Instance) attrs_init_type = expand_type_by_instance(attrs_init_type, attrs_type)