Skip to content

Commit 60f7631

Browse files
committed
support nested TypeVars
1 parent 9b7cc6b commit 60f7631

File tree

9 files changed

+76
-6
lines changed

9 files changed

+76
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Basedmypy Changelog
22

33
## [Unreleased]
4+
### Added
5+
- Support using `TypeVar`s in the bounds of other `TypeVar`s
46
### Enhancements
57
- Similar errors on the same line will now not be removed
68
- Render generic upper bound with `: ` instead of ` <: `

mypy/applytype.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ def apply_generic_arguments(
9595
if type is None:
9696
continue
9797

98+
# apply concrete bounds
99+
tvar.upper_bound = expand_type(tvar.upper_bound, id_to_type)
100+
98101
target_type = get_target_type(
99102
tvar, type, callable, report_incompatible_typevar_value, context, skip_unsatisfied
100103
)

mypy/checker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6617,6 +6617,7 @@ def __init__(self) -> None:
66176617
self.arg_types: set[TypeVarType] = set()
66186618

66196619
def visit_type_var(self, t: TypeVarType) -> None:
6620+
t.upper_bound.accept(self)
66206621
self.arg_types.add(t)
66216622

66226623

mypy/constraints.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ def _infer_constraints(template: Type, actual: Type, direction: int) -> list[Con
192192
# T :> U2", but they are not equivalent to the constraint solver,
193193
# which never introduces new Union types (it uses join() instead).
194194
if isinstance(template, TypeVarType):
195-
return [Constraint(template, direction, actual)]
195+
return _infer_constraints(template.upper_bound, actual, direction) + [
196+
Constraint(template, direction, actual)
197+
]
196198

197199
# Now handle the case of either template or actual being a Union.
198200
# For a Union to be a subtype of another type, every item of the Union

mypy/expandtype.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def freshen_function_type_vars(callee: F) -> F:
107107
for v in callee.variables:
108108
if isinstance(v, TypeVarType):
109109
tv: TypeVarLikeType = TypeVarType.new_unification_variable(v)
110+
tv.upper_bound = expand_type(tv.upper_bound, tvmap)
110111
elif isinstance(v, TypeVarTupleType):
111112
assert isinstance(v, TypeVarTupleType)
112113
tv = TypeVarTupleType.new_unification_variable(v)

mypy/semanal.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3856,7 +3856,10 @@ def process_typevar_parameters(
38563856
# We want to use our custom error message below, so we suppress
38573857
# the default error message for invalid types here.
38583858
analyzed = self.expr_to_analyzed_type(
3859-
param_value, allow_placeholder=True, report_invalid_types=False
3859+
param_value,
3860+
allow_placeholder=True,
3861+
report_invalid_types=False,
3862+
allow_unbound_tvars=True,
38603863
)
38613864
if analyzed is None:
38623865
# Type variables are special: we need to place them in the symbol table
@@ -6040,7 +6043,11 @@ def accept(self, node: Node) -> None:
60406043
report_internal_error(err, self.errors.file, node.line, self.errors, self.options)
60416044

60426045
def expr_to_analyzed_type(
6043-
self, expr: Expression, report_invalid_types: bool = True, allow_placeholder: bool = False
6046+
self,
6047+
expr: Expression,
6048+
report_invalid_types: bool = True,
6049+
allow_placeholder: bool = False,
6050+
allow_unbound_tvars=False,
60446051
) -> Type | None:
60456052
if isinstance(expr, CallExpr):
60466053
# This is a legacy syntax intended mostly for Python 2, we keep it for
@@ -6065,7 +6072,10 @@ def expr_to_analyzed_type(
60656072
return TupleType(info.tuple_type.items, fallback=fallback)
60666073
typ = self.expr_to_unanalyzed_type(expr)
60676074
return self.anal_type(
6068-
typ, report_invalid_types=report_invalid_types, allow_placeholder=allow_placeholder
6075+
typ,
6076+
report_invalid_types=report_invalid_types,
6077+
allow_placeholder=allow_placeholder,
6078+
allow_unbound_tvars=allow_unbound_tvars,
60696079
)
60706080

60716081
def analyze_type_expr(self, expr: Expression) -> None:

mypy/typeanal.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1394,11 +1394,16 @@ def bind_function_type_variables(
13941394
defn,
13951395
code=codes.VALID_TYPE,
13961396
)
1397+
# update inner type vars
1398+
typ = get_proper_type(tvar.upper_bound)
1399+
if isinstance(typ, Instance):
1400+
typ.args = tuple(it.accept(self) for it in typ.args)
1401+
if isinstance(typ, UnboundType):
1402+
tvar.upper_bound = tvar.upper_bound.accept(self)
13971403
self.tvar_scope.bind_new(name, tvar, scopename=defn.name)
13981404
binding = self.tvar_scope.get_binding(tvar.fullname)
13991405
assert binding is not None
14001406
defs.append(binding)
1401-
14021407
return defs
14031408

14041409
def is_defined_type_var(self, tvar: str, context: Context) -> bool:
@@ -1755,6 +1760,9 @@ def visit_unbound_type(self, t: UnboundType) -> TypeVarLikeList:
17551760
and (self.include_bound_tvars or self.scope.get_binding(node) is None)
17561761
):
17571762
assert isinstance(node.node, TypeVarLikeExpr)
1763+
upper_bound = get_proper_type(node.node.upper_bound)
1764+
if isinstance(upper_bound, (Instance, UnboundType)):
1765+
return upper_bound.accept(self) + [(name, node.node)]
17581766
return [(name, node.node)]
17591767
elif not self.include_callables and self._seems_like_callable(t):
17601768
return []
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[case testNestedTypeVarBound]
2+
from typing import TypeVar, List
3+
4+
T = TypeVar("T", bound=int)
5+
L = TypeVar("L", bound=List[T])
6+
7+
def foo(l: L) -> T:
8+
return l[0]
9+
10+
reveal_type(foo([True])) # N: Revealed type is "bool"
11+
foo([""]) # E: Value of type variable "T" of "foo" cannot be "str" [type-var]
12+
13+
14+
[case testNestedTypeVarConstraint-xfail]
15+
from typing import TypeVar, Iterable
16+
17+
E = TypeVar("E", int, str)
18+
I = TypeVar("I", bound=Iterable[T])
19+
20+
def foo(i: I, e: E) -> I:
21+
assert i[0] == e
22+
return i
23+
24+
reveal_type(foo([True], True)) # N: Revealed type is "list[int]"
25+
reveal_type(foo(["my"], "py")) # N: Revealed type is "list[str]"
26+
reveal_type(foo(["my"], 10)) # E: argument 2 is bad
27+
reveal_type(foo([None], None)) # E: "I of foo" cannot be "list[None]"
28+
29+
30+
[case testNestedTypeVarConstraint2-xfail]
31+
from typing import TypeVar, Iterable, Set
32+
33+
T = TypeVar("T", bound=int)
34+
C = TypeVar("L", Sequence[T], Mapping[T, T])
35+
36+
def foo(c: C, t: T) -> C:
37+
assert c[0] == t
38+
return c
39+
40+
reveal_type(foo([True], True)) # N: Revealed type is "Sequence[bool]"
41+
reveal_type(foo(["my"], "py")) # E: "T of foo" can't be "str"
42+
reveal_type(foo({1: 1}, "10")) # E: bad arg 2
43+
reveal_type(foo({True: True}, True)) # N: Mapping[bool]

test-data/unit/lib-stub/basedtyping.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
# DO NOT ADD TO THIS FILE UNLESS YOU HAVE A GOOD REASON! Additional definitions
55
# will slow down tests.
66

7-
Untyped = 0
7+
Untyped = 0

0 commit comments

Comments
 (0)