Skip to content

Commit 6682563

Browse files
Use namespaces for function type variables (#17311)
Fixes #16582 IMO this is long overdue. Currently, type variable IDs are 99% unique, but when they accidentally clash, it causes hard to debug issues. The implementation is generally straightforward, but it uncovered a whole bunch of unrelated bugs. Few notes: * This still doesn't fix the type variables in nested generic callable types (those that appear in return types of another generic callable). It is non-trivial to put namespace there, and luckily this situation is already special-cased in `checkexpr.py` to avoid ID clashes. * This uncovered a bug in overloaded dunder overrides handling, fix is simple. * This also uncovered a deeper problem in unsafe overload overlap logic (w.r.t. partial parameters overlap). Here proper fix would be hard, so instead I tweak current logic so it will not cause false positives, at a cost of possible false negatives. * This makes explicit that we use a somewhat ad-hoc logic for join/meet of generic callables. FWIW I decided to keep it, since it seems to work reasonably well. * This accidentally highlighted two bugs in error message locations. One very old one related to type aliases, I fixed newly discovered cases by extending a previous partial fix. Second, the error locations generated by `partial` plugin were completely off (you can see examples in `mypy_primer` where there were errors on empty lines etc). * This PR (naturally) causes a significant amount of new valid errors (fixed false negatives). To improve the error messages, I extend the name disambiguation logic to include type variables (and also type aliases, while I am at it), previously it only applied to `Instance`s. Note that I use a notation `TypeVar@namespace`, which is a semantic equivalent of qualified name for type variables. For now, I shorten the namespace to only the last component, to make errors less verbose. We can reconsider this if it causes confusion. * Finally, this PR will hopefully allow a more principled implementation of #15907 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 6bdd854 commit 6682563

26 files changed

+332
-133
lines changed

mypy/checker.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2171,7 +2171,9 @@ def bind_and_map_method(
21712171
def get_op_other_domain(self, tp: FunctionLike) -> Type | None:
21722172
if isinstance(tp, CallableType):
21732173
if tp.arg_kinds and tp.arg_kinds[0] == ARG_POS:
2174-
return tp.arg_types[0]
2174+
# For generic methods, domain comparison is tricky, as a first
2175+
# approximation erase all remaining type variables to bounds.
2176+
return erase_typevars(tp.arg_types[0], {v.id for v in tp.variables})
21752177
return None
21762178
elif isinstance(tp, Overloaded):
21772179
raw_items = [self.get_op_other_domain(it) for it in tp.items]

mypy/checkexpr.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
TypedDictType,
168168
TypeOfAny,
169169
TypeType,
170+
TypeVarId,
170171
TypeVarLikeType,
171172
TypeVarTupleType,
172173
TypeVarType,
@@ -4933,7 +4934,7 @@ def check_lst_expr(self, e: ListExpr | SetExpr | TupleExpr, fullname: str, tag:
49334934
tv = TypeVarType(
49344935
"T",
49354936
"T",
4936-
id=-1,
4937+
id=TypeVarId(-1, namespace="<lst>"),
49374938
values=[],
49384939
upper_bound=self.object_type(),
49394940
default=AnyType(TypeOfAny.from_omitted_generics),
@@ -5164,15 +5165,15 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
51645165
kt = TypeVarType(
51655166
"KT",
51665167
"KT",
5167-
id=-1,
5168+
id=TypeVarId(-1, namespace="<dict>"),
51685169
values=[],
51695170
upper_bound=self.object_type(),
51705171
default=AnyType(TypeOfAny.from_omitted_generics),
51715172
)
51725173
vt = TypeVarType(
51735174
"VT",
51745175
"VT",
5175-
id=-2,
5176+
id=TypeVarId(-2, namespace="<dict>"),
51765177
values=[],
51775178
upper_bound=self.object_type(),
51785179
default=AnyType(TypeOfAny.from_omitted_generics),
@@ -5564,7 +5565,7 @@ def check_generator_or_comprehension(
55645565
tv = TypeVarType(
55655566
"T",
55665567
"T",
5567-
id=-1,
5568+
id=TypeVarId(-1, namespace="<genexp>"),
55685569
values=[],
55695570
upper_bound=self.object_type(),
55705571
default=AnyType(TypeOfAny.from_omitted_generics),
@@ -5591,15 +5592,15 @@ def visit_dictionary_comprehension(self, e: DictionaryComprehension) -> Type:
55915592
ktdef = TypeVarType(
55925593
"KT",
55935594
"KT",
5594-
id=-1,
5595+
id=TypeVarId(-1, namespace="<dict>"),
55955596
values=[],
55965597
upper_bound=self.object_type(),
55975598
default=AnyType(TypeOfAny.from_omitted_generics),
55985599
)
55995600
vtdef = TypeVarType(
56005601
"VT",
56015602
"VT",
5602-
id=-2,
5603+
id=TypeVarId(-2, namespace="<dict>"),
56035604
values=[],
56045605
upper_bound=self.object_type(),
56055606
default=AnyType(TypeOfAny.from_omitted_generics),

mypy/expandtype.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def interpolate_args_for_unpack(self, t: CallableType, var_arg: UnpackType) -> l
316316
new_unpack: Type
317317
if isinstance(var_arg_type, Instance):
318318
# we have something like Unpack[Tuple[Any, ...]]
319-
new_unpack = var_arg
319+
new_unpack = UnpackType(var_arg.type.accept(self))
320320
elif isinstance(var_arg_type, TupleType):
321321
# We have something like Unpack[Tuple[Unpack[Ts], X1, X2]]
322322
expanded_tuple = var_arg_type.accept(self)

mypy/join.py

+31
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Sequence, overload
66

77
import mypy.typeops
8+
from mypy.expandtype import expand_type
89
from mypy.maptype import map_instance_to_supertype
910
from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT, VARIANCE_NOT_READY
1011
from mypy.state import state
@@ -36,6 +37,7 @@
3637
TypedDictType,
3738
TypeOfAny,
3839
TypeType,
40+
TypeVarId,
3941
TypeVarLikeType,
4042
TypeVarTupleType,
4143
TypeVarType,
@@ -718,7 +720,35 @@ def is_similar_callables(t: CallableType, s: CallableType) -> bool:
718720
)
719721

720722

723+
def update_callable_ids(c: CallableType, ids: list[TypeVarId]) -> CallableType:
724+
tv_map = {}
725+
tvs = []
726+
for tv, new_id in zip(c.variables, ids):
727+
new_tv = tv.copy_modified(id=new_id)
728+
tvs.append(new_tv)
729+
tv_map[tv.id] = new_tv
730+
return expand_type(c, tv_map).copy_modified(variables=tvs)
731+
732+
733+
def match_generic_callables(t: CallableType, s: CallableType) -> tuple[CallableType, CallableType]:
734+
# The case where we combine/join/meet similar callables, situation where both are generic
735+
# requires special care. A more principled solution may involve unify_generic_callable(),
736+
# but it would have two problems:
737+
# * This adds risk of infinite recursion: e.g. join -> unification -> solver -> join
738+
# * Using unification is an incorrect thing for meets, as it "widens" the types
739+
# Finally, this effectively falls back to an old behaviour before namespaces were added to
740+
# type variables, and it worked relatively well.
741+
max_len = max(len(t.variables), len(s.variables))
742+
min_len = min(len(t.variables), len(s.variables))
743+
if min_len == 0:
744+
return t, s
745+
new_ids = [TypeVarId.new(meta_level=0) for _ in range(max_len)]
746+
# Note: this relies on variables being in order they appear in function definition.
747+
return update_callable_ids(t, new_ids), update_callable_ids(s, new_ids)
748+
749+
721750
def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
751+
t, s = match_generic_callables(t, s)
722752
arg_types: list[Type] = []
723753
for i in range(len(t.arg_types)):
724754
arg_types.append(safe_meet(t.arg_types[i], s.arg_types[i]))
@@ -771,6 +801,7 @@ def safe_meet(t: Type, s: Type) -> Type:
771801

772802

773803
def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
804+
t, s = match_generic_callables(t, s)
774805
arg_types: list[Type] = []
775806
for i in range(len(t.arg_types)):
776807
arg_types.append(safe_join(t.arg_types[i], s.arg_types[i]))

mypy/meet.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1024,8 +1024,9 @@ def default(self, typ: Type) -> ProperType:
10241024

10251025

10261026
def meet_similar_callables(t: CallableType, s: CallableType) -> CallableType:
1027-
from mypy.join import safe_join
1027+
from mypy.join import match_generic_callables, safe_join
10281028

1029+
t, s = match_generic_callables(t, s)
10291030
arg_types: list[Type] = []
10301031
for i in range(len(t.arg_types)):
10311032
arg_types.append(safe_join(t.arg_types[i], s.arg_types[i]))

mypy/messages.py

+57-19
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
TypeOfAny,
8484
TypeStrVisitor,
8585
TypeType,
86+
TypeVarLikeType,
8687
TypeVarTupleType,
8788
TypeVarType,
8889
UnboundType,
@@ -2502,14 +2503,16 @@ def format_literal_value(typ: LiteralType) -> str:
25022503
return typ.value_repr()
25032504

25042505
if isinstance(typ, TypeAliasType) and typ.is_recursive:
2505-
# TODO: find balance here, str(typ) doesn't support custom verbosity, and may be
2506-
# too verbose for user messages, OTOH it nicely shows structure of recursive types.
2507-
if verbosity < 2:
2508-
type_str = typ.alias.name if typ.alias else "<alias (unfixed)>"
2506+
if typ.alias is None:
2507+
type_str = "<alias (unfixed)>"
2508+
else:
2509+
if verbosity >= 2 or (fullnames and typ.alias.fullname in fullnames):
2510+
type_str = typ.alias.fullname
2511+
else:
2512+
type_str = typ.alias.name
25092513
if typ.args:
25102514
type_str += f"[{format_list(typ.args)}]"
2511-
return type_str
2512-
return str(typ)
2515+
return type_str
25132516

25142517
# TODO: always mention type alias names in errors.
25152518
typ = get_proper_type(typ)
@@ -2550,9 +2553,15 @@ def format_literal_value(typ: LiteralType) -> str:
25502553
return f"Unpack[{format(typ.type)}]"
25512554
elif isinstance(typ, TypeVarType):
25522555
# This is similar to non-generic instance types.
2556+
fullname = scoped_type_var_name(typ)
2557+
if verbosity >= 2 or (fullnames and fullname in fullnames):
2558+
return fullname
25532559
return typ.name
25542560
elif isinstance(typ, TypeVarTupleType):
25552561
# This is similar to non-generic instance types.
2562+
fullname = scoped_type_var_name(typ)
2563+
if verbosity >= 2 or (fullnames and fullname in fullnames):
2564+
return fullname
25562565
return typ.name
25572566
elif isinstance(typ, ParamSpecType):
25582567
# Concatenate[..., P]
@@ -2563,6 +2572,7 @@ def format_literal_value(typ: LiteralType) -> str:
25632572

25642573
return f"[{args}, **{typ.name_with_suffix()}]"
25652574
else:
2575+
# TODO: better disambiguate ParamSpec name clashes.
25662576
return typ.name_with_suffix()
25672577
elif isinstance(typ, TupleType):
25682578
# Prefer the name of the fallback class (if not tuple), as it's more informative.
@@ -2680,29 +2690,51 @@ def format_literal_value(typ: LiteralType) -> str:
26802690
return "object"
26812691

26822692

2683-
def collect_all_instances(t: Type) -> list[Instance]:
2684-
"""Return all instances that `t` contains (including `t`).
2693+
def collect_all_named_types(t: Type) -> list[Type]:
2694+
"""Return all instances/aliases/type variables that `t` contains (including `t`).
26852695
26862696
This is similar to collect_all_inner_types from typeanal but only
26872697
returns instances and will recurse into fallbacks.
26882698
"""
2689-
visitor = CollectAllInstancesQuery()
2699+
visitor = CollectAllNamedTypesQuery()
26902700
t.accept(visitor)
2691-
return visitor.instances
2701+
return visitor.types
26922702

26932703

2694-
class CollectAllInstancesQuery(TypeTraverserVisitor):
2704+
class CollectAllNamedTypesQuery(TypeTraverserVisitor):
26952705
def __init__(self) -> None:
2696-
self.instances: list[Instance] = []
2706+
self.types: list[Type] = []
26972707

26982708
def visit_instance(self, t: Instance) -> None:
2699-
self.instances.append(t)
2709+
self.types.append(t)
27002710
super().visit_instance(t)
27012711

27022712
def visit_type_alias_type(self, t: TypeAliasType) -> None:
27032713
if t.alias and not t.is_recursive:
2704-
t.alias.target.accept(self)
2705-
super().visit_type_alias_type(t)
2714+
get_proper_type(t).accept(self)
2715+
else:
2716+
self.types.append(t)
2717+
super().visit_type_alias_type(t)
2718+
2719+
def visit_type_var(self, t: TypeVarType) -> None:
2720+
self.types.append(t)
2721+
super().visit_type_var(t)
2722+
2723+
def visit_type_var_tuple(self, t: TypeVarTupleType) -> None:
2724+
self.types.append(t)
2725+
super().visit_type_var_tuple(t)
2726+
2727+
def visit_param_spec(self, t: ParamSpecType) -> None:
2728+
self.types.append(t)
2729+
super().visit_param_spec(t)
2730+
2731+
2732+
def scoped_type_var_name(t: TypeVarLikeType) -> str:
2733+
if not t.id.namespace:
2734+
return t.name
2735+
# TODO: support rare cases when both TypeVar name and namespace suffix coincide.
2736+
*_, suffix = t.id.namespace.split(".")
2737+
return f"{t.name}@{suffix}"
27062738

27072739

27082740
def find_type_overlaps(*types: Type) -> set[str]:
@@ -2713,8 +2745,14 @@ def find_type_overlaps(*types: Type) -> set[str]:
27132745
"""
27142746
d: dict[str, set[str]] = {}
27152747
for type in types:
2716-
for inst in collect_all_instances(type):
2717-
d.setdefault(inst.type.name, set()).add(inst.type.fullname)
2748+
for t in collect_all_named_types(type):
2749+
if isinstance(t, ProperType) and isinstance(t, Instance):
2750+
d.setdefault(t.type.name, set()).add(t.type.fullname)
2751+
elif isinstance(t, TypeAliasType) and t.alias:
2752+
d.setdefault(t.alias.name, set()).add(t.alias.fullname)
2753+
else:
2754+
assert isinstance(t, TypeVarLikeType)
2755+
d.setdefault(t.name, set()).add(scoped_type_var_name(t))
27182756
for shortname in d.keys():
27192757
if f"typing.{shortname}" in TYPES_FOR_UNIMPORTED_HINTS:
27202758
d[shortname].add(f"typing.{shortname}")
@@ -2732,7 +2770,7 @@ def format_type(
27322770
"""
27332771
Convert a type to a relatively short string suitable for error messages.
27342772
2735-
`verbosity` is a coarse grained control on the verbosity of the type
2773+
`verbosity` is a coarse-grained control on the verbosity of the type
27362774
27372775
This function returns a string appropriate for unmodified use in error
27382776
messages; this means that it will be quoted in most cases. If
@@ -2748,7 +2786,7 @@ def format_type_bare(
27482786
"""
27492787
Convert a type to a relatively short string suitable for error messages.
27502788
2751-
`verbosity` is a coarse grained control on the verbosity of the type
2789+
`verbosity` is a coarse-grained control on the verbosity of the type
27522790
`fullnames` specifies a set of names that should be printed in full
27532791
27542792
This function will return an unquoted string. If a caller doesn't need to

mypy/plugins/attrs.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
Type,
7070
TypeOfAny,
7171
TypeType,
72+
TypeVarId,
7273
TypeVarType,
7374
UninhabitedType,
7475
UnionType,
@@ -807,25 +808,25 @@ def _add_order(ctx: mypy.plugin.ClassDefContext, adder: MethodAdder) -> None:
807808
# AT = TypeVar('AT')
808809
# def __lt__(self: AT, other: AT) -> bool
809810
# This way comparisons with subclasses will work correctly.
811+
fullname = f"{ctx.cls.info.fullname}.{SELF_TVAR_NAME}"
810812
tvd = TypeVarType(
811813
SELF_TVAR_NAME,
812-
ctx.cls.info.fullname + "." + SELF_TVAR_NAME,
813-
id=-1,
814+
fullname,
815+
# Namespace is patched per-method below.
816+
id=TypeVarId(-1, namespace=""),
814817
values=[],
815818
upper_bound=object_type,
816819
default=AnyType(TypeOfAny.from_omitted_generics),
817820
)
818821
self_tvar_expr = TypeVarExpr(
819-
SELF_TVAR_NAME,
820-
ctx.cls.info.fullname + "." + SELF_TVAR_NAME,
821-
[],
822-
object_type,
823-
AnyType(TypeOfAny.from_omitted_generics),
822+
SELF_TVAR_NAME, fullname, [], object_type, AnyType(TypeOfAny.from_omitted_generics)
824823
)
825824
ctx.cls.info.names[SELF_TVAR_NAME] = SymbolTableNode(MDEF, self_tvar_expr)
826825

827-
args = [Argument(Var("other", tvd), tvd, None, ARG_POS)]
828826
for method in ["__lt__", "__le__", "__gt__", "__ge__"]:
827+
namespace = f"{ctx.cls.info.fullname}.{method}"
828+
tvd = tvd.copy_modified(id=TypeVarId(tvd.id.raw_id, namespace=namespace))
829+
args = [Argument(Var("other", tvd), tvd, None, ARG_POS)]
829830
adder.add_method(method, args, bool_type, self_type=tvd, tvd=tvd)
830831

831832

mypy/plugins/dataclasses.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
TupleType,
6666
Type,
6767
TypeOfAny,
68+
TypeVarId,
6869
TypeVarType,
6970
UninhabitedType,
7071
UnionType,
@@ -314,8 +315,8 @@ def transform(self) -> bool:
314315
obj_type = self._api.named_type("builtins.object")
315316
order_tvar_def = TypeVarType(
316317
SELF_TVAR_NAME,
317-
info.fullname + "." + SELF_TVAR_NAME,
318-
id=-1,
318+
f"{info.fullname}.{SELF_TVAR_NAME}",
319+
id=TypeVarId(-1, namespace=f"{info.fullname}.{method_name}"),
319320
values=[],
320321
upper_bound=obj_type,
321322
default=AnyType(TypeOfAny.from_omitted_generics),

mypy/plugins/functools.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import mypy.checker
88
import mypy.plugin
99
from mypy.argmap import map_actuals_to_formals
10-
from mypy.nodes import ARG_POS, ARG_STAR2, ArgKind, Argument, FuncItem, Var
10+
from mypy.nodes import ARG_POS, ARG_STAR2, ArgKind, Argument, CallExpr, FuncItem, Var
1111
from mypy.plugins.common import add_method_to_class
1212
from mypy.types import (
1313
AnyType,
@@ -151,12 +151,22 @@ def partial_new_callback(ctx: mypy.plugin.FunctionContext) -> Type:
151151
actual_arg_names = [a for param in ctx.arg_names[1:] for a in param]
152152
actual_types = [a for param in ctx.arg_types[1:] for a in param]
153153

154+
# Create a valid context for various ad-hoc inspections in check_call().
155+
call_expr = CallExpr(
156+
callee=ctx.args[0][0],
157+
args=actual_args,
158+
arg_kinds=actual_arg_kinds,
159+
arg_names=actual_arg_names,
160+
analyzed=ctx.context.analyzed if isinstance(ctx.context, CallExpr) else None,
161+
)
162+
call_expr.set_line(ctx.context)
163+
154164
_, bound = ctx.api.expr_checker.check_call(
155165
callee=defaulted,
156166
args=actual_args,
157167
arg_kinds=actual_arg_kinds,
158168
arg_names=actual_arg_names,
159-
context=defaulted,
169+
context=call_expr,
160170
)
161171
bound = get_proper_type(bound)
162172
if not isinstance(bound, CallableType):

0 commit comments

Comments
 (0)