diff --git a/mypy/semanal.py b/mypy/semanal.py index 4401183dbdf51..42d0f1c857547 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -43,6 +43,7 @@ traverse the entire AST. """ +import sys from typing import ( List, Dict, Set, Tuple, cast, Any, overload, TypeVar, Union, Optional, Callable ) @@ -64,7 +65,7 @@ SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr, YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr, IntExpr, FloatExpr, UnicodeExpr, - COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, + COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, ) from mypy.visitor import NodeVisitor from mypy.traverser import TraverserVisitor @@ -2781,17 +2782,22 @@ def infer_if_condition_value(expr: Node, pyversion: Tuple[int, int]) -> int: if alias.op == 'not': expr = alias.expr negated = True + result = TRUTH_VALUE_UNKNOWN if isinstance(expr, NameExpr): name = expr.name elif isinstance(expr, MemberExpr): name = expr.name - result = TRUTH_VALUE_UNKNOWN - if name == 'PY2': - result = ALWAYS_TRUE if pyversion[0] == 2 else ALWAYS_FALSE - elif name == 'PY3': - result = ALWAYS_TRUE if pyversion[0] == 3 else ALWAYS_FALSE - elif name == 'MYPY': - result = ALWAYS_TRUE + else: + result = consider_sys_version_info(expr, pyversion) + if result == TRUTH_VALUE_UNKNOWN: + result = consider_sys_platform(expr, sys.platform) + if result == TRUTH_VALUE_UNKNOWN: + if name == 'PY2': + result = ALWAYS_TRUE if pyversion[0] == 2 else ALWAYS_FALSE + elif name == 'PY3': + result = ALWAYS_TRUE if pyversion[0] == 3 else ALWAYS_FALSE + elif name == 'MYPY': + result = ALWAYS_TRUE if negated: if result == ALWAYS_TRUE: result = ALWAYS_FALSE @@ -2800,6 +2806,144 @@ def infer_if_condition_value(expr: Node, pyversion: Tuple[int, int]) -> int: return result +def consider_sys_version_info(expr: Node, pyversion: Tuple[int, ...]) -> int: + """Consider whether expr is a comparison involving sys.version_info. + + Return ALWAYS_TRUE, ALWAYS_FALSE, or TRUTH_VALUE_UNKNOWN. + """ + # Cases supported: + # - sys.version_info[] + # - sys.version_info[:] + # - sys.version_info + # (in this case must be >, >=, <, <=, but cannot be ==, !=) + if not isinstance(expr, ComparisonExpr): + return TRUTH_VALUE_UNKNOWN + # Let's not yet support chained comparisons. + if len(expr.operators) > 1: + return TRUTH_VALUE_UNKNOWN + op = expr.operators[0] + if op not in ('==', '!=', '<=', '>=', '<', '>'): + return TRUTH_VALUE_UNKNOWN + thing = contains_int_or_tuple_of_ints(expr.operands[1]) + if thing is None: + return TRUTH_VALUE_UNKNOWN + index = contains_sys_version_info(expr.operands[0]) + if isinstance(index, int) and isinstance(thing, int): + # sys.version_info[i] k + if 0 <= index <= 1: + return fixed_comparison(pyversion[index], op, thing) + else: + return TRUTH_VALUE_UNKNOWN + elif isinstance(index, tuple) and isinstance(thing, tuple): + # Why doesn't mypy see that index can't be None here? + lo, hi = cast(tuple, index) + if lo is None: + lo = 0 + if hi is None: + hi = 2 + if 0 <= lo < hi <= 2: + val = pyversion[lo:hi] + if len(val) == len(thing) or len(val) > len(thing) and op not in ('==', '!='): + return fixed_comparison(val, op, thing) + return TRUTH_VALUE_UNKNOWN + + +def consider_sys_platform(expr: Node, platform: str) -> int: + """Consider whether expr is a comparison involving sys.platform. + + Return ALWAYS_TRUE, ALWAYS_FALSE, or TRUTH_VALUE_UNKNOWN. + """ + # Cases supported: + # - sys.platform == 'posix' + # - sys.platform != 'win32' + # TODO: Maybe support e.g.: + # - sys.platform.startswith('win') + if not isinstance(expr, ComparisonExpr): + return TRUTH_VALUE_UNKNOWN + # Let's not yet support chained comparisons. + if len(expr.operators) > 1: + return TRUTH_VALUE_UNKNOWN + op = expr.operators[0] + if op not in ('==', '!='): + return TRUTH_VALUE_UNKNOWN + if not is_sys_attr(expr.operands[0], 'platform'): + return TRUTH_VALUE_UNKNOWN + right = expr.operands[1] + if not isinstance(right, (StrExpr, UnicodeExpr)): + return TRUTH_VALUE_UNKNOWN + return fixed_comparison(platform, op, right.value) + + +Targ = TypeVar('Targ', int, str, Tuple[int, ...]) + + +def fixed_comparison(left: Targ, op: str, right: Targ) -> int: + rmap = {False: ALWAYS_FALSE, True: ALWAYS_TRUE} + if op == '==': + return rmap[left == right] + if op == '!=': + return rmap[left != right] + if op == '<=': + return rmap[left <= right] + if op == '>=': + return rmap[left >= right] + if op == '<': + return rmap[left < right] + if op == '>': + return rmap[left > right] + return TRUTH_VALUE_UNKNOWN + + +def contains_int_or_tuple_of_ints(expr: Node) -> Union[None, int, Tuple[int], Tuple[int, ...]]: + if isinstance(expr, IntExpr): + return expr.value + if isinstance(expr, TupleExpr): + if expr.literal == LITERAL_YES: + thing = [] + for x in expr.items: + if not isinstance(x, IntExpr): + return None + thing.append(x.value) + return tuple(thing) + return None + + +def contains_sys_version_info(expr: Node) -> Union[None, int, Tuple[Optional[int], Optional[int]]]: + if is_sys_attr(expr, 'version_info'): + return (None, None) # Same as sys.version_info[:] + if isinstance(expr, IndexExpr) and is_sys_attr(expr.base, 'version_info'): + index = expr.index + if isinstance(index, IntExpr): + return index.value + if isinstance(index, SliceExpr): + if index.stride is not None: + if not isinstance(index.stride, IntExpr) or index.stride.value != 1: + return None + begin = end = None + if index.begin_index is not None: + if not isinstance(index.begin_index, IntExpr): + return None + begin = index.begin_index.value + if index.end_index is not None: + if not isinstance(index.end_index, IntExpr): + return None + end = index.end_index.value + return (begin, end) + return None + + +def is_sys_attr(expr: Node, name: str) -> bool: + # TODO: This currently doesn't work with code like this: + # - import sys as _sys + # - from sys import version_info + if isinstance(expr, MemberExpr) and expr.name == name: + if isinstance(expr.expr, NameExpr) and expr.expr.name == 'sys': + # TODO: Guard against a local named sys, etc. + # (Though later passes will still do most checking.) + return True + return False + + def mark_block_unreachable(block: Block) -> None: block.is_unreachable = True block.accept(MarkImportsUnreachableVisitor()) diff --git a/test-data/samples/crawl.py b/test-data/samples/crawl.py index 19c327f40979b..c7e8ab5497460 100644 --- a/test-data/samples/crawl.py +++ b/test-data/samples/crawl.py @@ -20,9 +20,10 @@ ARGS = argparse.ArgumentParser(description="Web crawler") -ARGS.add_argument( - '--iocp', action='store_true', dest='iocp', - default=False, help='Use IOCP event loop (Windows only)') +if sys.platform == 'win32': + ARGS.add_argument( + '--iocp', action='store_true', dest='iocp', + default=False, help='Use IOCP event loop (Windows only)') ARGS.add_argument( '--select', action='store_true', dest='select', default=False, help='Use Select event loop instead of default') @@ -824,9 +825,12 @@ def main() -> None: log = Logger(args.level) if args.iocp: - from asyncio import ProactorEventLoop - loop = ProactorEventLoop() # type: ignore - asyncio.set_event_loop(loop) + if sys.platform == 'win32': + from asyncio import ProactorEventLoop + loop = ProactorEventLoop() # type: ignore + asyncio.set_event_loop(loop) + else: + assert False elif args.select: loop = asyncio.SelectorEventLoop() # type: ignore asyncio.set_event_loop(loop) diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index c1b74835ebac5..1668aeae87134 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -111,3 +111,233 @@ else: import bad_name 1 + '' [out] + +[case testSysVersionInfo_python2] +import sys +if sys.version_info[0] >= 3: + def foo() -> int: return 0 +else: + def foo() -> str: return '' +reveal_type(foo()) # E: Revealed type is 'builtins.str' +[builtins_py2 fixtures/ops.py] +[out] + +[case testSysVersionInfo] +import sys +if sys.version_info[0] >= 3: + def foo() -> int: return 0 +else: + def foo() -> str: return '' +reveal_type(foo()) # E: Revealed type is 'builtins.int' +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoNegated_python2] +import sys +if not (sys.version_info[0] < 3): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +reveal_type(foo()) # E: Revealed type is 'builtins.str' +[builtins_py2 fixtures/ops.py] +[out] + +[case testSysVersionInfoNegated] +import sys +if not (sys.version_info[0] < 3): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +reveal_type(foo()) # E: Revealed type is 'builtins.int' +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced1] +import sys +if sys.version_info[:1] >= (3,): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced2] +import sys +if sys.version_info[:2] >= (3, 0): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced3] +import sys +if sys.version_info[:] >= (3, 0): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced4] +import sys +if sys.version_info[0:2] >= (3, 0): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced5] +import sys +if sys.version_info[0:] >= (3,): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced6] +import sys +if sys.version_info[1:] >= (5,): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced7] +import sys +if sys.version_info >= (3, 5): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced8] +# Our pyversion only has (major, minor), +# so testing for (major, minor, bugfix) is unsupported. +import sys +if sys.version_info >= (3, 5, 0): + def foo() -> int: return 0 +else: + def foo() -> str: return '' # E: All conditional function variants must have identical signatures +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoSliced9] +# Our pyversion only has (major, minor), +# so testing for (minor, bugfix) is unsupported (also it's silly :-). +import sys +if sys.version_info[1:] >= (5, 0): + def foo() -> int: return 0 +else: + def foo() -> str: return '' # E: All conditional function variants must have identical signatures +[builtins fixtures/ops.py] +[out] + +[case testSysPlatform1] +import sys +if sys.platform == 'fictional': + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + '' +[builtins fixtures/ops.py] +[out] + +[case testSysPlatform2] +import sys +if sys.platform != 'fictional': + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysPlatformNegated] +import sys +if not (sys.platform == 'fictional'): + def foo() -> int: return 0 +else: + def foo() -> str: return '' +foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoClass] +import sys +if sys.version_info < (3, 5): + class C: + pass +else: + class C: + def foo(self) -> int: return 0 +C().foo() + 0 +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoImport] +import sys +if sys.version_info >= (3, 5): + import collections +else: + collections = None +Pt = collections.namedtuple('Pt', 'x y z') +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoVariable] +import sys +if sys.version_info >= (3, 5): + x = '' +else: + x = 0 +x + '' +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoInClass] +import sys +class C: + if sys.version_info >= (3, 5): + def foo(self) -> int: return 0 + else: + def foo(self) -> str: return '' +reveal_type(C().foo()) # E: Revealed type is 'builtins.int' +[builtins fixtures/ops.py] +[out] + +[case testSysVersionInfoInFunction] +import sys +def foo() -> None: + if sys.version_info >= (3, 5): + x = '' + else: + x = 0 + reveal_type(x) # E: Revealed type is 'builtins.str' +[builtins fixtures/ops.py] +[out] +main: note: In function "foo": + +[case testSysVersionInfoInMethod] +import sys +class C: + def foo(self) -> None: + if sys.platform != 'fictional': + x = '' + else: + x = 0 + reveal_type(x) # E: Revealed type is 'builtins.str' +[builtins fixtures/ops.py] +[out] +main: note: In member "foo" of class "C": diff --git a/test-data/unit/fixtures/ops.py b/test-data/unit/fixtures/ops.py index 7d99aa1ef71e9..b19c37be6cd96 100644 --- a/test-data/unit/fixtures/ops.py +++ b/test-data/unit/fixtures/ops.py @@ -1,4 +1,6 @@ -from typing import builtinclass +from typing import builtinclass, overload, Any, Generic, Sequence, TypeVar + +Tco = TypeVar('Tco', covariant=True) # This is an extension of transform builtins with additional operations. @@ -9,7 +11,18 @@ def __eq__(self, o: 'object') -> 'bool': pass def __ne__(self, o: 'object') -> 'bool': pass class type: pass -class tuple: pass + +class slice: pass + +class tuple(Sequence[Tco], Generic[Tco]): + def __getitem__(self, x: int) -> Tco: pass + def __eq__(self, x: object) -> bool: pass + def __ne__(self, x: object) -> bool: pass + def __lt__(self, x: 'tuple') -> bool: pass + def __le__(self, x: 'tuple') -> bool: pass + def __gt__(self, x: 'tuple') -> bool: pass + def __ge__(self, x: 'tuple') -> bool: pass + class function: pass class bool: pass diff --git a/test-data/unit/lib-stub/sys.py b/test-data/unit/lib-stub/sys.py new file mode 100644 index 0000000000000..3959cb035ca15 --- /dev/null +++ b/test-data/unit/lib-stub/sys.py @@ -0,0 +1,2 @@ +version_info = (0, 0, 0, '', 0) +platform = ''