Skip to content

Commit e602da8

Browse files
Michael0x2agvanrossum
authored andcommitted
Add new --platform flag (#2045)
* Add cmd line flag to let you set the OS version This adds a new flag allowing you to set what the expected value of sys.platform is for this particular run. It is similar to the --python-version flag, which lets you control what the expected value of sys.version_info for this run is. * Support sys.platform.startswith(...) checks Previously, mypy only took into account sys.platform of the form `sys.platform == ???` or `sys.platform != ???`. This adds support for `sys.platform.startswith(???)` checks. This will probably be helpful when writing Python2/Python3-straddling code. For example, Python 2, sys.platform returns 'linux2' on Linux systems, but in Python 3, sys.platform will return just 'linux'. Fixes #1988.
1 parent 8d0e86d commit e602da8

File tree

7 files changed

+133
-38
lines changed

7 files changed

+133
-38
lines changed

docs/source/command_line.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ summary of command line flags can always be printed using the ``-h``
88
flag (or its long form ``--help``)::
99

1010
$ mypy -h
11-
usage: mypy [-h] [-v] [-V] [--python-version x.y] [--py2] [-s] [--silent]
12-
[--almost-silent] [--disallow-untyped-calls]
13-
[--disallow-untyped-defs] [--check-untyped-defs]
11+
usage: mypy [-h] [-v] [-V] [--python-version x.y] [--platform PLATFORM]
12+
[--py2] [-s] [--silent] [--almost-silent]
13+
[--disallow-untyped-calls] [--disallow-untyped-defs]
14+
[--check-untyped-defs]
1415
[--warn-incomplete-stub] [--warn-redundant-casts]
1516
[--warn-unused-ignores] [--fast-parser] [-i] [--cache-dir DIR]
1617
[--strict-optional] [-f] [--pdb] [--use-python-path] [--stats]

mypy/main.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ def __getattr__(self, name: str) -> Any:
107107
return getattr(self._standard_namespace, name)
108108

109109

110+
def parse_version(v: str) -> Tuple[int, int]:
111+
m = re.match(r'\A(\d)\.(\d+)\Z', v)
112+
if m:
113+
return int(m.group(1)), int(m.group(2))
114+
else:
115+
raise argparse.ArgumentTypeError(
116+
"Invalid python version '{}' (expected format: 'x.y')".format(v))
117+
118+
110119
def process_options(args: List[str]) -> Tuple[List[BuildSource], Options]:
111120
"""Process command line arguments.
112121
@@ -121,14 +130,6 @@ def process_options(args: List[str]) -> Tuple[List[BuildSource], Options]:
121130
parser = argparse.ArgumentParser(prog='mypy', epilog=FOOTER,
122131
formatter_class=help_factory)
123132

124-
def parse_version(v: str) -> Tuple[int, int]:
125-
m = re.match(r'\A(\d)\.(\d+)\Z', v)
126-
if m:
127-
return int(m.group(1)), int(m.group(2))
128-
else:
129-
raise argparse.ArgumentTypeError(
130-
"Invalid python version '{}' (expected format: 'x.y')".format(v))
131-
132133
# Unless otherwise specified, arguments will be parsed directly onto an
133134
# Options object. Options that require further processing should have
134135
# their `dest` prefixed with `special-opts:`, which will cause them to be
@@ -139,6 +140,9 @@ def parse_version(v: str) -> Tuple[int, int]:
139140
version='%(prog)s ' + __version__)
140141
parser.add_argument('--python-version', type=parse_version, metavar='x.y',
141142
help='use Python x.y')
143+
parser.add_argument('--platform', action='store', metavar='PLATFORM',
144+
help="typecheck special-cased code for the given OS platform "
145+
"(defaults to sys.platform).")
142146
parser.add_argument('-2', '--py2', dest='python_version', action='store_const',
143147
const=defaults.PYTHON2_VERSION, help="use Python 2 mode")
144148
parser.add_argument('-s', '--silent-imports', action='store_true',

mypy/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from mypy import defaults
22
import pprint
3+
import sys
34
from typing import Any
45

56

@@ -16,6 +17,7 @@ def __init__(self) -> None:
1617
# -- build options --
1718
self.build_type = BuildType.STANDARD
1819
self.python_version = defaults.PYTHON3_VERSION
20+
self.platform = sys.platform
1921
self.custom_typing_module = None # type: str
2022
self.report_dirs = {} # type: Dict[str, str]
2123
self.silent_imports = False

mypy/semanal.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,7 +1819,9 @@ def visit_continue_stmt(self, s: ContinueStmt) -> None:
18191819
self.fail("'continue' outside loop", s, True, blocker=True)
18201820

18211821
def visit_if_stmt(self, s: IfStmt) -> None:
1822-
infer_reachability_of_if_statement(s, pyversion=self.options.python_version)
1822+
infer_reachability_of_if_statement(s,
1823+
pyversion=self.options.python_version,
1824+
platform=self.options.platform)
18231825
for i in range(len(s.expr)):
18241826
s.expr[i].accept(self)
18251827
self.visit_block(s.body[i])
@@ -2473,6 +2475,7 @@ class FirstPass(NodeVisitor):
24732475
def __init__(self, sem: SemanticAnalyzer) -> None:
24742476
self.sem = sem
24752477
self.pyversion = sem.options.python_version
2478+
self.platform = sem.options.platform
24762479

24772480
def analyze(self, file: MypyFile, fnam: str, mod_id: str) -> None:
24782481
"""Perform the first analysis pass.
@@ -2641,7 +2644,7 @@ def visit_decorator(self, d: Decorator) -> None:
26412644
self.sem.add_symbol(d.var.name(), SymbolTableNode(GDEF, d.var), d)
26422645

26432646
def visit_if_stmt(self, s: IfStmt) -> None:
2644-
infer_reachability_of_if_statement(s, pyversion=self.pyversion)
2647+
infer_reachability_of_if_statement(s, pyversion=self.pyversion, platform=self.platform)
26452648
for node in s.body:
26462649
node.accept(self)
26472650
if s.else_body:
@@ -2878,9 +2881,10 @@ def remove_imported_names_from_symtable(names: SymbolTable,
28782881

28792882

28802883
def infer_reachability_of_if_statement(s: IfStmt,
2881-
pyversion: Tuple[int, int]) -> None:
2884+
pyversion: Tuple[int, int],
2885+
platform: str) -> None:
28822886
for i in range(len(s.expr)):
2883-
result = infer_if_condition_value(s.expr[i], pyversion)
2887+
result = infer_if_condition_value(s.expr[i], pyversion, platform)
28842888
if result == ALWAYS_FALSE:
28852889
# The condition is always false, so we skip the if/elif body.
28862890
mark_block_unreachable(s.body[i])
@@ -2894,7 +2898,7 @@ def infer_reachability_of_if_statement(s: IfStmt,
28942898
break
28952899

28962900

2897-
def infer_if_condition_value(expr: Node, pyversion: Tuple[int, int]) -> int:
2901+
def infer_if_condition_value(expr: Node, pyversion: Tuple[int, int], platform: str) -> int:
28982902
"""Infer whether if condition is always true/false.
28992903
29002904
Return ALWAYS_TRUE if always true, ALWAYS_FALSE if always false,
@@ -2915,7 +2919,7 @@ def infer_if_condition_value(expr: Node, pyversion: Tuple[int, int]) -> int:
29152919
else:
29162920
result = consider_sys_version_info(expr, pyversion)
29172921
if result == TRUTH_VALUE_UNKNOWN:
2918-
result = consider_sys_platform(expr, sys.platform)
2922+
result = consider_sys_platform(expr, platform)
29192923
if result == TRUTH_VALUE_UNKNOWN:
29202924
if name == 'PY2':
29212925
result = ALWAYS_TRUE if pyversion[0] == 2 else ALWAYS_FALSE
@@ -2981,22 +2985,35 @@ def consider_sys_platform(expr: Node, platform: str) -> int:
29812985
# Cases supported:
29822986
# - sys.platform == 'posix'
29832987
# - sys.platform != 'win32'
2984-
# TODO: Maybe support e.g.:
29852988
# - sys.platform.startswith('win')
2986-
if not isinstance(expr, ComparisonExpr):
2987-
return TRUTH_VALUE_UNKNOWN
2988-
# Let's not yet support chained comparisons.
2989-
if len(expr.operators) > 1:
2990-
return TRUTH_VALUE_UNKNOWN
2991-
op = expr.operators[0]
2992-
if op not in ('==', '!='):
2993-
return TRUTH_VALUE_UNKNOWN
2994-
if not is_sys_attr(expr.operands[0], 'platform'):
2995-
return TRUTH_VALUE_UNKNOWN
2996-
right = expr.operands[1]
2997-
if not isinstance(right, (StrExpr, UnicodeExpr)):
2989+
if isinstance(expr, ComparisonExpr):
2990+
# Let's not yet support chained comparisons.
2991+
if len(expr.operators) > 1:
2992+
return TRUTH_VALUE_UNKNOWN
2993+
op = expr.operators[0]
2994+
if op not in ('==', '!='):
2995+
return TRUTH_VALUE_UNKNOWN
2996+
if not is_sys_attr(expr.operands[0], 'platform'):
2997+
return TRUTH_VALUE_UNKNOWN
2998+
right = expr.operands[1]
2999+
if not isinstance(right, (StrExpr, UnicodeExpr)):
3000+
return TRUTH_VALUE_UNKNOWN
3001+
return fixed_comparison(platform, op, right.value)
3002+
elif isinstance(expr, CallExpr):
3003+
if not isinstance(expr.callee, MemberExpr):
3004+
return TRUTH_VALUE_UNKNOWN
3005+
if len(expr.args) != 1 or not isinstance(expr.args[0], (StrExpr, UnicodeExpr)):
3006+
return TRUTH_VALUE_UNKNOWN
3007+
if not is_sys_attr(expr.callee.expr, 'platform'):
3008+
return TRUTH_VALUE_UNKNOWN
3009+
if expr.callee.name != 'startswith':
3010+
return TRUTH_VALUE_UNKNOWN
3011+
if platform.startswith(expr.args[0].value):
3012+
return ALWAYS_TRUE
3013+
else:
3014+
return ALWAYS_FALSE
3015+
else:
29983016
return TRUTH_VALUE_UNKNOWN
2999-
return fixed_comparison(platform, op, right.value)
30003017

30013018

30023019
Targ = TypeVar('Targ', int, str, Tuple[int, ...])

mypy/test/testcheck.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Tuple, List, Dict, Set
1010

1111
from mypy import build, defaults
12+
from mypy.main import parse_version
1213
from mypy.build import BuildSource, find_module_clear_caches
1314
from mypy.myunit import AssertionFailure
1415
from mypy.test.config import test_temp_dir, test_data_prefix
@@ -109,9 +110,8 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental=0) -> None:
109110
original_program_text = '\n'.join(testcase.input)
110111
module_data = self.parse_module(original_program_text, incremental)
111112

112-
options = self.parse_options(original_program_text)
113+
options = self.parse_options(original_program_text, testcase)
113114
options.use_builtins_fixtures = True
114-
options.python_version = testcase_pyversion(testcase.file, testcase.name)
115115
set_show_tb(True) # Show traceback on crash.
116116

117117
output = testcase.output
@@ -276,11 +276,24 @@ def parse_module(self, program_text: str, incremental: int = 0) -> List[Tuple[st
276276
else:
277277
return [('__main__', 'main', program_text)]
278278

279-
def parse_options(self, program_text: str) -> Options:
279+
def parse_options(self, program_text: str, testcase: DataDrivenTestCase) -> Options:
280280
options = Options()
281-
m = re.search('# options: (.*)$', program_text, flags=re.MULTILINE)
282-
if m:
283-
options_to_enable = m.group(1).split()
281+
flags = re.search('# options: (.*)$', program_text, flags=re.MULTILINE)
282+
version_flag = re.search('# pyversion: (.*)$', program_text, flags=re.MULTILINE)
283+
platform_flag = re.search('# platform: (.*)$', program_text, flags=re.MULTILINE)
284+
285+
if flags:
286+
options_to_enable = flags.group(1).split()
284287
for opt in options_to_enable:
285288
setattr(options, opt, True)
289+
290+
# Allow custom pyversion comment to override testcase_pyversion
291+
if version_flag:
292+
options.python_version = parse_version(version_flag.group(1))
293+
else:
294+
options.python_version = testcase_pyversion(testcase.file, testcase.name)
295+
296+
if platform_flag:
297+
options.platform = platform_flag.group(1)
298+
286299
return options

test-data/unit/check-unreachable-code.test

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,58 @@ class C:
341341
[builtins fixtures/ops.pyi]
342342
[out]
343343
main: note: In member "foo" of class "C":
344+
345+
[case testCustomSysVersionInfo]
346+
# pyversion: 3.2
347+
import sys
348+
if sys.version_info == (3, 2):
349+
x = "foo"
350+
else:
351+
x = 3
352+
reveal_type(x) # E: Revealed type is 'builtins.str'
353+
[builtins fixtures/ops.pyi]
354+
[out]
355+
356+
[case testCustomSysVersionInfo2]
357+
# pyversion: 3.1
358+
import sys
359+
if sys.version_info == (3, 2):
360+
x = "foo"
361+
else:
362+
x = 3
363+
reveal_type(x) # E: Revealed type is 'builtins.int'
364+
[builtins fixtures/ops.pyi]
365+
[out]
366+
367+
[case testCustomSysPlatform]
368+
# platform: linux
369+
import sys
370+
if sys.platform == 'linux':
371+
x = "foo"
372+
else:
373+
x = 3
374+
reveal_type(x) # E: Revealed type is 'builtins.str'
375+
[builtins fixtures/ops.pyi]
376+
[out]
377+
378+
[case testCustomSysPlatform2]
379+
# platform: win32
380+
import sys
381+
if sys.platform == 'linux':
382+
x = "foo"
383+
else:
384+
x = 3
385+
reveal_type(x) # E: Revealed type is 'builtins.int'
386+
[builtins fixtures/ops.pyi]
387+
[out]
388+
389+
[case testCustomSysPlatformStartsWith]
390+
# platform: win32
391+
import sys
392+
if sys.platform.startswith('win'):
393+
x = "foo"
394+
else:
395+
x = 3
396+
reveal_type(x) # E: Revealed type is 'builtins.str'
397+
[builtins fixtures/ops.pyi]
398+
[out]

test-data/unit/fixtures/ops.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import builtinclass, overload, Any, Generic, Sequence, TypeVar
1+
from typing import builtinclass, overload, Any, Generic, Sequence, Tuple, TypeVar
22

33
Tco = TypeVar('Tco', covariant=True)
44

@@ -30,6 +30,7 @@ class bool: pass
3030
class str:
3131
def __init__(self, x: 'int') -> None: pass
3232
def __add__(self, x: 'str') -> 'str': pass
33+
def startswith(self, x: 'str') -> bool: pass
3334

3435
class int:
3536
def __add__(self, x: 'int') -> 'int': pass
@@ -54,3 +55,5 @@ True = None # type: bool
5455
False = None # type: bool
5556

5657
def __print(a1=None, a2=None, a3=None, a4=None): pass
58+
59+
class module: pass

0 commit comments

Comments
 (0)