From 14a85343652f7c69da626fe467f36a02e209aae7 Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Wed, 19 Apr 2017 23:26:18 -0700 Subject: [PATCH 1/7] Fix crash when looking up _importlib_modulespec names --- mypy/fixup.py | 3 +++ mypy/semanal.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/mypy/fixup.py b/mypy/fixup.py index 58c1abad53566..dd3846128db41 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -15,6 +15,8 @@ ) from mypy.visitor import NodeVisitor +from mypy.semanal import rev_module_rename_map + def fixup_module_pass_one(tree: MypyFile, modules: Dict[str, MypyFile], quick_and_dirty: bool) -> None: @@ -248,6 +250,7 @@ def lookup_qualified(modules: Dict[str, MypyFile], name: str, def lookup_qualified_stnode(modules: Dict[str, MypyFile], name: str, quick_and_dirty: bool) -> Optional[SymbolTableNode]: + name = rev_module_rename_map.get(name, name) head = name rest = [] while True: diff --git a/mypy/semanal.py b/mypy/semanal.py index c59448bf62e4d..6b595c3a0a2f8 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -120,6 +120,8 @@ '_importlib_modulespec.Loader': 'importlib.abc.Loader' } +rev_module_rename_map = {v: k for (k, v) in module_rename_map.items()} + # Hard coded type promotions (shared between all Python versions). # These add extra ad-hoc edges to the subtyping relation. For example, # int is considered a subtype of float, even though there is no From 16a0ef6019d99dddbf3b5dfb19b9d8feaf8dba0c Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Sun, 2 Apr 2017 09:28:31 -0700 Subject: [PATCH 2/7] Add test without builtins fixtures * Env. var. NO_BUILTINS_FIXTURES=1 triggers using regular version for tests * If NO_BUILTINS_FIXTURES=1, minor test error message differences are ignored * Travis under python 3.6 build will run tests without fixtures --- .travis.yml | 3 ++- mypy/build.py | 6 ++++-- mypy/defaults.py | 6 ++++++ mypy/test/data.py | 8 ++++++-- mypy/test/helpers.py | 39 ++++++++++++++++++++++++++++++++++++++- typeshed | 2 +- 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4c25d0e4bdd69..2ed091ccefffe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,6 @@ install: - python setup.py install script: - - python runtests.py -x lint + - if [[ $TRAVIS_PYTHON_VERSION != '3.6' ]]; then python runtests.py -x lint; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then flake8; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then NO_BUILTINS_FIXTURES=1 python runtests.py -x lint -x "unit-test mypy.test.testtypegen" -x "unit-test mypy.test.testtransform" -x "unit-test mypy.test.testsemanal"; fi diff --git a/mypy/build.py b/mypy/build.py index 51e82e6a85434..9fd523179e22c 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -42,6 +42,7 @@ from mypy.stats import dump_type_stats from mypy.types import Type from mypy.version import __version__ +from mypy.defaults import allow_fixtures # We need to know the location of this file to load data, but @@ -142,8 +143,9 @@ def build(sources: List[BuildSource], # debug). This is a test-only feature, so assume our files are laid out # as in the source tree. root_dir = dirname(dirname(__file__)) - lib_path.insert(0, os.path.join(root_dir, 'test-data', 'unit', 'lib-stub')) - else: + if allow_fixtures(): + lib_path.insert(0, os.path.join(root_dir, 'test-data', 'unit', 'lib-stub')) + if not options.use_builtins_fixtures or not allow_fixtures(): for source in sources: if source.path: # Include directory of the program file in the module search path. diff --git a/mypy/defaults.py b/mypy/defaults.py index b5398f990d11d..cbaadefad9bcb 100644 --- a/mypy/defaults.py +++ b/mypy/defaults.py @@ -1,4 +1,10 @@ +import os PYTHON2_VERSION = (2, 7) PYTHON3_VERSION = (3, 6) CACHE_DIR = '.mypy_cache' CONFIG_FILE = 'mypy.ini' +# Set this to False (here in the file, not dynamically!) to disable builtins fixtures in tests + + +def allow_fixtures() -> bool: + return not os.environ.get('NO_BUILTINS_FIXTURES', False) diff --git a/mypy/test/data.py b/mypy/test/data.py index a0573d55479c1..dc0682407c46a 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -12,6 +12,8 @@ from mypy.myunit import TestCase, SkipTestCaseException +from mypy.defaults import allow_fixtures + def parse_test_cases( path: str, @@ -63,7 +65,8 @@ def parse_test_cases( assert arg is not None file_entry = (join(base_path, arg), '\n'.join(p[i].data)) if p[i].id == 'file': - files.append(file_entry) + if arg != 'builtins.py' or allow_fixtures(): + files.append(file_entry) elif p[i].id == 'outfile': output_files.append(file_entry) elif p[i].id in ('builtins', 'builtins_py2'): @@ -77,7 +80,8 @@ def parse_test_cases( # Python 2 fnam = '__builtin__.pyi' with open(mpath) as f: - files.append((join(base_path, fnam), f.read())) + if allow_fixtures(): + files.append((join(base_path, fnam), f.read())) elif p[i].id == 'stale': arg = p[i].arg if arg is None: diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index 32d889bd20cff..16f3f61173c62 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -2,11 +2,13 @@ import re import os -from typing import List, Dict, Tuple +from typing import List, Dict, Tuple, Iterable +from itertools import groupby from mypy import defaults from mypy.myunit import AssertionFailure from mypy.test.data import DataDrivenTestCase +from mypy.defaults import allow_fixtures # AssertStringArraysEqual displays special line alignment helper messages if @@ -14,6 +16,37 @@ MIN_LINE_LENGTH_FOR_ALIGNMENT = 5 +# should match 'tmp/foo.pyi', 'main.py', etc. +MODULE_NAME_PATTERN = r'[\w/.]' +# we really want to be precise because accidentally matching the pattern +# may cause failed tests to look like they pass +# should match 'tmp/foo.pyi:15:30', 'tmp/foo.pyi:15', 'main.py:1:2', etc. +ERROR_PATTERN = re.compile(r'^{}+:(\d+):(\d+:)? error: '.format(MODULE_NAME_PATTERN)) + + +class ErrorStr(str): + ''' + Same as str, but compares equal iff it starts with the same file and line no prefix + ''' + def __eq__(self, other: object) -> bool: + if not isinstance(other, str): + return False + m_self = re.search(ERROR_PATTERN, self) + m_other = re.search(ERROR_PATTERN, other) + if not m_self or not m_other: # we can't parse it, just use normal str comparison + return super().__eq__(other) + else: # parsed successfully, compare line no (ignore filename and column no) + return m_self.group(1) == m_other.group(1) + + +def skip_same_line_errors(iterable: Iterable) -> List: + ''' + Converts all strings into ErrorStr objects, skips consecutive + duplicates, and returns the list (preserving original order) + ''' + return [k for k, g in groupby(map(ErrorStr, iterable))] + + def assert_string_arrays_equal(expected: List[str], actual: List[str], msg: str) -> None: """Assert that two string arrays are equal. @@ -23,6 +56,10 @@ def assert_string_arrays_equal(expected: List[str], actual: List[str], actual = clean_up(actual) + if not allow_fixtures(): + actual = skip_same_line_errors(actual) + expected = skip_same_line_errors(expected) + if actual != expected: num_skip_start = num_skipped_prefix_lines(expected, actual) num_skip_end = num_skipped_suffix_lines(expected, actual) diff --git a/typeshed b/typeshed index 1ea3d2de5794c..8b835f95001b6 160000 --- a/typeshed +++ b/typeshed @@ -1 +1 @@ -Subproject commit 1ea3d2de5794c2224a1bc086aa471d0097699bf1 +Subproject commit 8b835f95001b61734c6b147d3aa6eb4fbe7bce03 From 5364461ecda132b202291708873438a77caeeb14 Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Tue, 18 Apr 2017 17:41:20 -0700 Subject: [PATCH 3/7] Suppress output of actual errors beyond 25 lines --- mypy/test/helpers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index 16f3f61173c62..5418ea1a9f64a 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -14,6 +14,8 @@ # AssertStringArraysEqual displays special line alignment helper messages if # the first different line has at least this many characters, MIN_LINE_LENGTH_FOR_ALIGNMENT = 5 +# Maximum lines of actual error output to print (to prevent hitting log size limit) +MAX_ACTUAL_LINES = 25 # should match 'tmp/foo.pyi', 'main.py', etc. @@ -97,6 +99,9 @@ def assert_string_arrays_equal(expected: List[str], actual: List[str], sys.stderr.write(' ...\n') for j in range(num_skip_start, len(actual) - num_skip_end): + if j - num_skip_start > MAX_ACTUAL_LINES: + sys.stderr.write('... (additional lines suppressed)') + break if j >= len(expected) or expected[j] != actual[j]: sys.stderr.write(' {:<45} (diff)'.format(actual[j])) else: From 206eecbfbe210a3ee697d6fcef0b12c007a22074 Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Thu, 20 Apr 2017 07:26:12 -0700 Subject: [PATCH 4/7] Suppress modules from stdlib from polluting mergetest --- mypy/test/testmerge.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mypy/test/testmerge.py b/mypy/test/testmerge.py index 6802e34310915..2c5ee7e92fd54 100644 --- a/mypy/test/testmerge.py +++ b/mypy/test/testmerge.py @@ -33,6 +33,7 @@ TYPEINFO = ' TYPEINFO' TYPES = 'TYPES' AST = 'AST' +MODULES_TO_DUMP = ('__main__', 'target') class ASTMergeSuite(DataSuite): @@ -132,8 +133,8 @@ def dump(self, def dump_asts(self, modules: Dict[str, MypyFile]) -> List[str]: a = [] for m in sorted(modules): - if m == 'builtins': - # We don't support incremental checking of changes to builtins. + if m not in MODULES_TO_DUMP: + # We don't support incremental checking of changes to builtins or stdlib. continue s = modules[m].accept(self.str_conv) a.extend(s.splitlines()) @@ -142,8 +143,8 @@ def dump_asts(self, modules: Dict[str, MypyFile]) -> List[str]: def dump_symbol_tables(self, modules: Dict[str, MypyFile]) -> List[str]: a = [] for id in sorted(modules): - if id == 'builtins': - # We don't support incremental checking of changes to builtins. + if id not in MODULES_TO_DUMP: + # We don't support incremental checking of changes to builtins or stdlib. continue a.extend(self.dump_symbol_table(id, modules[id].names)) return a @@ -168,7 +169,7 @@ def format_symbol_table_node(self, node: SymbolTableNode) -> str: def dump_typeinfos(self, modules: Dict[str, MypyFile]) -> List[str]: a = [] for id in sorted(modules): - if id == 'builtins': + if id not in MODULES_TO_DUMP: continue a.extend(self.dump_typeinfos_recursive(modules[id].names)) return a @@ -191,7 +192,7 @@ def dump_types(self, graph: Dict[str, State]) -> List[str]: # To make the results repeatable, we try to generate unique and # deterministic sort keys. for module_id in sorted(graph): - if module_id == 'builtins': + if module_id not in MODULES_TO_DUMP: continue type_map = graph[module_id].type_checker.type_map if type_map: From 5989124dd08b668fafff6fa6292abf7e8d213550 Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Tue, 25 Apr 2017 03:33:08 -0700 Subject: [PATCH 5/7] Make minor fixes to tests to pass without builtins fixtures --- runtests.py | 11 ++++++++--- test-data/unit/check-flags.test | 2 +- test-data/unit/check-functions.test | 2 ++ test-data/unit/check-isinstance.test | 16 ++++++++-------- test-data/unit/check-modules.test | 22 +++++++++++----------- test-data/unit/check-newsyntax.test | 4 +++- test-data/unit/check-unreachable-code.test | 4 ++-- 7 files changed, 35 insertions(+), 26 deletions(-) diff --git a/runtests.py b/runtests.py index 83a6ffa0d3da9..fb7c27a879623 100755 --- a/runtests.py +++ b/runtests.py @@ -30,6 +30,7 @@ def get_versions(): # type: () -> typing.List[str] from mypy import util from mypy.test.config import test_data_prefix from mypy.test.testpythoneval import python_eval_files, python_34_eval_files +from mypy.defaults import allow_fixtures import itertools import os @@ -208,14 +209,18 @@ def add_imports(driver: Driver) -> None: driver.add_python_string('import %s' % mod, 'import %s' % mod) -PYTEST_FILES = [os.path.join('mypy', 'test', '{}.py'.format(name)) for name in [ +PYTESTS = [ 'testcheck', 'testextensions', 'testdeps', 'testdiff', - 'testfinegrained', 'testmerge', -]] +] + +if allow_fixtures(): + PYTESTS += ['testfinegrained'] + +PYTEST_FILES = [os.path.join('mypy', 'test', '{}.py'.format(name)) for name in PYTESTS] def add_pytest(driver: Driver) -> None: diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 2b0e5549589a2..8b53dec9c4ef1 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -194,7 +194,7 @@ x = 0 # type: NoReturn # E: Incompatible types in assignment (expression has t [builtins fixtures/dict.pyi] [case testNoReturnImportFromTyping] -from typing import NoReturn +from mypy_extensions import NoReturn def h() -> NoReturn: if bool(): diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 33fc51d3b046c..65324213da0c1 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -88,6 +88,7 @@ def r(x) -> None: ... r = l # E: Incompatible types in assignment (expression has type Callable[[Any, Any], None], variable has type Callable[[Any], None]) [case testSubtypingFunctionsImplicitNames] +from typing import Any def f(a, b): pass def g(c: Any, d: Any) -> Any: pass @@ -1567,6 +1568,7 @@ class A(Generic[t]): [case testRedefineFunction] +from typing import Any def f(x) -> Any: pass def g(x, y): pass def h(x): pass diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index f3f4ea429f563..5466c25ab1873 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -434,13 +434,13 @@ def foo() -> Union[int, str]: pass x = foo() x = 1 -x = x + 1 +x + 1 x = foo() -x = x + 1 # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + (likely involving Union) if isinstance(x, str): - x = x + 1 # E: Unsupported operand types for + ("str" and "int") + x + 1 # E: Unsupported operand types for + ("str" and "int") x = 1 - x = x + 1 + x + 1 [builtins fixtures/isinstancelist.pyi] @@ -463,11 +463,11 @@ from typing import Union x = None # type: Union[int, str] if isinstance(x, str): - x = x + 1 # E: Unsupported operand types for + ("str" and "int") - x = x + 'a' + x + 1 # E: Unsupported operand types for + ("str" and "int") + x + 'a' else: - x = x + 'a' # E: Unsupported operand types for + ("int" and "str") - x = x + 1 + x + 'a' # E: Unsupported operand types for + ("int" and "str") + x + 1 [builtins fixtures/isinstancelist.pyi] [case testIsInstanceIndexing] diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 5824197a39a66..33c18afb1b4c0 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -670,45 +670,45 @@ def f(x: str) -> None: pass [case testTypeCheckNamedModule] # cmd: mypy -m m.a [file m/__init__.py] -None + 1 +object() + 1 [file m/a.py] [out] -tmp/m/__init__.py:1: error: Unsupported left operand type for + (None) +tmp/m/__init__.py:1: error: Unsupported left operand type for + ("object") [case testTypeCheckNamedModule2] # cmd: mypy -m m.a [file m/__init__.py] [file m/a.py] -None + 1 +object() + 1 [out] -tmp/m/a.py:1: error: Unsupported left operand type for + (None) +tmp/m/a.py:1: error: Unsupported left operand type for + ("object") [case testTypeCheckNamedModule3] # cmd: mypy -m m [file m/__init__.py] -None + 1 +object() + 1 [file m/a.py] [out] -tmp/m/__init__.py:1: error: Unsupported left operand type for + (None) +tmp/m/__init__.py:1: error: Unsupported left operand type for + ("object") [case testTypeCheckNamedModule4] # cmd: mypy -m m [file m/__init__.py] [file m/a.py] -None + 1 # Not analyzed. +object() + 1 # Not analyzed. [out] [case testTypeCheckNamedModule5] # cmd: mypy -m m -None + '' # Not analyzed. +object() + 1 # Not analyzed. [file m.py] -None + 1 +object() + 1 [out] -tmp/m.py:1: error: Unsupported left operand type for + (None) +tmp/m.py:1: error: Unsupported left operand type for + ("object") [case testTypeCheckNamedModuleWithImportCycle] # cmd: mypy -m m.a -None + 1 # Does not generate error, as this file won't be analyzed. +object() + 1 # Does not generate error, as this file won't be analyzed. [file m/__init__.py] import m.a [file m/a.py] diff --git a/test-data/unit/check-newsyntax.test b/test-data/unit/check-newsyntax.test index 9f3f87853e63b..45eacb91ece92 100644 --- a/test-data/unit/check-newsyntax.test +++ b/test-data/unit/check-newsyntax.test @@ -35,7 +35,9 @@ d['ab'] = 'ab' # E: Invalid index type "str" for Dict[int, str]; expected type [case testNewSyntaxWithRevealType] # flags: --python-version 3.6 -from typing import Dict +from typing import Dict, TypeVar + +T = TypeVar('T') def tst_local(dct: Dict[int, T]) -> Dict[T, int]: ret: Dict[T, int] = {} diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index a18a42bd6dda8..17a0bd8d77475 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -84,9 +84,9 @@ main:6: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" import typing MYPY = 0 if MYPY: - None + 1 # E: Unsupported left operand type for + (None) + object() + 1 # E: Unsupported left operand type for + ("object") else: - None + '' + object() + 1 [builtins fixtures/bool.pyi] [case testTypeCheckingConditional] From 575c0e8cc6e68e7ec581e5e52dfab80462aba6b3 Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Tue, 25 Apr 2017 04:43:14 -0700 Subject: [PATCH 6/7] Address CR --- test-data/unit/check-flags.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 8b53dec9c4ef1..2b0e5549589a2 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -194,7 +194,7 @@ x = 0 # type: NoReturn # E: Incompatible types in assignment (expression has t [builtins fixtures/dict.pyi] [case testNoReturnImportFromTyping] -from mypy_extensions import NoReturn +from typing import NoReturn def h() -> NoReturn: if bool(): From 00e7b6345563abb2a4bf6032d104836df0683dd9 Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Tue, 25 Apr 2017 04:54:00 -0700 Subject: [PATCH 7/7] Don't run no-fixture tests (just leave the option to do so) --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3ed763eddaa31..56804c71bf503 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,5 @@ install: - python setup.py install script: - - if [[ $TRAVIS_PYTHON_VERSION != '3.6' ]]; then python runtests.py -x lint; fi + - python runtests.py -x lint - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then flake8; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then NO_BUILTINS_FIXTURES=1 python runtests.py -x lint -x "unit-test mypy.test.testtypegen" -x "unit-test mypy.test.testtransform" -x "unit-test mypy.test.testsemanal"; fi