diff --git a/mypy/semanal.py b/mypy/semanal.py index d7b50bd09496..902ff53556c7 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2404,7 +2404,7 @@ def tvar_defs_from_tvars( self.fail( message_registry.TYPE_VAR_REDECLARED_IN_NESTED_CLASS.format(name), context ) - tvar_def = self.tvar_scope.bind_new(name, tvar_expr) + tvar_def = self.tvar_scope.bind_new(name, tvar_expr, self.fail, context) if last_tvar_name_with_default is not None and not tvar_def.has_default(): self.msg.tvar_without_default_type( tvar_def.name, last_tvar_name_with_default, context @@ -2422,19 +2422,18 @@ def get_and_bind_all_tvars(self, type_exprs: list[Expression]) -> list[TypeVarLi a simplified version of the logic we use for ClassDef bases. We duplicate some amount of code, because it is hard to refactor common pieces. """ - tvars = [] + tvars: dict[str, tuple[TypeVarLikeExpr, Expression]] = {} for base_expr in type_exprs: try: base = self.expr_to_unanalyzed_type(base_expr) except TypeTranslationError: # This error will be caught later. continue - base_tvars = self.find_type_var_likes(base) - tvars.extend(base_tvars) - tvars = remove_dups(tvars) # Variables are defined in order of textual appearance. + for name, expr in self.find_type_var_likes(base): + tvars.setdefault(name, (expr, base_expr)) tvar_defs = [] - for name, tvar_expr in tvars: - tvar_def = self.tvar_scope.bind_new(name, tvar_expr) + for name, (tvar_expr, context) in tvars.items(): + tvar_def = self.tvar_scope.bind_new(name, tvar_expr, self.fail, context) tvar_defs.append(tvar_def) return tvar_defs diff --git a/mypy/tvar_scope.py b/mypy/tvar_scope.py index fe97a8359287..353809517e8b 100644 --- a/mypy/tvar_scope.py +++ b/mypy/tvar_scope.py @@ -1,6 +1,10 @@ from __future__ import annotations +from collections.abc import Callable +from typing_extensions import TypeAlias as _TypeAlias + from mypy.nodes import ( + Context, ParamSpecExpr, SymbolTableNode, TypeVarExpr, @@ -8,33 +12,68 @@ TypeVarTupleExpr, ) from mypy.types import ( + AnyType, ParamSpecFlavor, ParamSpecType, + TrivialSyntheticTypeTranslator, + Type, + TypeAliasType, + TypeOfAny, TypeVarId, TypeVarLikeType, TypeVarTupleType, TypeVarType, ) -from mypy.typetraverser import TypeTraverserVisitor - - -class TypeVarLikeNamespaceSetter(TypeTraverserVisitor): - """Set namespace for all TypeVarLikeTypes types.""" - def __init__(self, namespace: str) -> None: - self.namespace = namespace +FailFunc: _TypeAlias = Callable[[str, Context], None] - def visit_type_var(self, t: TypeVarType) -> None: - t.id.namespace = self.namespace - super().visit_type_var(t) - def visit_param_spec(self, t: ParamSpecType) -> None: - t.id.namespace = self.namespace - return super().visit_param_spec(t) +class TypeVarLikeDefaultFixer(TrivialSyntheticTypeTranslator): + """Set namespace for all TypeVarLikeTypes types.""" - def visit_type_var_tuple(self, t: TypeVarTupleType) -> None: - t.id.namespace = self.namespace - super().visit_type_var_tuple(t) + def __init__( + self, + scope: TypeVarLikeScope, + fail_func: FailFunc, + source_tv: TypeVarLikeExpr, + context: Context, + ) -> None: + self.scope = scope + self.fail_func = fail_func + self.source_tv = source_tv + self.context = context + super().__init__() + + def visit_type_var(self, t: TypeVarType) -> Type: + existing = self.scope.get_binding(t.fullname) + if existing is None: + self._report_unbound_tvar(t) + return AnyType(TypeOfAny.from_error) + return existing + + def visit_param_spec(self, t: ParamSpecType) -> Type: + existing = self.scope.get_binding(t.fullname) + if existing is None: + self._report_unbound_tvar(t) + return AnyType(TypeOfAny.from_error) + return existing + + def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: + existing = self.scope.get_binding(t.fullname) + if existing is None: + self._report_unbound_tvar(t) + return AnyType(TypeOfAny.from_error) + return existing + + def visit_type_alias_type(self, t: TypeAliasType) -> Type: + return t + + def _report_unbound_tvar(self, tvar: TypeVarLikeType) -> None: + self.fail_func( + f"Type variable {tvar.name} referenced in the default" + f" of {self.source_tv.name} is unbound", + self.context, + ) class TypeVarLikeScope: @@ -98,7 +137,9 @@ def new_unique_func_id(self) -> TypeVarId: self.func_id -= 1 return TypeVarId(self.func_id) - def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType: + def bind_new( + self, name: str, tvar_expr: TypeVarLikeExpr, fail_func: FailFunc, context: Context + ) -> TypeVarLikeType: if self.is_class_scope: self.class_id += 1 i = self.class_id @@ -106,7 +147,15 @@ 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)) + + # Defaults may reference other type variables. That is only valid when the + # referenced variable is already in scope (textually precedes the definition we're + # processing now). + default = tvar_expr.default.accept( + TypeVarLikeDefaultFixer( + self, fail_func=fail_func, source_tv=tvar_expr, context=context + ) + ) if isinstance(tvar_expr, TypeVarExpr): tvar_def: TypeVarLikeType = TypeVarType( @@ -115,7 +164,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, @@ -127,7 +176,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, ) @@ -138,7 +187,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, ) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index d7a07c9f48e3..7da38000c6d8 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1561,7 +1561,9 @@ def analyze_callable_type(self, t: UnboundType) -> Type: # below happens at very early stage. variables = [] for name, tvar_expr in self.find_type_var_likes(callable_args): - variables.append(self.tvar_scope.bind_new(name, tvar_expr)) + variables.append( + self.tvar_scope.bind_new(name, tvar_expr, self.fail_func, t) + ) maybe_ret = self.analyze_callable_args_for_paramspec( callable_args, ret_type, fallback ) or self.analyze_callable_args_for_concatenate( @@ -1833,7 +1835,7 @@ def bind_function_type_variables( assert var_node, "Binding for function type variable not found within function" var_expr = var_node.node assert isinstance(var_expr, TypeVarLikeExpr) - binding = self.tvar_scope.bind_new(var.name, var_expr) + binding = self.tvar_scope.bind_new(var.name, var_expr, self.fail_func, fun_type) defs.append(binding) return tuple(defs), has_self_type typevars, has_self_type = self.infer_type_variables(fun_type) @@ -1846,7 +1848,7 @@ def bind_function_type_variables( if not self.tvar_scope.allow_binding(tvar.fullname): err_msg = message_registry.TYPE_VAR_REDECLARED_IN_NESTED_CLASS.format(name) self.fail(err_msg.value, defn, code=err_msg.code) - binding = self.tvar_scope.bind_new(name, tvar) + binding = self.tvar_scope.bind_new(name, tvar, self.fail_func, fun_type) defs.append(binding) return tuple(defs), has_self_type diff --git a/test-data/unit/check-typevar-defaults.test b/test-data/unit/check-typevar-defaults.test index 103c0e782797..8f32b37c8c12 100644 --- a/test-data/unit/check-typevar-defaults.test +++ b/test-data/unit/check-typevar-defaults.test @@ -880,3 +880,45 @@ reveal_type(A1().x) # N: Revealed type is "TypedDict('__main__.TD', {'foo': bui reveal_type(A2().x) # N: Revealed type is "tuple[builtins.int, fallback=__main__.NT[builtins.int]]" reveal_type(A3().x) # N: Revealed type is "TypedDict('__main__.TD', {'foo': builtins.int})" [builtins fixtures/tuple.pyi] + +[case testDefaultsApplicationInAliasNoCrash] +# https://github.com/python/mypy/issues/19186 +from typing import Generic, TypeVar +from typing_extensions import TypeAlias + +T1 = TypeVar("T1") +T2 = TypeVar("T2", default=T1) + +Alias: TypeAlias = "MyClass[T1, T2]" + +class MyClass(Generic["T1", "T2"]): ... +[builtins fixtures/tuple.pyi] + +[case testDefaultsMustBeInScope] +from typing import Generic, TypeVar + +T1 = TypeVar("T1") +T2 = TypeVar("T2", default=T1) +T3 = TypeVar("T3", default=T2) + +class A(Generic[T1, T2, T3]): ... +reveal_type(A) # N: Revealed type is "def [T1, T2 = T1`1, T3 = T2`2 = T1`1] () -> __main__.A[T1`1, T2`2 = T1`1, T3`3 = T2`2 = T1`1]" +a: A[int] +reveal_type(a) # N: Revealed type is "__main__.A[builtins.int, builtins.int, T1`1]" + +class B(Generic[T1, T3]): ... # E: Type variable T2 referenced in the default of T3 is unbound +reveal_type(B) # N: Revealed type is "def [T1, T3 = Any] () -> __main__.B[T1`1, T3`2 = Any]" +b: B[int] +reveal_type(b) # N: Revealed type is "__main__.B[builtins.int, Any]" + +class C(Generic[T2]): ... # E: Type variable T1 referenced in the default of T2 is unbound +reveal_type(C) # N: Revealed type is "def [T2 = Any] () -> __main__.C[T2`1 = Any]" +c: C +reveal_type(c) # N: Revealed type is "__main__.C[Any]" + +class D(Generic[T2, T1]): ... # E: Type variable T1 referenced in the default of T2 is unbound \ + # E: "T1" cannot appear after "T2" in type parameter list because it has no default type +reveal_type(D) # N: Revealed type is "def [T2 = Any, T1 = Any] () -> __main__.D[T2`1 = Any, T1`2 = Any]" +d: D +reveal_type(d) # N: Revealed type is "__main__.D[Any, Any]" +[builtins fixtures/tuple.pyi]