Skip to content

Commit cc7b062

Browse files
authored
Fix attrs.evolve on bound TypeVar (#15022)
Fixes the error on the last line of this example: ```python @attrs.define class A: x: int T = TypeVar('T', bound=A) def f(t: T) -> None: _ = attrs.evolve(t, x=42) # E: Argument 1 to "evolve" has incompatible type "T"; expected an attrs class ``` Since `T` is bounded by `A`, we know it can be treated as `A`.
1 parent b43e0d3 commit cc7b062

File tree

2 files changed

+85
-8
lines changed

2 files changed

+85
-8
lines changed

mypy/plugins/attrs.py

+16-8
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
LiteralType,
6363
NoneType,
6464
Overloaded,
65+
ProperType,
6566
TupleType,
6667
Type,
6768
TypeOfAny,
@@ -929,13 +930,10 @@ def add_method(
929930
add_method(self.ctx, method_name, args, ret_type, self_type, tvd)
930931

931932

932-
def _get_attrs_init_type(typ: Type) -> CallableType | None:
933+
def _get_attrs_init_type(typ: Instance) -> CallableType | None:
933934
"""
934935
If `typ` refers to an attrs class, gets the type of its initializer method.
935936
"""
936-
typ = get_proper_type(typ)
937-
if not isinstance(typ, Instance):
938-
return None
939937
magic_attr = typ.type.get(MAGIC_ATTR_NAME)
940938
if magic_attr is None or not magic_attr.plugin_generated:
941939
return None
@@ -945,6 +943,14 @@ def _get_attrs_init_type(typ: Type) -> CallableType | None:
945943
return init_method.type
946944

947945

946+
def _get_attrs_cls_and_init(typ: ProperType) -> tuple[Instance | None, CallableType | None]:
947+
if isinstance(typ, TypeVarType):
948+
typ = get_proper_type(typ.upper_bound)
949+
if not isinstance(typ, Instance):
950+
return None, None
951+
return typ, _get_attrs_init_type(typ)
952+
953+
948954
def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType:
949955
"""
950956
Generates a signature for the 'attr.evolve' function that's specific to the call site
@@ -967,13 +973,15 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl
967973

968974
inst_type = get_proper_type(inst_type)
969975
if isinstance(inst_type, AnyType):
970-
return ctx.default_signature
976+
return ctx.default_signature # evolve(Any, ....) -> Any
971977
inst_type_str = format_type_bare(inst_type)
972978

973-
attrs_init_type = _get_attrs_init_type(inst_type)
974-
if not attrs_init_type:
979+
attrs_type, attrs_init_type = _get_attrs_cls_and_init(inst_type)
980+
if attrs_type is None or attrs_init_type is None:
975981
ctx.api.fail(
976-
f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class',
982+
f'Argument 1 to "evolve" has a variable type "{inst_type_str}" not bound to an attrs class'
983+
if isinstance(inst_type, TypeVarType)
984+
else f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class',
977985
ctx.context,
978986
)
979987
return ctx.default_signature

test-data/unit/check-attr.test

+69
Original file line numberDiff line numberDiff line change
@@ -1970,6 +1970,75 @@ reveal_type(ret) # N: Revealed type is "Any"
19701970

19711971
[typing fixtures/typing-medium.pyi]
19721972

1973+
[case testEvolveTypeVarBound]
1974+
import attrs
1975+
from typing import TypeVar
1976+
1977+
@attrs.define
1978+
class A:
1979+
x: int
1980+
1981+
@attrs.define
1982+
class B(A):
1983+
pass
1984+
1985+
TA = TypeVar('TA', bound=A)
1986+
1987+
def f(t: TA) -> TA:
1988+
t2 = attrs.evolve(t, x=42)
1989+
reveal_type(t2) # N: Revealed type is "TA`-1"
1990+
t3 = attrs.evolve(t, x='42') # E: Argument "x" to "evolve" of "TA" has incompatible type "str"; expected "int"
1991+
return t2
1992+
1993+
f(A(x=42))
1994+
f(B(x=42))
1995+
1996+
[builtins fixtures/attr.pyi]
1997+
1998+
[case testEvolveTypeVarBoundNonAttrs]
1999+
import attrs
2000+
from typing import TypeVar
2001+
2002+
TInt = TypeVar('TInt', bound=int)
2003+
TAny = TypeVar('TAny')
2004+
TNone = TypeVar('TNone', bound=None)
2005+
2006+
def f(t: TInt) -> None:
2007+
_ = attrs.evolve(t, x=42) # E: Argument 1 to "evolve" has a variable type "TInt" not bound to an attrs class
2008+
2009+
def g(t: TAny) -> None:
2010+
_ = attrs.evolve(t, x=42) # E: Argument 1 to "evolve" has a variable type "TAny" not bound to an attrs class
2011+
2012+
def h(t: TNone) -> None:
2013+
_ = attrs.evolve(t, x=42) # E: Argument 1 to "evolve" has a variable type "TNone" not bound to an attrs class
2014+
2015+
[builtins fixtures/attr.pyi]
2016+
2017+
[case testEvolveTypeVarConstrained]
2018+
import attrs
2019+
from typing import TypeVar
2020+
2021+
@attrs.define
2022+
class A:
2023+
x: int
2024+
2025+
@attrs.define
2026+
class B:
2027+
x: str # conflicting with A.x
2028+
2029+
T = TypeVar('T', A, B)
2030+
2031+
def f(t: T) -> T:
2032+
t2 = attrs.evolve(t, x=42) # E: Argument "x" to "evolve" of "B" has incompatible type "int"; expected "str"
2033+
reveal_type(t2) # N: Revealed type is "__main__.A" # N: Revealed type is "__main__.B"
2034+
t2 = attrs.evolve(t, x='42') # E: Argument "x" to "evolve" of "A" has incompatible type "str"; expected "int"
2035+
return t2
2036+
2037+
f(A(x=42))
2038+
f(B(x='42'))
2039+
2040+
[builtins fixtures/attr.pyi]
2041+
19732042
[case testEvolveVariants]
19742043
from typing import Any
19752044
import attr

0 commit comments

Comments
 (0)