Skip to content

Try to fix problems caused by in-place modification of typevar defaults #19382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions mypy/applytype.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ def apply_generic_arguments(
report_incompatible_typevar_value: Callable[[CallableType, Type, str, Context], None],
context: Context,
skip_unsatisfied: bool = False,
*,
prefer_defaults: bool = False,
) -> CallableType:
"""Apply generic type arguments to a callable type.

Expand All @@ -100,6 +102,9 @@ def apply_generic_arguments(

If `skip_unsatisfied` is True, then just skip the types that don't satisfy type variable
bound or constraints, instead of giving an error.

If `prefer_defaults` is True, we will use defaults of all variables that have one
instead of making the result generic in those.
"""
tvars = callable.variables
assert len(orig_types) <= len(tvars)
Expand Down Expand Up @@ -140,10 +145,6 @@ def apply_generic_arguments(
callable = expand_type(callable, id_to_type)
assert isinstance(callable, CallableType)
return callable.copy_modified(variables=[tv for tv in tvars if tv.id not in id_to_type])
else:
callable = callable.copy_modified(
arg_types=[expand_type(at, id_to_type) for at in callable.arg_types]
)

# Apply arguments to TypeGuard and TypeIs if any.
if callable.type_guard is not None:
Expand All @@ -167,12 +168,16 @@ def apply_generic_arguments(
remaining_tvars.append(tv)
continue
# TypeVarLike isn't in id_to_type mapping.
# Only expand the TypeVar default here.
typ = expand_type(tv, id_to_type)
assert isinstance(typ, TypeVarLikeType)
remaining_tvars.append(typ)
if prefer_defaults:
# We were asked to avoid polymorphic results as much as possible.
id_to_type[tv.id] = expand_type(tv.default, id_to_type)
else:
# Only expand the TypeVar default here.
typ = tv.copy_modified(id=tv.id, default=expand_type(tv.default, id_to_type))
remaining_tvars.append(typ)

return callable.copy_modified(
arg_types=[expand_type(at, id_to_type) for at in callable.arg_types],
ret_type=expand_type(callable.ret_type, id_to_type),
variables=remaining_tvars,
type_guard=type_guard,
Expand Down
11 changes: 9 additions & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3292,6 +3292,8 @@ def apply_generic_arguments(
types: Sequence[Type | None],
context: Context,
skip_unsatisfied: bool = False,
*,
prefer_defaults: bool = False,
) -> CallableType:
"""Simple wrapper around mypy.applytype.apply_generic_arguments."""
return applytype.apply_generic_arguments(
Expand All @@ -3300,6 +3302,7 @@ def apply_generic_arguments(
self.msg.incompatible_typevar_value,
context,
skip_unsatisfied=skip_unsatisfied,
prefer_defaults=prefer_defaults,
)

def check_any_type_call(self, args: list[Expression], callee: Type) -> tuple[Type, Type]:
Expand Down Expand Up @@ -5021,7 +5024,9 @@ def apply_type_arguments_to_callable(
min_arg_count, len(type_vars), len(args), ctx
)
return AnyType(TypeOfAny.from_error)
return self.apply_generic_arguments(tp, self.split_for_callable(tp, args, ctx), ctx)
return self.apply_generic_arguments(
tp, self.split_for_callable(tp, args, ctx), ctx, prefer_defaults=True
)
if isinstance(tp, Overloaded):
for it in tp.items:
if tp.is_type_obj():
Expand All @@ -5040,7 +5045,9 @@ def apply_type_arguments_to_callable(
return AnyType(TypeOfAny.from_error)
return Overloaded(
[
self.apply_generic_arguments(it, self.split_for_callable(it, args, ctx), ctx)
self.apply_generic_arguments(
it, self.split_for_callable(it, args, ctx), ctx, prefer_defaults=True
)
for it in tp.items
]
)
Expand Down
38 changes: 23 additions & 15 deletions mypy/tvar_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,39 @@
from mypy.types import (
ParamSpecFlavor,
ParamSpecType,
TrivialSyntheticTypeTranslator,
TypeAliasType,
TypeVarId,
TypeVarLikeType,
TypeVarTupleType,
TypeVarType,
)
from mypy.typetraverser import TypeTraverserVisitor


class TypeVarLikeNamespaceSetter(TypeTraverserVisitor):
class TypeVarLikeNamespaceSetter(TrivialSyntheticTypeTranslator):
"""Set namespace for all TypeVarLikeTypes types."""

def __init__(self, namespace: str) -> None:
super().__init__()
self.namespace = namespace

def visit_type_var(self, t: TypeVarType) -> None:
t.id.namespace = self.namespace
super().visit_type_var(t)
def visit_type_var(self, t: TypeVarType) -> TypeVarType:
return t.copy_modified(
id=TypeVarId(raw_id=t.id.raw_id, meta_level=t.id.meta_level, namespace=self.namespace)
)

def visit_param_spec(self, t: ParamSpecType) -> None:
t.id.namespace = self.namespace
return super().visit_param_spec(t)
def visit_param_spec(self, t: ParamSpecType) -> ParamSpecType:
return t.copy_modified(
id=TypeVarId(raw_id=t.id.raw_id, meta_level=t.id.meta_level, namespace=self.namespace)
)

def visit_type_var_tuple(self, t: TypeVarTupleType) -> None:
t.id.namespace = self.namespace
super().visit_type_var_tuple(t)
def visit_type_var_tuple(self, t: TypeVarTupleType) -> TypeVarTupleType:
return t.copy_modified(
id=TypeVarId(raw_id=t.id.raw_id, meta_level=t.id.meta_level, namespace=self.namespace)
)

def visit_type_alias_type(self, t: TypeAliasType, /) -> TypeAliasType:
return t


class TypeVarLikeScope:
Expand Down Expand Up @@ -106,7 +114,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
self.func_id -= 1
i = self.func_id
namespace = self.namespace
tvar_expr.default.accept(TypeVarLikeNamespaceSetter(namespace))
default = tvar_expr.default.accept(TypeVarLikeNamespaceSetter(namespace))

if isinstance(tvar_expr, TypeVarExpr):
tvar_def: TypeVarLikeType = TypeVarType(
Expand All @@ -115,7 +123,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
id=TypeVarId(i, namespace=namespace),
values=tvar_expr.values,
upper_bound=tvar_expr.upper_bound,
default=tvar_expr.default,
default=default,
variance=tvar_expr.variance,
line=tvar_expr.line,
column=tvar_expr.column,
Expand All @@ -127,7 +135,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
id=TypeVarId(i, namespace=namespace),
flavor=ParamSpecFlavor.BARE,
upper_bound=tvar_expr.upper_bound,
default=tvar_expr.default,
default=default,
line=tvar_expr.line,
column=tvar_expr.column,
)
Expand All @@ -138,7 +146,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
id=TypeVarId(i, namespace=namespace),
upper_bound=tvar_expr.upper_bound,
tuple_fallback=tvar_expr.tuple_fallback,
default=tvar_expr.default,
default=default,
line=tvar_expr.line,
column=tvar_expr.column,
)
Expand Down
103 changes: 100 additions & 3 deletions test-data/unit/check-typevar-defaults.test
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ T3 = TypeVar("T3", bytes, str, default=str)
P1 = ParamSpec("P1", default=[int, str])
Ts1 = TypeVarTuple("Ts1", default=Unpack[Tuple[int, str]])

def callback1(x: str) -> None: ...
def callback1(x: int) -> None: ...

def func_a1(x: Union[int, T1]) -> T1: ...
reveal_type(func_a1(2)) # N: Revealed type is "builtins.str"
Expand All @@ -175,15 +175,112 @@ reveal_type(func_a3(2)) # N: Revealed type is "builtins.str"
def func_a4(x: Union[int, T3]) -> T3: ...
reveal_type(func_a4(2)) # N: Revealed type is "builtins.str"

def func_a5(x: Union[int, Callable[[T1], None]]) -> Callable[[T1], None]: ...
reveal_type(func_a5(callback1)) # N: Revealed type is "def (builtins.int)"
reveal_type(func_a5(2)) # N: Revealed type is "def (builtins.str)"

def func_b1(x: Union[int, Callable[P1, None]]) -> Callable[P1, None]: ...
reveal_type(func_b1(callback1)) # N: Revealed type is "def (x: builtins.str)"
reveal_type(func_b1(callback1)) # N: Revealed type is "def (x: builtins.int)"
reveal_type(func_b1(2)) # N: Revealed type is "def (builtins.int, builtins.str)"

def func_c1(x: Union[int, Callable[[Unpack[Ts1]], None]]) -> Tuple[Unpack[Ts1]]: ...
# reveal_type(func_c1(callback1)) # Revealed type is "Tuple[str]" # TODO
# reveal_type(func_c1(callback1)) # Revealed type is "Tuple[int]" # TODO
reveal_type(func_c1(2)) # N: Revealed type is "tuple[builtins.int, builtins.str]"
[builtins fixtures/tuple.pyi]

[case testTypeVarDefaultsClassInit]
from typing import Generic, TypeVar, ParamSpec, List, Union, Callable, Tuple
from typing_extensions import TypeVarTuple, Unpack

T1 = TypeVar("T1", default=str)
T2 = TypeVar("T2", bound=str, default=str)
T3 = TypeVar("T3", bytes, str, default=str)
P1 = ParamSpec("P1", default=[int, str])
Ts1 = TypeVarTuple("Ts1", default=Unpack[Tuple[int, str]])

def callback1(x: int) -> None: ...

class A1(Generic[T1]):
def __init__(self, x: Union[int, T1]) -> None: ...

reveal_type(A1(2)) # N: Revealed type is "__main__.A1[builtins.str]"
reveal_type(A1(2.1)) # N: Revealed type is "__main__.A1[builtins.float]"

class A2(Generic[T2]):
def __init__(self, x: Union[int, T2]) -> None: ...
reveal_type(A2(2)) # N: Revealed type is "__main__.A2[builtins.str]"

class A3(Generic[T3]):
def __init__(self, x: Union[int, T3]) -> None: ...
reveal_type(A3(2)) # N: Revealed type is "__main__.A3[builtins.str]"

class A4(Generic[T2]):
def __init__(self, x: Union[int, Callable[[T1], None]]) -> None: ...
reveal_type(A4(callback1)) # N: Revealed type is "__main__.A4[builtins.str]"
reveal_type(A4(2)) # N: Revealed type is "__main__.A4[builtins.str]"

class B1(Generic[P1]):
def __init__(self, x: Union[int, Callable[P1, None]]) -> None: ...
reveal_type(B1(callback1)) # N: Revealed type is "__main__.B1[[x: builtins.int]]"
reveal_type(B1(2)) # N: Revealed type is "__main__.B1[[builtins.int, builtins.str]]"

class C1(Generic[Unpack[Ts1]]):
def __init__(self, x: Union[int, Callable[[Unpack[Ts1]], None]]) -> None: ...
reveal_type(C1(callback1)) # Revealed type is "Tuple[int]" # TODO # N: Revealed type is "__main__.C1[builtins.int]"
reveal_type(C1(2)) # N: Revealed type is "__main__.C1[builtins.int, builtins.str]"
[builtins fixtures/tuple.pyi]

[case testTypeVarDefaultsInTypeApplications]
from typing import Generic
from typing_extensions import TypeVar

T1 = TypeVar("T1")
T2 = TypeVar("T2", default=T1)

class MyClass(Generic[T1, T2]): ...

reveal_type(MyClass[int]) # N: Revealed type is "def () -> __main__.MyClass[builtins.int, builtins.int]"

# `T2` now defaults to `int` for `MyClass` from now on
reveal_type(MyClass[str]) # N: Revealed type is "def () -> __main__.MyClass[builtins.str, builtins.str]"
reveal_type(MyClass[bytes]) # N: Revealed type is "def () -> __main__.MyClass[builtins.bytes, builtins.bytes]"

c_int: type[MyClass[int]]
c_str: type[MyClass[str]]
a_str = MyClass[str]()

reveal_type(c_int) # N: Revealed type is "type[__main__.MyClass[builtins.int, builtins.int]]"
reveal_type(c_str) # N: Revealed type is "type[__main__.MyClass[builtins.str, builtins.str]]"
reveal_type(a_str) # N: Revealed type is "__main__.MyClass[builtins.str, builtins.str]"

class MyClass2(Generic[T1, T2]):
def __init__(self, t: T2) -> None: ...

reveal_type(MyClass2[int]) # N: Revealed type is "def (t: builtins.int) -> __main__.MyClass2[builtins.int, builtins.int]"

# `T2` now defaults to `int` for `MyClass` from now on
reveal_type(MyClass2[str]) # N: Revealed type is "def (t: builtins.str) -> __main__.MyClass2[builtins.str, builtins.str]"
reveal_type(MyClass2[bytes]) # N: Revealed type is "def (t: builtins.bytes) -> __main__.MyClass2[builtins.bytes, builtins.bytes]"

d_int: type[MyClass2[int]]
d_str: type[MyClass2[str]]

reveal_type(d_int) # N: Revealed type is "type[__main__.MyClass2[builtins.int, builtins.int]]"
reveal_type(d_str) # N: Revealed type is "type[__main__.MyClass2[builtins.str, builtins.str]]"
[builtins fixtures/tuple.pyi]

[case testDefaultsApplicationInAliasNoCrash]
# https://github.com/python/mypy/issues/19186
from typing import Generic, TypeVar, TypeAlias

T1 = TypeVar("T1")
T2 = TypeVar("T2", default=T1)

Alias: TypeAlias = "MyClass[T1, T2]"

class MyClass(Generic["T1", "T2"]): ...
[builtins fixtures/tuple.pyi]

[case testTypeVarDefaultsClass1]
# flags: --disallow-any-generics
from typing import Generic, TypeVar, Union, overload
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/lib-stub/typing.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ NoReturn = 0
Never = 0
NewType = 0
ParamSpec = 0
TypeAlias = 0
TypeVarTuple = 0
Unpack = 0
Self = 0
Expand Down