Skip to content

Infer variable types from simple literals in semanal.py #1756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,8 @@ def __init__(self, data_dir: str,
check_untyped_defs = CHECK_UNTYPED_DEFS in self.flags
self.semantic_analyzer = SemanticAnalyzer(lib_path, self.errors,
pyversion=pyversion,
check_untyped_defs=check_untyped_defs)
check_untyped_defs=check_untyped_defs,
lightweight_type_check=(target >= TYPE_CHECK))
self.modules = self.semantic_analyzer.modules
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
self.type_checker = TypeChecker(self.errors,
Expand Down
36 changes: 35 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
YieldFromExpr, NamedTupleExpr, NonlocalDecl,
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, COVARIANT, CONTRAVARIANT,
IntExpr, FloatExpr, UnicodeExpr,
INVARIANT, UNBOUND_IMPORTED
)
from mypy.visitor import NodeVisitor
Expand Down Expand Up @@ -169,6 +170,10 @@ class SemanticAnalyzer(NodeVisitor):
bound_tvars = None # type: List[SymbolTableNode]
# Stack of type variables that were bound by outer classess
tvar_stack = None # type: List[List[SymbolTableNode]]
# Do weak type checking in this file
weak_opts = set() # type: Set[str]
# Do lightweight type checking
lightweight_type_check = False # type: bool

