diff --git a/mypy/build.py b/mypy/build.py index b204c43bd1f4..c620ca838daf 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -26,6 +26,7 @@ from mypy.errors import Errors, CompileError from mypy import parse from mypy import stats +from mypy import defaults debug = False @@ -86,7 +87,7 @@ def build(program_path: str, program_text: Union[str, bytes] = None, alt_lib_path: str = None, bin_dir: str = None, - pyversion: int = 3, + pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION, custom_typing_module: str = None, html_report_dir: str = None, flags: List[str] = None, @@ -108,7 +109,7 @@ def build(program_path: str, (takes precedence over other directories) bin_dir: directory containing the mypy script, used for finding data directories; if omitted, use '.' as the data directory - pyversion: Python version (2 for 2.x or 3 for 3.x) + pyversion: Python version (major, minor) custom_typing_module: if not None, use this module id as an alias for typing flags: list of build options (e.g. COMPILE_ONLY) """ @@ -187,7 +188,7 @@ def default_data_dir(bin_dir: str) -> str: raise RuntimeError("Broken installation: can't determine base dir") -def default_lib_path(data_dir: str, target: int, pyversion: int, +def default_lib_path(data_dir: str, target: int, pyversion: Tuple[int, int], python_path: bool) -> List[str]: """Return default standard library search paths.""" # IDEA: Make this more portable. @@ -202,7 +203,7 @@ def default_lib_path(data_dir: str, target: int, pyversion: int, # stubs/x.y directory of the mypy installation. version_dir = '3.2' third_party_dir = 'third-party-3.2' - if pyversion < 3: + if pyversion[0] < 3: version_dir = '2.7' third_party_dir = 'third-party-2.7' path.append(os.path.join(data_dir, 'stubs', version_dir)) @@ -268,7 +269,7 @@ class BuildManager: Semantic analyzer, pass 3 type_checker: Type checker errors: Used for reporting all errors - pyversion: Python version (2 or 3) + pyversion: Python version (major, minor) flags: Build options states: States of all individual files that are being processed. Each file in a build is always represented @@ -286,7 +287,7 @@ class BuildManager: def __init__(self, data_dir: str, lib_path: List[str], target: int, - pyversion: int, + pyversion: Tuple[int, int], flags: List[str], ignore_prefix: str, custom_typing_module: str, diff --git a/mypy/checker.py b/mypy/checker.py index 4a20dbe6f963..7090f1a8663a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -30,6 +30,7 @@ from mypy.sametypes import is_same_type from mypy.messages import MessageBuilder import mypy.checkexpr +from mypy import defaults from mypy import messages from mypy.subtypes import ( is_subtype, is_equivalent, is_proper_subtype, @@ -302,8 +303,8 @@ class TypeChecker(NodeVisitor[Type]): Type check mypy source files that have been semantically analyzed. """ - # Target Python major version - pyversion = 3 + # Target Python version + pyversion = defaults.PYTHON3_VERSION # Are we type checking a stub? is_stub = False # Error message reporter @@ -338,7 +339,7 @@ class TypeChecker(NodeVisitor[Type]): modules = None # type: Dict[str, MypyFile] def __init__(self, errors: Errors, modules: Dict[str, MypyFile], - pyversion: int = 3) -> None: + pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION) -> None: """Construct a type checker. Use errors to report type check errors. Assume symtable has been @@ -1530,7 +1531,7 @@ def type_check_raise(self, e: Node, s: RaiseStmt) -> None: # Good! return None # Else fall back to the checks below (which will fail). - if isinstance(typ, TupleType) and self.pyversion == 2: + if isinstance(typ, TupleType) and self.pyversion[0] == 2: # allow `raise type, value, traceback` # https://docs.python.org/2/reference/simple_stmts.html#the-raise-statement # TODO: Also check tuple item types. @@ -1642,7 +1643,7 @@ def analyze_iterable_item_type(self, expr: Node) -> Type: method = echk.analyze_external_member_access('__iter__', iterable, expr) iterator = echk.check_call(method, [], [], expr)[0] - if self.pyversion >= 3: + if self.pyversion[0] >= 3: nextmethod = '__next__' else: nextmethod = 'next' diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index cc563cf562af..1fb2b0bfefc4 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -727,7 +727,7 @@ def visit_complex_expr(self, e: ComplexExpr) -> Type: def visit_ellipsis(self, e: EllipsisExpr) -> Type: """Type check '...'.""" - if self.chk.pyversion >= 3: + if self.chk.pyversion[0] >= 3: return self.named_type('builtins.ellipsis') else: # '...' is not valid in normal Python 2 code, but it can @@ -821,7 +821,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: return result def get_operator_method(self, op: str) -> str: - if op == '/' and self.chk.pyversion == 2: + if op == '/' and self.chk.pyversion[0] == 2: # TODO also check for "from __future__ import division" return '__div__' else: @@ -893,7 +893,7 @@ def check_op(self, method: str, base_type: Type, arg: Node, self.msg) def get_reverse_op_method(self, method: str) -> str: - if method == '__div__' and self.chk.pyversion == 2: + if method == '__div__' and self.chk.pyversion[0] == 2: return '__rdiv__' else: return nodes.reverse_op_methods[method] diff --git a/mypy/defaults.py b/mypy/defaults.py new file mode 100644 index 000000000000..5a0875f525e2 --- /dev/null +++ b/mypy/defaults.py @@ -0,0 +1,2 @@ +PYTHON2_VERSION = (2, 7) +PYTHON3_VERSION = (3, 5) diff --git a/mypy/lex.py b/mypy/lex.py index 484b901928ed..56f8c79afbb8 100644 --- a/mypy/lex.py +++ b/mypy/lex.py @@ -9,6 +9,7 @@ import re from mypy.util import short_type +from mypy import defaults from typing import List, Callable, Dict, Any, Match, Pattern, Set, Union, Tuple @@ -158,7 +159,8 @@ def __str__(self): def lex(string: Union[str, bytes], first_line: int = 1, - pyversion: int = 3, is_stub_file: bool = False) -> Tuple[List[Token], Set[int]]: + pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION, + is_stub_file: bool = False) -> Tuple[List[Token], Set[int]]: """Analyze string, and return an array of token objects and the lines to ignore. The last token is always Eof. The intention is to ignore any @@ -291,12 +293,13 @@ class Lexer: # newlines within parentheses/brackets. open_brackets = None # type: List[str] - pyversion = 3 + pyversion = defaults.PYTHON3_VERSION # Ignore errors on these lines (defined using '# type: ignore'). ignored_lines = None # type: Set[int] - def __init__(self, pyversion: int = 3, is_stub_file: bool = False) -> None: + def __init__(self, pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION, + is_stub_file: bool = False) -> None: self.map = [self.unknown_character] * 256 self.tok = [] self.indents = [0] @@ -322,12 +325,12 @@ def __init__(self, pyversion: int = 3, is_stub_file: bool = False) -> None: ('-+*/<>%&|^~=!,@', self.lex_misc)]: for c in seq: self.map[ord(c)] = method - if pyversion == 2: + if pyversion[0] == 2: self.keywords = keywords_common | keywords2 # Decimal/hex/octal/binary literal or integer complex literal self.number_exp1 = re.compile('(0[xXoObB][0-9a-fA-F]+|[0-9]+)[lL]?') - if pyversion == 3: + if pyversion[0] == 3: self.keywords = keywords_common | keywords3 self.number_exp1 = re.compile('0[xXoObB][0-9a-fA-F]+|[0-9]+') @@ -394,7 +397,7 @@ def find_encoding(self, text: bytes) -> Tuple[str, int]: line = 2 if result.group(1) else 1 return result.group(3).decode('ascii'), line else: - default_encoding = 'utf8' if self.pyversion >= 3 else 'ascii' + default_encoding = 'utf8' if self.pyversion[0] >= 3 else 'ascii' return default_encoding, -1 def report_unicode_decode_error(self, exc: UnicodeDecodeError, text: bytes) -> None: @@ -484,7 +487,7 @@ def lex_number(self) -> None: self.add_token(LexError(' ' * maxlen, NUMERIC_LITERAL_ERROR)) elif len(s1) == maxlen: # Integer literal. - if self.pyversion >= 3 and self.octal_int.match(s1): + if self.pyversion[0] >= 3 and self.octal_int.match(s1): # Python 2 style octal literal such as 0377 not supported in Python 3. self.add_token(LexError(s1, NUMERIC_LITERAL_ERROR)) else: diff --git a/mypy/parse.py b/mypy/parse.py index fcf7946b5354..a6ac4184db7d 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -28,6 +28,7 @@ StarExpr, YieldFromStmt, YieldFromExpr, NonlocalDecl, DictionaryComprehension, SetComprehension, ComplexExpr, EllipsisExpr, YieldExpr, ExecStmt ) +from mypy import defaults from mypy import nodes from mypy.errors import Errors, CompileError from mypy.types import Void, Type, CallableType, AnyType, UnboundType @@ -67,14 +68,14 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None, - pyversion: int = 3, custom_typing_module: str = None) -> MypyFile: + pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION, + custom_typing_module: str = None) -> MypyFile: """Parse a source file, without doing any semantic analysis. Return the parse tree. If errors is not provided, raise ParseError on failure. Otherwise, use the errors object to report parse errors. - The pyversion argument determines the Python syntax variant (2 for 2.x and - 3 for 3.x). + The pyversion (major, minor) argument determines the Python syntax variant. """ is_stub_file = bool(fnam) and fnam.endswith('.pyi') parser = Parser(fnam, errors, pyversion, custom_typing_module, is_stub_file=is_stub_file) @@ -108,7 +109,7 @@ class Parser: # Lines to ignore (using # type: ignore). ignored_lines = None # type: Set[int] - def __init__(self, fnam: str, errors: Errors, pyversion: int, + def __init__(self, fnam: str, errors: Errors, pyversion: Tuple[int, int], custom_typing_module: str = None, is_stub_file: bool = False) -> None: self.raise_on_error = errors is None self.pyversion = pyversion @@ -761,7 +762,7 @@ def parse_statement(self) -> Tuple[Node, bool]: is_simple = False elif ts == 'global': stmt = self.parse_global_decl() - elif ts == 'nonlocal' and self.pyversion >= 3: + elif ts == 'nonlocal' and self.pyversion[0] >= 3: stmt = self.parse_nonlocal_decl() elif ts == 'assert': stmt = self.parse_assert_stmt() @@ -775,10 +776,10 @@ def parse_statement(self) -> Tuple[Node, bool]: elif ts == '@': stmt = self.parse_decorated_function_or_class() is_simple = False - elif ts == 'print' and (self.pyversion == 2 and + elif ts == 'print' and (self.pyversion[0] == 2 and 'print_function' not in self.future_options): stmt = self.parse_print_stmt() - elif ts == 'exec' and self.pyversion == 2: + elif ts == 'exec' and self.pyversion[0] == 2: stmt = self.parse_exec_stmt() else: stmt = self.parse_expression_or_assignment() @@ -1033,7 +1034,7 @@ def parse_try_stmt(self) -> Node: self.expect('as') vars.append(self.parse_name_expr()) else: - if (self.pyversion == 2 and + if (self.pyversion[0] == 2 and isinstance(types[-1], TupleExpr) and len(cast(TupleExpr, types[-1]).items) == 2 and isinstance(cast(TupleExpr, types[-1]).items[1], NameExpr)): @@ -1166,7 +1167,8 @@ def parse_expression(self, prec: int = 0, star_expr_allowed: bool = False) -> No elif isinstance(current, Keyword) and s == "yield": # The expression yield from and yield to assign expr = self.parse_yield_or_yield_from_expr() - elif isinstance(current, EllipsisToken) and (self.pyversion >= 3 or self.is_stub_file): + elif isinstance(current, EllipsisToken) and (self.pyversion[0] >= 3 + or self.is_stub_file): expr = self.parse_ellipsis() else: # Invalid expression. @@ -1417,7 +1419,7 @@ def parse_str_expr(self) -> Node: tok.append(t) value += t.parsed() node = None # type: Node - if self.pyversion == 2 and 'unicode_literals' in self.future_options: + if self.pyversion[0] == 2 and 'unicode_literals' in self.future_options: node = UnicodeExpr(value) else: node = StrExpr(value) @@ -1430,7 +1432,7 @@ def parse_bytes_literal(self) -> Node: while isinstance(self.current(), BytesLit): t = cast(BytesLit, self.skip()) value += t.parsed() - if self.pyversion >= 3: + if self.pyversion[0] >= 3: node = BytesExpr(value) # type: Node else: node = StrExpr(value) @@ -1443,7 +1445,7 @@ def parse_unicode_literal(self) -> Node: while isinstance(self.current(), UnicodeLit): t = cast(UnicodeLit, self.skip()) value += t.parsed() - if self.pyversion >= 3: + if self.pyversion[0] >= 3: # Python 3.3 supports u'...' as an alias of '...'. node = StrExpr(value) # type: Node else: @@ -1833,11 +1835,11 @@ def usage(): sys.exit(2) args = sys.argv[1:] - pyversion = 3 + pyversion = defaults.PYTHON3_VERSION quiet = False while args and args[0].startswith('--'): if args[0] == '--py2': - pyversion = 2 + pyversion = defaults.PYTHON2_VERSION elif args[0] == '--quiet': quiet = True else: diff --git a/mypy/semanal.py b/mypy/semanal.py index 2d073c4b0542..22e1467dfe3b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -75,6 +75,7 @@ from mypy.lex import lex from mypy.parsetype import parse_type from mypy.sametypes import is_same_type +from mypy import defaults T = TypeVar('T') @@ -163,7 +164,8 @@ class SemanticAnalyzer(NodeVisitor): imports = None # type: Set[str] # Imported modules (during phase 2 analysis) errors = None # type: Errors # Keeps track of generated errors - def __init__(self, lib_path: List[str], errors: Errors, pyversion: int = 3) -> None: + def __init__(self, lib_path: List[str], errors: Errors, + pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION) -> None: """Construct semantic analyzer. Use lib_path to search for modules, and report analysis errors @@ -534,7 +536,7 @@ def setup_type_promotion(self, defn: ClassDef) -> None: # _promote class decorator (undocumented faeture). promote_target = analyzed.type if not promote_target: - promotions = (TYPE_PROMOTIONS_PYTHON3 if self.pyversion >= 3 + promotions = (TYPE_PROMOTIONS_PYTHON3 if self.pyversion[0] >= 3 else TYPE_PROMOTIONS_PYTHON2) if defn.fullname in promotions: promote_target = self.named_type_or_none(promotions[defn.fullname]) @@ -2227,7 +2229,8 @@ def remove_imported_names_from_symtable(names: SymbolTable, del names[name] -def infer_reachability_of_if_statement(s: IfStmt, pyversion: int) -> None: +def infer_reachability_of_if_statement(s: IfStmt, + pyversion: Tuple[int, int]) -> None: for i in range(len(s.expr)): result = infer_if_condition_value(s.expr[i], pyversion) if result == ALWAYS_FALSE: @@ -2243,7 +2246,7 @@ def infer_reachability_of_if_statement(s: IfStmt, pyversion: int) -> None: break -def infer_if_condition_value(expr: Node, pyversion: int) -> int: +def infer_if_condition_value(expr: Node, pyversion: Tuple[int, int]) -> int: """Infer whether if condition is always true/false. Return ALWAYS_TRUE if always true, ALWAYS_FALSE if always false, @@ -2262,9 +2265,9 @@ def infer_if_condition_value(expr: Node, pyversion: int) -> int: name = expr.name result = TRUTH_VALUE_UNKNOWN if name == 'PY2': - result = ALWAYS_TRUE if pyversion == 2 else ALWAYS_FALSE + result = ALWAYS_TRUE if pyversion[0] == 2 else ALWAYS_FALSE elif name == 'PY3': - result = ALWAYS_TRUE if pyversion == 3 else ALWAYS_FALSE + result = ALWAYS_TRUE if pyversion[0] == 3 else ALWAYS_FALSE elif name == 'MYPY': result = ALWAYS_TRUE if negated: diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 74e19d94a8b1..f798e1bb418d 100644 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -39,6 +39,7 @@ import mypy.parse import mypy.errors import mypy.traverser +from mypy import defaults from mypy.nodes import ( IntExpr, UnaryExpr, StrExpr, BytesExpr, NameExpr, FloatExpr, MemberExpr, TupleExpr, ListExpr, ComparisonExpr, CallExpr, ClassDef, ARG_STAR, ARG_STAR2, ARG_NAMED @@ -48,7 +49,7 @@ def generate_stub(path, output_dir, _all_=None, target=None, add_header=False, module=None, - pyversion=3): + pyversion=defaults.PYTHON3_VERSION): source = open(path, 'rb').read() try: ast = mypy.parse.parse(source, fnam=path, pyversion=pyversion) @@ -72,8 +73,8 @@ def generate_stub(path, output_dir, _all_=None, target=None, add_header=False, m def generate_stub_for_module(module, output_dir, quiet=False, add_header=False, sigs={}, - class_sigs={}, pyversion=3): - if pyversion == 2: + class_sigs={}, pyversion=defaults.PYTHON3_VERSION): + if pyversion[0] == 2: module_path, module_all = load_python2_module_info(module) else: mod = importlib.import_module(module) @@ -469,7 +470,7 @@ def main(): args = sys.argv[1:] sigs = {} class_sigs = {} - pyversion = 3 + pyversion = defaults.PYTHON3_VERSION while args and args[0].startswith('--'): if args[0] == '--docpath': docpath = args[1] @@ -483,7 +484,7 @@ def main(): sigs = dict(find_unique_signatures(all_sigs)) class_sigs = dict(find_unique_signatures(all_class_sigs)) elif args[0] == '--py2': - pyversion = 2 + pyversion = defaults.PYTHON2_VERSION else: raise SystemExit('Unrecognized option %s' % args[0]) args = args[1:] diff --git a/mypy/stubutil.py b/mypy/stubutil.py index 3de75ed36823..b2547987abe1 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -78,9 +78,9 @@ def is_c_module(module): return '__file__' not in module.__dict__ or module.__dict__['__file__'].endswith('.so') -def write_header(file, module_name, pyversion=3): +def write_header(file, module_name, pyversion=(3, 5)): if module_name: - if pyversion >= 3: + if pyversion[0] >= 3: version = '%d.%d' % (sys.version_info.major, sys.version_info.minor) else: diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index 5201309f375a..b6b1e5a850cf 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -2,8 +2,9 @@ import re import os -from typing import List, Dict +from typing import List, Dict, Tuple +from mypy import defaults from mypy.myunit import AssertionFailure from mypy.test import config @@ -260,15 +261,15 @@ def num_skipped_suffix_lines(a1: List[str], a2: List[str]) -> int: return max(0, num_eq - 4) -def testfile_pyversion(path: str) -> int: +def testfile_pyversion(path: str) -> Tuple[int, int]: if path.endswith('python2.test'): - return 2 + return defaults.PYTHON2_VERSION else: - return 3 + return defaults.PYTHON3_VERSION -def testcase_pyversion(path: str, testcase_name: str) -> int: +def testcase_pyversion(path: str, testcase_name: str) -> Tuple[int, int]: if testcase_name.endswith('python2'): - return 2 + return defaults.PYTHON2_VERSION else: return testfile_pyversion(path) diff --git a/mypy/test/testparse.py b/mypy/test/testparse.py index 26c6cd249d34..4fe309ebfab2 100644 --- a/mypy/test/testparse.py +++ b/mypy/test/testparse.py @@ -6,6 +6,7 @@ import typing +from mypy import defaults from mypy.myunit import Suite, AssertionFailure, run_test from mypy.test.helpers import assert_string_arrays_equal from mypy.test.data import parse_test_cases @@ -33,9 +34,10 @@ def test_parser(testcase): The argument contains the description of the test case. """ - pyversion = 3 if testcase.file.endswith('python2.test'): - pyversion = 2 + pyversion = defaults.PYTHON2_VERSION + else: + pyversion = defaults.PYTHON3_VERSION try: n = parse(bytes('\n'.join(testcase.input), 'ascii'), pyversion=pyversion, fnam='main') diff --git a/scripts/mypy b/scripts/mypy index 5c734269cf8d..29d1a0e5a192 100755 --- a/scripts/mypy +++ b/scripts/mypy @@ -12,6 +12,7 @@ import typing from typing import List, Tuple from mypy import build +from mypy import defaults from mypy.errors import CompileError from mypy.version import __version__ @@ -22,7 +23,7 @@ class Options: # Set default options. self.target = build.TYPE_CHECK self.build_flags = [] # type: List[str] - self.pyversion = 3 + self.pyversion = defaults.PYTHON3_VERSION self.custom_typing_module = None # type: str self.html_report_dir = None # type: str self.python_path = False @@ -95,9 +96,18 @@ def process_options(args: List[str]) -> Tuple[str, str, Options]: options.build_flags.append(build.VERBOSE) args = args[1:] elif args[0] == '--py2': - # Use Python 2 mode. - options.pyversion = 2 + options.pyversion = defaults.PYTHON2_VERSION args = args[1:] + elif args[0] == '--python-version': + version = args[1].split(".")[0:2] + if len(version) != 2: + fail("Invalid python version: {} (Format: 'x.y')".format( + args[1])) + if not all(item.isdigit for item in version): + fail("Found non-digit in python version: {}".format( + args[1])) + options.pyversion = tuple(int(v) for v in version) + args = args[2:] elif args[0] == '-m' and args[1:]: options.build_flags.append(build.MODULE) return None, args[1], options @@ -138,8 +148,8 @@ def process_options(args: List[str]) -> Tuple[str, str, Options]: if args[1:]: usage('Extra argument: {}'.format(args[1])) - if options.python_path and options.pyversion == 2: - usage('--py2 specified, ' + if options.python_path and options.pyversion[0] == 2: + usage('Python version 2 (or --py2) specified, ' 'but --use-python-path will search in sys.path of Python 3') return args[0], None, options