Skip to content

Commit

Permalink
Fix attrs.evolve on bound TypeVar
Browse files Browse the repository at this point in the history
  • Loading branch information
ikonst committed Apr 9, 2023
1 parent e21ddbf commit dfd155b
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 9 deletions.
22 changes: 13 additions & 9 deletions mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
69 changes: 69 additions & 0 deletions test-data/unit/check-attr.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit dfd155b

Please sign in to comment.