Skip to content

Commit 2c152e6

Browse files
AurelienJaquierJaquier Aurélien Tristananilbeyilevkivskyi
authored
Add unbound typevar check (#13166)
### Description This should fix Issue #13061 . Mypy should now raise an error when it encounters unbound plain TypeVar. It should not get triggered when the return type contains a TypeVar (e.g. List[T]). It should not get triggered when a same TypeVar is present in the scope (e.g. inner function returns T, and T is an argument of outer function). ## Test Plan 5 tests were added: - a plain unbound typevar triggering the new error - a typevar inner function not triggering the error - a function returning an iterable of typevars, not triggering the error - a plain bound typevar, not triggering the error - nested bound typevars, not triggering the error We also changed the other functions triggering our error for the tests to pass. This is our 1st contribution to Mypy. Please, guide us if there is anything we can improve in this PR. This PR was made with @anilbey * Add simple unbound typevar check and rudimentary test * Add test for unbound func returning iterables of TypeVars * Add appropriate error message and make the new test pass * add CollectArgTypes to get set of argument types * lint fix * fix type error * extract check_unbound_return_typevar as method * check if return type is instantiated in typ.variables * Fix some tests that are affected by new implementation * add testInnerFunctionTypeVar test * fix testErrorCodeNeedTypeAnnotation test * add TYPE_VAR error code to unbound error failure * Fix the tests * move new tests in new test file * add 'check-typevar-unbound.test' to testcheck * add more nested tests for unbound type * microoptimise check_unbound_return_typevar check ret_type first * add missing builtins fixture to failing test Co-authored-by: Jaquier Aurélien Tristan <[email protected]> Co-authored-by: Anil Tuncel <[email protected]> Co-authored-by: Anil Tuncel <[email protected]> Co-authored-by: Ivan Levkivskyi <[email protected]>
1 parent 98f24f4 commit 2c152e6

10 files changed

+98
-13
lines changed

mypy/checker.py

+22
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing_extensions import Final, TypeAlias as _TypeAlias
1313

1414
from mypy.backports import nullcontext
15+
from mypy.errorcodes import TYPE_VAR
1516
from mypy.errors import Errors, report_internal_error, ErrorWatcher
1617
from mypy.nodes import (
1718
SymbolTable, Statement, MypyFile, Var, Expression, Lvalue, Node,
@@ -40,6 +41,7 @@
4041
get_proper_types, is_literal_type, TypeAliasType, TypeGuardedType, ParamSpecType,
4142
OVERLOAD_NAMES, UnboundType
4243
)
44+
from mypy.typetraverser import TypeTraverserVisitor
4345
from mypy.sametypes import is_same_type
4446
from mypy.messages import (
4547
MessageBuilder, make_inferred_type_note, append_invariance_notes, pretty_seq,
@@ -918,6 +920,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
918920
if typ.ret_type.variance == CONTRAVARIANT:
919921
self.fail(message_registry.RETURN_TYPE_CANNOT_BE_CONTRAVARIANT,
920922
typ.ret_type)
923+
self.check_unbound_return_typevar(typ)
921924

922925
# Check that Generator functions have the appropriate return type.
923926
if defn.is_generator:
@@ -1062,6 +1065,16 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
10621065

10631066
self.binder = old_binder
10641067

1068+
def check_unbound_return_typevar(self, typ: CallableType) -> None:
1069+
"""Fails when the return typevar is not defined in arguments."""
1070+
if (typ.ret_type in typ.variables):
1071+
arg_type_visitor = CollectArgTypes()
1072+
for argtype in typ.arg_types:
1073+
argtype.accept(arg_type_visitor)
1074+
1075+
if typ.ret_type not in arg_type_visitor.arg_types:
1076+
self.fail(message_registry.UNBOUND_TYPEVAR, typ.ret_type, code=TYPE_VAR)
1077+
10651078
def check_default_args(self, item: FuncItem, body_is_trivial: bool) -> None:
10661079
for arg in item.arguments:
10671080
if arg.initializer is None:
@@ -5862,6 +5875,15 @@ class Foo(Enum):
58625875
and member_type.fallback.type == parent_type.type_object())
58635876

58645877

5878+
class CollectArgTypes(TypeTraverserVisitor):
5879+
"""Collects the non-nested argument types in a set."""
5880+
def __init__(self) -> None:
5881+
self.arg_types: Set[TypeVarType] = set()
5882+
5883+
def visit_type_var(self, t: TypeVarType) -> None:
5884+
self.arg_types.add(t)
5885+
5886+
58655887
@overload
58665888
def conditional_types(current_type: Type,
58675889
proposed_type_ranges: Optional[List[TypeRange]],

mypy/message_registry.py

+3
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage":
167167
TYPEVAR_VARIANCE_DEF: Final = 'TypeVar "{}" may only be a literal bool'
168168
TYPEVAR_BOUND_MUST_BE_TYPE: Final = 'TypeVar "bound" must be a type'
169169
TYPEVAR_UNEXPECTED_ARGUMENT: Final = 'Unexpected argument to "TypeVar()"'
170+
UNBOUND_TYPEVAR: Final = (
171+
'A function returning TypeVar should receive at least '
172+
'one argument containing the same Typevar')
170173

171174
# Super
172175
TOO_MANY_ARGS_FOR_SUPER: Final = ErrorMessage('Too many arguments for "super"')

test-data/unit/check-classes.test

+2-1
Original file line numberDiff line numberDiff line change
@@ -3269,10 +3269,11 @@ def new_pro(pro_c: Type[P]) -> P:
32693269
return new_user(pro_c)
32703270
wiz = new_pro(WizUser)
32713271
reveal_type(wiz)
3272-
def error(u_c: Type[U]) -> P:
3272+
def error(u_c: Type[U]) -> P: # Error here, see below
32733273
return new_pro(u_c) # Error here, see below
32743274
[out]
32753275
main:11: note: Revealed type is "__main__.WizUser"
3276+
main:12: error: A function returning TypeVar should receive at least one argument containing the same Typevar
32763277
main:13: error: Value of type variable "P" of "new_pro" cannot be "U"
32773278
main:13: error: Incompatible return value type (got "U", expected "P")
32783279

test-data/unit/check-errorcodes.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ z: y # E: Variable "__main__.y" is not valid as a type [valid-type] \
335335
from typing import TypeVar
336336

337337
T = TypeVar('T')
338-
def f() -> T: pass
338+
def f() -> T: pass # E: A function returning TypeVar should receive at least one argument containing the same Typevar [type-var]
339339
x = f() # E: Need type annotation for "x" [var-annotated]
340340
y = [] # E: Need type annotation for "y" (hint: "y: List[<type>] = ...") [var-annotated]
341341
[builtins fixtures/list.pyi]

test-data/unit/check-generics.test

+5-5
Original file line numberDiff line numberDiff line change
@@ -1533,9 +1533,9 @@ A = TypeVar('A')
15331533
B = TypeVar('B')
15341534

15351535
def f1(x: A) -> A: ...
1536-
def f2(x: A) -> B: ...
1536+
def f2(x: A) -> B: ... # E: A function returning TypeVar should receive at least one argument containing the same Typevar
15371537
def f3(x: B) -> B: ...
1538-
def f4(x: int) -> A: ...
1538+
def f4(x: int) -> A: ... # E: A function returning TypeVar should receive at least one argument containing the same Typevar
15391539

15401540
y1 = f1
15411541
if int():
@@ -1584,8 +1584,8 @@ B = TypeVar('B')
15841584
T = TypeVar('T')
15851585
def outer(t: T) -> None:
15861586
def f1(x: A) -> A: ...
1587-
def f2(x: A) -> B: ...
1588-
def f3(x: T) -> A: ...
1587+
def f2(x: A) -> B: ... # E: A function returning TypeVar should receive at least one argument containing the same Typevar
1588+
def f3(x: T) -> A: ... # E: A function returning TypeVar should receive at least one argument containing the same Typevar
15891589
def f4(x: A) -> T: ...
15901590
def f5(x: T) -> T: ...
15911591

@@ -1754,7 +1754,7 @@ from typing import TypeVar
17541754
A = TypeVar('A')
17551755
B = TypeVar('B')
17561756
def f1(x: int, y: A) -> A: ...
1757-
def f2(x: int, y: A) -> B: ...
1757+
def f2(x: int, y: A) -> B: ... # E: A function returning TypeVar should receive at least one argument containing the same Typevar
17581758
def f3(x: A, y: B) -> B: ...
17591759
g = f1
17601760
g = f2

test-data/unit/check-inference.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ g(None) # Ok
448448
f() # Ok because not used to infer local variable type
449449
g(a)
450450

451-
def f() -> T: pass
451+
def f() -> T: pass # E: A function returning TypeVar should receive at least one argument containing the same Typevar
452452
def g(a: T) -> None: pass
453453
[out]
454454

@@ -2341,7 +2341,7 @@ def main() -> None:
23412341
[case testDontMarkUnreachableAfterInferenceUninhabited]
23422342
from typing import TypeVar
23432343
T = TypeVar('T')
2344-
def f() -> T: pass
2344+
def f() -> T: pass # E: A function returning TypeVar should receive at least one argument containing the same Typevar
23452345

23462346
class C:
23472347
x = f() # E: Need type annotation for "x"

test-data/unit/check-literal.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -972,15 +972,15 @@ b: bt # E: Variable "__main__.bt" is not valid as a ty
972972
[out]
973973

974974
[case testLiteralDisallowTypeVar]
975-
from typing import TypeVar
975+
from typing import TypeVar, Tuple
976976
from typing_extensions import Literal
977977

978978
T = TypeVar('T')
979979

980980
at = Literal[T] # E: Parameter 1 of Literal[...] is invalid
981981
a: at
982982

983-
def foo(b: Literal[T]) -> T: pass # E: Parameter 1 of Literal[...] is invalid
983+
def foo(b: Literal[T]) -> Tuple[T]: pass # E: Parameter 1 of Literal[...] is invalid
984984
[builtins fixtures/tuple.pyi]
985985
[out]
986986

test-data/unit/check-parameter-specification.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,7 @@ def callback(func: Callable[[Any], Any]) -> None: ...
10621062
class Job(Generic[P]): ...
10631063

10641064
@callback
1065-
def run_job(job: Job[...]) -> T: ...
1065+
def run_job(job: Job[...]) -> T: ... # E: A function returning TypeVar should receive at least one argument containing the same Typevar
10661066
[builtins fixtures/tuple.pyi]
10671067

10681068
[case testTupleAndDictOperationsOnParamSpecArgsAndKwargs]

test-data/unit/check-typevar-tuple.test

-1
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,3 @@ args: Tuple[bool, int, str, int, str, object]
9393
reveal_type(g(args)) # N: Revealed type is "Tuple[builtins.str, builtins.str, builtins.int]"
9494
reveal_type(h(args)) # N: Revealed type is "Tuple[builtins.str, builtins.str, builtins.int]"
9595
[builtins fixtures/tuple.pyi]
96-
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
[case testUnboundTypeVar]
3+
from typing import TypeVar
4+
5+
T = TypeVar('T')
6+
7+
def f() -> T: # E: A function returning TypeVar should receive at least one argument containing the same Typevar
8+
...
9+
10+
f()
11+
12+
13+
[case testInnerFunctionTypeVar]
14+
15+
from typing import TypeVar
16+
17+
T = TypeVar('T')
18+
19+
def g(a: T) -> T:
20+
def f() -> T:
21+
...
22+
return f()
23+
24+
25+
[case testUnboundIterableOfTypeVars]
26+
from typing import Iterable, TypeVar
27+
28+
T = TypeVar('T')
29+
30+
def f() -> Iterable[T]:
31+
...
32+
33+
f()
34+
35+
[case testBoundTypeVar]
36+
from typing import TypeVar
37+
38+
T = TypeVar('T')
39+
40+
def f(a: T, b: T, c: int) -> T:
41+
...
42+
43+
44+
[case testNestedBoundTypeVar]
45+
from typing import Callable, List, Union, Tuple, TypeVar
46+
47+
T = TypeVar('T')
48+
49+
def f(a: Union[int, T], b: str) -> T:
50+
...
51+
52+
def g(a: Callable[..., T], b: str) -> T:
53+
...
54+
55+
def h(a: List[Union[Callable[..., T]]]) -> T:
56+
...
57+
58+
def j(a: List[Union[Callable[..., Tuple[T, T]], int]]) -> T:
59+
...
60+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)