# Stack of functions being analyzed
function_stack = None # type: List[FuncItem]
Expand All @@ -193,7 +198,8 @@ def __init__(self,
lib_path: List[str],
errors: Errors,
pyversion: Tuple[int, int],
check_untyped_defs: bool) -> None:
check_untyped_defs: bool,
lightweight_type_check: bool = False) -> None:
"""Construct semantic analyzer.

Use lib_path to search for modules, and report analysis errors
Expand All @@ -214,6 +220,7 @@ def __init__(self,
self.modules = {}
self.pyversion = pyversion
self.check_untyped_defs = check_untyped_defs
self.lightweight_type_check = lightweight_type_check
self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES]
self.postponed_functions_stack = []
self.all_exports = set() # type: Set[str]
Expand All @@ -224,6 +231,7 @@ def visit_file(self, file_node: MypyFile, fnam: str) -> None:
self.cur_mod_id = file_node.fullname()
self.is_stub_file = fnam.lower().endswith('.pyi')
self.globals = file_node.names
self.weak_opts = file_node.weak_opts

if 'builtins' in self.modules:
self.globals['__builtins__'] = SymbolTableNode(
Expand Down Expand Up @@ -1043,8 +1051,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
s.type = self.anal_type(s.type, allow_tuple_literal)
else:
# For simple assignments, allow binding type aliases.
# Also set the type if the rvalue is a simple literal.
if (s.type is None and len(s.lvalues) == 1 and
isinstance(s.lvalues[0], NameExpr)):
if s.lvalues[0].is_def:
s.type = self.analyze_simple_literal_type(s.rvalue)
res = analyze_type_alias(s.rvalue,
self.lookup_qualified,
self.lookup_fully_qualified,
Expand All @@ -1070,6 +1081,29 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
isinstance(s.rvalue, (ListExpr, TupleExpr))):
self.add_exports(*s.rvalue.items)

def analyze_simple_literal_type(self, rvalue: Node) -> Optional[Type]:
"""Return builtins.int if rvalue is an int literal, etc."""
if self.weak_opts or not self.lightweight_type_check or self.function_stack:
# Skip this if any weak options are set.
# Also skip if lightweight type check not requested.
# This is mostly to avoid breaking unit tests.
# Also skip inside a function; this is to avoid confusing
# the code that handles dead code due to isinstance()
# inside type variables with value restrictions (like
# AnyStr).
return None
if isinstance(rvalue, IntExpr):
return self.named_type_or_none('builtins.int')
if isinstance(rvalue, FloatExpr):
return self.named_type_or_none('builtins.float')
if isinstance(rvalue, StrExpr):
return self.named_type_or_none('builtins.str')
if isinstance(rvalue, BytesExpr):
return self.named_type_or_none('builtins.bytes')
if isinstance(rvalue, UnicodeExpr):
return self.named_type_or_none('builtins.unicode')
return None

def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
"""Check if assignment creates a type alias and set it up as needed."""
# For now, type aliases only work at the top level of a module.
Expand Down
3 changes: 2 additions & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,9 @@ A.B = None # E: Cannot assign to a type

[case testAccessingClassAttributeWithTypeInferenceIssue]
x = C.x # E: Cannot determine type of 'x'
def f() -> int: return 1
class C:
x = 1
x = f()
[builtins fixtures/list.py]

[case testAccessingClassAttributeWithTypeInferenceIssue2]
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1492,4 +1492,4 @@ reveal_type("foo") # E: Argument 1 to "reveal_type" has incompatible type "str";

[case testRevealTypeVar]
reveal_type = 1
1 + "foo" # E: Unsupported left operand type for + ("int")
1 + "foo" # E: Unsupported operand types for + ("int" and "str")
10 changes: 5 additions & 5 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ f('x') # fail
[out]
main: note: In function "f":
main:5: error: Incompatible types in assignment (expression has type "str", variable has type "int")
main:9: error: Unsupported left operand type for + ("int")
main:9: error: Unsupported operand types for + ("int" and "str")
main: note: At top level:
main:12: error: Argument 1 to "f" has incompatible type "str"; expected "int"

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

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

Expand Down Expand Up @@ -1272,7 +1272,7 @@ def f(x: Callable[..., int]) -> None:
x()
x(1)
x(z=1)
x() + '' # E: Unsupported left operand type for + ("int")
x() + '' # E: Unsupported operand types for + ("int" and "str")
[out]
main: note: In function "f":

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

[case testCallableWithArbitraryArgsInErrorMessage]
from typing import Callable
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,7 @@ main: note: In member "f" of class "A":
[case testMultipassAndTopLevelVariable]
y = x # E: Cannot determine type of 'x'
y()
x = 1
x = 1+0
[out]

[case testMultipassAndDecoratedMethod]
Expand Down
3 changes: 1 addition & 2 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -1118,11 +1118,10 @@ def f(x: Union[str, int]):
from typing import Any
def f(x: Any):
assert isinstance(x, int) # this should narrow x to type int
x + "foo"
x + "foo" # E: Unsupported operand types for + ("int" and "str")
[builtins fixtures/isinstance.py]
[out]
main: note: In function "f":
main:4: error: Unsupported left operand type for + ("int")

[case testIsinstanceOfGenericClassRetainsParameters]
from typing import List, Union
Expand Down
124 changes: 118 additions & 6 deletions test-data/unit/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -486,13 +486,12 @@ x = 1
x = 1

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


Expand Down Expand Up @@ -816,9 +815,11 @@ x = 0
-- Test stability under import cycles
-- ----------------------------------

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

[case testImportCycleStability1]
import x
Expand Down Expand Up @@ -847,3 +848,114 @@ import x
class Sub(x.Base):
attr = x.Base.attr
[out]

-- This case isn't fixed by order_ascc(), but is fixed by the
-- lightweight type inference added to semanal.py
-- (analyze_simple_literal_type()).

[case testImportCycleStability3]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another potential test case is one where two files that form a cycle symmetrically refer to a attribute defined in the other file. This would remain a valid test case no matter in which order we'll end up type checking the modules.

import y
[file x.py]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also suggest testing some additional things:

  • Test every supported literal type.
  • Test variables defined at module top level.
  • Reveal the inferred type (to verify that it somehow isn't Any, for example).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test case that checks something like this:

x = 1 # type: str   # should be an error
x()  # should be str, not int

class Base:
pass
def foo() -> int:
import y
reveal_type(y.Sub.attr)
return y.Sub.attr
[file y.py]
import x
class Sub(x.Base):
attr = 0
[out]
tmp/y.py:1: note: In module imported here,
main:1: note: ... from here:
tmp/x.py: note: In function "foo":
tmp/x.py:5: error: Revealed type is 'builtins.int'

-- This case has a symmetrical cycle, so it doesn't matter in what
-- order the files are processed. It depends on the lightweight type
-- interference.

[case testImportCycleStability4]
import x
[file x.py]
import y
class C:
attr = ''
def foo() -> int:
return y.D.attr
[file y.py]
import x
class D:
attr = 0
def bar() -> str:
return x.C.attr

-- These cases test all supported literal types.

[case testImportCycleStability5]
import y
[file x.py]
class Base:
pass
def foo() -> None:
import y
i = y.Sub.iattr # type: int
f = y.Sub.fattr # type: float
s = y.Sub.sattr # type: str
b = y.Sub.battr # type: bytes
[file y.py]
import x
class Sub(x.Base):
iattr = 0
fattr = 0.0
sattr = ''
battr = b''
[out]

[case testImportCycleStability6_python2]
import y
[file x.py]
class Base:
pass
def foo() -> None:
import y
i = y.Sub.iattr # type: int
f = y.Sub.fattr # type: float
s = y.Sub.sattr # type: str
u = y.Sub.uattr # type: unicode
[file y.py]
import x
class Sub(x.Base):
iattr = 0
fattr = 0.0
sattr = ''
uattr = u''
[out]

-- This case tests module-level variables.

[case testImportCycleStability7]
import x
[file x.py]
def foo() -> int:
import y
reveal_type(y.value)
return y.value
[file y.py]
import x
value = 12
[out]
main:1: note: In module imported here:
tmp/x.py: note: In function "foo":
tmp/x.py:3: error: Revealed type is 'builtins.int'

-- This is not really cycle-related but still about the lightweight
-- type checker.

[case testImportCycleStability8]
x = 1 # type: str
reveal_type(x)
[out]
main:1: error: Incompatible types in assignment (expression has type "int", variable has type "str")
main:2: error: Revealed type is 'builtins.str'
2 changes: 1 addition & 1 deletion test-data/unit/check-weak-typing.test
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ main: note: In function "f":
[case testWeakFunction3]
# mypy: weak
def f():
1 + 'a' # E: Unsupported left operand type for + ("int")
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
[out]
main: note: In function "f":
[case testWeakFunctionCall]
Expand Down
6 changes: 4 additions & 2 deletions test-data/unit/fixtures/isinstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ class function: pass

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

class int: pass
class int:
def __add__(self, other: 'int') -> 'int': pass
class float: pass
class bool(int): pass
class str: pass
class str:
def __add__(self, other: 'str') -> 'str': pass
3 changes: 3 additions & 0 deletions test-data/unit/lib-stub/__builtin__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ def __init__(self, x):

# These are provided here for convenience.
class int: pass
class float: pass

class str: pass
class unicode: pass

class tuple: pass
class function: pass
Expand Down
9 changes: 7 additions & 2 deletions test-data/unit/lib-stub/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ class type:
def __init__(self, x: Any) -> None: pass

# These are provided here for convenience.
class int: pass
class str: pass
class int:
def __add__(self, other: 'int') -> 'int': pass
class float: pass

class str:
def __add__(self, other: 'str') -> 'str': pass
class bytes: pass

class tuple: pass
class function: pass
Expand Down
Loading