Skip to content

Commit 9c40e56

Browse files
gvanrossumJukkaL
authored andcommitted
Infer variable types from simple literals in semanal.py (#1756)
Set the type of `x = 0` and similar in the semantic analyzer. This thwarts certain kinds of errors due to circular dependencies (see #1530). Skip this business if weak_opts is non-empty. Disable lightweight type check if build target < TYPE_CHECK. Only do the lightweight type inferencing thing outside functions.
1 parent ea06196 commit 9c40e56

13 files changed

+186
-29
lines changed

mypy/build.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,8 @@ def __init__(self, data_dir: str,
391391
check_untyped_defs = CHECK_UNTYPED_DEFS in self.flags
392392
self.semantic_analyzer = SemanticAnalyzer(lib_path, self.errors,
393393
pyversion=pyversion,
394-
check_untyped_defs=check_untyped_defs)
394+
check_untyped_defs=check_untyped_defs,
395+
lightweight_type_check=(target >= TYPE_CHECK))
395396
self.modules = self.semantic_analyzer.modules
396397
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
397398
self.type_checker = TypeChecker(self.errors,

mypy/semanal.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
YieldFromExpr, NamedTupleExpr, NonlocalDecl,
6464
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
6565
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, COVARIANT, CONTRAVARIANT,
66+
IntExpr, FloatExpr, UnicodeExpr,
6667
INVARIANT, UNBOUND_IMPORTED
6768
)
6869
from mypy.visitor import NodeVisitor
@@ -169,6 +170,10 @@ class SemanticAnalyzer(NodeVisitor):
169170
bound_tvars = None # type: List[SymbolTableNode]
170171
# Stack of type variables that were bound by outer classess
171172
tvar_stack = None # type: List[List[SymbolTableNode]]
173+
# Do weak type checking in this file
174+
weak_opts = set() # type: Set[str]
175+
# Do lightweight type checking
176+
lightweight_type_check = False # type: bool
172177

173178
# Stack of functions being analyzed
174179
function_stack = None # type: List[FuncItem]
@@ -193,7 +198,8 @@ def __init__(self,
193198
lib_path: List[str],
194199
errors: Errors,
195200
pyversion: Tuple[int, int],
196-
check_untyped_defs: bool) -> None:
201+
check_untyped_defs: bool,
202+
lightweight_type_check: bool = False) -> None:
197203
"""Construct semantic analyzer.
198204
199205
Use lib_path to search for modules, and report analysis errors
@@ -214,6 +220,7 @@ def __init__(self,
214220
self.modules = {}
215221
self.pyversion = pyversion
216222
self.check_untyped_defs = check_untyped_defs
223+
self.lightweight_type_check = lightweight_type_check
217224
self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES]
218225
self.postponed_functions_stack = []
219226
self.all_exports = set() # type: Set[str]
@@ -224,6 +231,7 @@ def visit_file(self, file_node: MypyFile, fnam: str) -> None:
224231
self.cur_mod_id = file_node.fullname()
225232
self.is_stub_file = fnam.lower().endswith('.pyi')
226233
self.globals = file_node.names
234+
self.weak_opts = file_node.weak_opts
227235

228236
if 'builtins' in self.modules:
229237
self.globals['__builtins__'] = SymbolTableNode(
@@ -1043,8 +1051,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
10431051
s.type = self.anal_type(s.type, allow_tuple_literal)
10441052
else:
10451053
# For simple assignments, allow binding type aliases.
1054+
# Also set the type if the rvalue is a simple literal.
10461055
if (s.type is None and len(s.lvalues) == 1 and
10471056
isinstance(s.lvalues[0], NameExpr)):
1057+
if s.lvalues[0].is_def:
1058+
s.type = self.analyze_simple_literal_type(s.rvalue)
10481059
res = analyze_type_alias(s.rvalue,
10491060
self.lookup_qualified,
10501061
self.lookup_fully_qualified,
@@ -1070,6 +1081,29 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
10701081
isinstance(s.rvalue, (ListExpr, TupleExpr))):
10711082
self.add_exports(*s.rvalue.items)
10721083

1084+
def analyze_simple_literal_type(self, rvalue: Node) -> Optional[Type]:
1085+
"""Return builtins.int if rvalue is an int literal, etc."""
1086+
if self.weak_opts or not self.lightweight_type_check or self.function_stack:
1087+
# Skip this if any weak options are set.
1088+
# Also skip if lightweight type check not requested.
1089+
# This is mostly to avoid breaking unit tests.
1090+
# Also skip inside a function; this is to avoid confusing
1091+
# the code that handles dead code due to isinstance()
1092+
# inside type variables with value restrictions (like
1093+
# AnyStr).
1094+
return None
1095+
if isinstance(rvalue, IntExpr):
1096+
return self.named_type_or_none('builtins.int')
1097+
if isinstance(rvalue, FloatExpr):
1098+
return self.named_type_or_none('builtins.float')
1099+
if isinstance(rvalue, StrExpr):
1100+
return self.named_type_or_none('builtins.str')
1101+
if isinstance(rvalue, BytesExpr):
1102+
return self.named_type_or_none('builtins.bytes')
1103+
if isinstance(rvalue, UnicodeExpr):
1104+
return self.named_type_or_none('builtins.unicode')
1105+
return None
1106+
10731107
def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
10741108
"""Check if assignment creates a type alias and set it up as needed."""
10751109
# For now, type aliases only work at the top level of a module.

test-data/unit/check-classes.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,9 @@ A.B = None # E: Cannot assign to a type
575575

576576
[case testAccessingClassAttributeWithTypeInferenceIssue]
577577
x = C.x # E: Cannot determine type of 'x'
578+
def f() -> int: return 1
578579
class C:
579-
x = 1
580+
x = f()
580581
[builtins fixtures/list.py]
581582

582583
[case testAccessingClassAttributeWithTypeInferenceIssue2]

test-data/unit/check-expressions.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1492,4 +1492,4 @@ reveal_type("foo") # E: Argument 1 to "reveal_type" has incompatible type "str";
14921492

14931493
[case testRevealTypeVar]
14941494
reveal_type = 1
1495-
1 + "foo" # E: Unsupported left operand type for + ("int")
1495+
1 + "foo" # E: Unsupported operand types for + ("int" and "str")

test-data/unit/check-functions.test

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,7 @@ f('x') # fail
10431043
[out]
10441044
main: note: In function "f":
10451045
main:5: error: Incompatible types in assignment (expression has type "str", variable has type "int")
1046-
main:9: error: Unsupported left operand type for + ("int")
1046+
main:9: error: Unsupported operand types for + ("int" and "str")
10471047
main: note: At top level:
10481048
main:12: error: Argument 1 to "f" has incompatible type "str"; expected "int"
10491049

@@ -1064,7 +1064,7 @@ def top() -> None:
10641064
[out]
10651065
main: note: In function "f":
10661066
main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int")
1067-
main:10: error: Unsupported left operand type for + ("int")
1067+
main:10: error: Unsupported operand types for + ("int" and "str")
10681068
main: note: In function "top":
10691069
main:13: error: Argument 1 to "f" has incompatible type "str"; expected "int"
10701070

@@ -1227,7 +1227,7 @@ A().f('x') # fail
12271227
[out]
12281228
main: note: In member "f" of class "A":
12291229
main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int")
1230-
main:10: error: Unsupported left operand type for + ("int")
1230+
main:10: error: Unsupported operand types for + ("int" and "str")
12311231
main: note: At top level:
12321232
main:13: error: Argument 1 to "f" of "A" has incompatible type "str"; expected "int"
12331233

@@ -1272,7 +1272,7 @@ def f(x: Callable[..., int]) -> None:
12721272
x()
12731273
x(1)
12741274
x(z=1)
1275-
x() + '' # E: Unsupported left operand type for + ("int")
1275+
x() + '' # E: Unsupported operand types for + ("int" and "str")
12761276
[out]
12771277
main: note: In function "f":
12781278

@@ -1285,7 +1285,7 @@ def f(x: Callable[..., int]) -> None:
12851285
[case testCastWithCallableAndArbitraryArgs]
12861286
from typing import Callable, cast
12871287
f = cast(Callable[..., int], None)
1288-
f(x=4) + '' # E: Unsupported left operand type for + ("int")
1288+
f(x=4) + '' # E: Unsupported operand types for + ("int" and "str")
12891289

12901290
[case testCallableWithArbitraryArgsInErrorMessage]
12911291
from typing import Callable

test-data/unit/check-inference.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1523,7 +1523,7 @@ main: note: In member "f" of class "A":
15231523
[case testMultipassAndTopLevelVariable]
15241524
y = x # E: Cannot determine type of 'x'
15251525
y()
1526-
x = 1
1526+
x = 1+0
15271527
[out]
15281528

15291529
[case testMultipassAndDecoratedMethod]

test-data/unit/check-isinstance.test

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,11 +1118,10 @@ def f(x: Union[str, int]):
11181118
from typing import Any
11191119
def f(x: Any):
11201120
assert isinstance(x, int) # this should narrow x to type int
1121-
x + "foo"
1121+
x + "foo" # E: Unsupported operand types for + ("int" and "str")
11221122
[builtins fixtures/isinstance.py]
11231123
[out]
11241124
main: note: In function "f":
1125-
main:4: error: Unsupported left operand type for + ("int")
11261125

11271126
[case testIsinstanceOfGenericClassRetainsParameters]
11281127
from typing import List, Union

test-data/unit/check-modules.test

Lines changed: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -486,13 +486,12 @@ x = 1
486486
x = 1
487487

488488
[case testAssignToFuncDefViaImport]
489-
from m import *
490-
# TODO: This is bad, we don't see what's coming from m.
489+
from m import * # E: Incompatible import of "x" (imported name has type "int", local name has type "str")
491490
f = None # E: Need type annotation for variable
492491
x = ''
493492
[file m.py]
494493
def f(): pass
495-
x = 1
494+
x = 1+0
496495
[out]
497496

498497

@@ -816,9 +815,11 @@ x = 0
816815
-- Test stability under import cycles
817816
-- ----------------------------------
818817

819-
-- The two tests are identical except one main has 'import x' and the other 'import y'.
820-
-- Previously (before build.order_ascc() was added) one of these would fail because the
821-
-- imports were processed in the (reverse) order in which the files were encountered.
818+
-- The first two tests are identical except one main has 'import x'
819+
-- and the other 'import y'. Previously (before build.order_ascc()
820+
-- was added) one of these would fail because the imports were
821+
-- processed in the (reverse) order in which the files were
822+
-- encountered.
822823

823824
[case testImportCycleStability1]
824825
import x
@@ -847,3 +848,114 @@ import x
847848
class Sub(x.Base):
848849
attr = x.Base.attr
849850
[out]
851+
852+
-- This case isn't fixed by order_ascc(), but is fixed by the
853+
-- lightweight type inference added to semanal.py
854+
-- (analyze_simple_literal_type()).
855+
856+
[case testImportCycleStability3]
857+
import y
858+
[file x.py]
859+
class Base:
860+
pass
861+
def foo() -> int:
862+
import y
863+
reveal_type(y.Sub.attr)
864+
return y.Sub.attr
865+
[file y.py]
866+
import x
867+
class Sub(x.Base):
868+
attr = 0
869+
[out]
870+
tmp/y.py:1: note: In module imported here,
871+
main:1: note: ... from here:
872+
tmp/x.py: note: In function "foo":
873+
tmp/x.py:5: error: Revealed type is 'builtins.int'
874+
875+
-- This case has a symmetrical cycle, so it doesn't matter in what
876+
-- order the files are processed. It depends on the lightweight type
877+
-- interference.
878+
879+
[case testImportCycleStability4]
880+
import x
881+
[file x.py]
882+
import y
883+
class C:
884+
attr = ''
885+
def foo() -> int:
886+
return y.D.attr
887+
[file y.py]
888+
import x
889+
class D:
890+
attr = 0
891+
def bar() -> str:
892+
return x.C.attr
893+
894+
-- These cases test all supported literal types.
895+
896+
[case testImportCycleStability5]
897+
import y
898+
[file x.py]
899+
class Base:
900+
pass
901+
def foo() -> None:
902+
import y
903+
i = y.Sub.iattr # type: int
904+
f = y.Sub.fattr # type: float
905+
s = y.Sub.sattr # type: str
906+
b = y.Sub.battr # type: bytes
907+
[file y.py]
908+
import x
909+
class Sub(x.Base):
910+
iattr = 0
911+
fattr = 0.0
912+
sattr = ''
913+
battr = b''
914+
[out]
915+
916+
[case testImportCycleStability6_python2]
917+
import y
918+
[file x.py]
919+
class Base:
920+
pass
921+
def foo() -> None:
922+
import y
923+
i = y.Sub.iattr # type: int
924+
f = y.Sub.fattr # type: float
925+
s = y.Sub.sattr # type: str
926+
u = y.Sub.uattr # type: unicode
927+
[file y.py]
928+
import x
929+
class Sub(x.Base):
930+
iattr = 0
931+
fattr = 0.0
932+
sattr = ''
933+
uattr = u''
934+
[out]
935+
936+
-- This case tests module-level variables.
937+
938+
[case testImportCycleStability7]
939+
import x
940+
[file x.py]
941+
def foo() -> int:
942+
import y
943+
reveal_type(y.value)
944+
return y.value
945+
[file y.py]
946+
import x
947+
value = 12
948+
[out]
949+
main:1: note: In module imported here:
950+
tmp/x.py: note: In function "foo":
951+
tmp/x.py:3: error: Revealed type is 'builtins.int'
952+
953+
-- This is not really cycle-related but still about the lightweight
954+
-- type checker.
955+
956+
[case testImportCycleStability8]
957+
x = 1 # type: str
958+
reveal_type(x)
959+
[out]
960+
main:1: error: Incompatible types in assignment (expression has type "int", variable has type "str")
961+
main:2: error: Revealed type is 'builtins.str'

test-data/unit/check-weak-typing.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ main: note: In function "f":
120120
[case testWeakFunction3]
121121
# mypy: weak
122122
def f():
123-
1 + 'a' # E: Unsupported left operand type for + ("int")
123+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
124124
[out]
125125
main: note: In function "f":
126126
[case testWeakFunctionCall]

test-data/unit/fixtures/isinstance.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ class function: pass
1414

1515
def isinstance(x: object, t: Union[type, Tuple[type, ...]]) -> bool: pass
1616

17-
class int: pass
17+
class int:
18+
def __add__(self, other: 'int') -> 'int': pass
1819
class float: pass
1920
class bool(int): pass
20-
class str: pass
21+
class str:
22+
def __add__(self, other: 'str') -> 'str': pass

test-data/unit/lib-stub/__builtin__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ def __init__(self, x):
1212

1313
# These are provided here for convenience.
1414
class int: pass
15+
class float: pass
16+
1517
class str: pass
18+
class unicode: pass
1619

1720
class tuple: pass
1821
class function: pass

test-data/unit/lib-stub/builtins.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ class type:
77
def __init__(self, x: Any) -> None: pass
88

99
# These are provided here for convenience.
10-
class int: pass
11-
class str: pass
10+
class int:
11+
def __add__(self, other: 'int') -> 'int': pass
12+
class float: pass
13+
14+
class str:
15+
def __add__(self, other: 'str') -> 'str': pass
16+
class bytes: pass
1217

1318
class tuple: pass
1419
class function: pass

0 commit comments

Comments
 (0)