From 61bc813d8702eebac648e023ef4593cb9e843b3d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 1 Jun 2017 14:30:12 +0100 Subject: [PATCH] Improve test output when a test doesn't specify the correct fixture Improve things in two ways: 1) Don't crash in tests due to missing types.ModuleType The fixtures used in many test cases don't import the `types` module to speed up tests, and this would often cause uncaught exceptions while writing tests. Now we generate a useful message instead. 2) Suggest fixtures If a developer is doing certain common things in a mypy test that require a non-default builtins fixture (such as `typing.List`), add a hint that points to a fixture that is likely to help. --- mypy/checkexpr.py | 8 +- mypy/semanal.py | 40 +++++++- mypy/test/testcheck.py | 1 + test-data/unit/check-incomplete-fixture.test | 98 ++++++++++++++++++++ 4 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 test-data/unit/check-incomplete-fixture.test diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4829b99231cd..074438e53761 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -154,7 +154,13 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: result = type_object_type(node, self.named_type) elif isinstance(node, MypyFile): # Reference to a module object. - result = self.named_type('types.ModuleType') + try: + result = self.named_type('types.ModuleType') + except KeyError: + # In test cases might 'types' may not be available. + # Fall back to a dummy 'object' type instead to + # avoid a crash. + result = self.named_type('builtins.object') elif isinstance(node, Decorator): result = self.analyze_var_ref(node.var, e) else: diff --git a/mypy/semanal.py b/mypy/semanal.py index a51997f232d8..0d74b7558c23 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -163,6 +163,21 @@ '_make', '_replace', '_asdict', '_source', '__annotations__') +# Map from the full name of a missing definition to the test fixture (under +# test-data/unit/fixtures/) that provides the definition. This is used for +# generating better error messages when running mypy tests only. +SUGGESTED_TEST_FIXTURES = { + 'typing.List': 'list.pyi', + 'typing.Dict': 'dict.pyi', + 'typing.Set': 'set.pyi', + 'builtins.bool': 'bool.pyi', + 'builtins.Exception': 'exception.pyi', + 'builtins.BaseException': 'exception.pyi', + 'builtins.isinstance': 'isinstancelist.pyi', + 'builtins.property': 'property.pyi', + 'builtins.classmethod': 'classmethod.pyi', +} + class SemanticAnalyzer(NodeVisitor): """Semantically analyze parsed mypy files. @@ -1373,20 +1388,31 @@ def process_import_over_existing_name(self, def normalize_type_alias(self, node: SymbolTableNode, ctx: Context) -> SymbolTableNode: normalized = False - if node.fullname in type_aliases: + fullname = node.fullname + if fullname in type_aliases: # Node refers to an aliased type such as typing.List; normalize. - node = self.lookup_qualified(type_aliases[node.fullname], ctx) + node = self.lookup_qualified(type_aliases[fullname], ctx) + if node is None: + self.add_fixture_note(fullname, ctx) + return None normalized = True - if node.fullname in collections_type_aliases: + if fullname in collections_type_aliases: # Similar, but for types from the collections module like typing.DefaultDict self.add_module_symbol('collections', '__mypy_collections__', False, ctx) - node = self.lookup_qualified(collections_type_aliases[node.fullname], ctx) + node = self.lookup_qualified(collections_type_aliases[fullname], ctx) normalized = True if normalized: node = SymbolTableNode(node.kind, node.node, node.mod_id, node.type_override, normalized=True) return node + def add_fixture_note(self, fullname: str, ctx: Context) -> None: + self.note('Maybe your test fixture does not define "{}"?'.format(fullname), ctx) + if fullname in SUGGESTED_TEST_FIXTURES: + self.note( + 'Consider adding [builtins fixtures/{}] to your test description'.format( + SUGGESTED_TEST_FIXTURES[fullname]), ctx) + def correct_relative_import(self, node: Union[ImportFrom, ImportAll]) -> str: if node.relative == 0: return node.id @@ -3337,6 +3363,12 @@ def name_not_defined(self, name: str, ctx: Context) -> None: if extra: message += ' {}'.format(extra) self.fail(message, ctx) + if 'builtins.{}'.format(name) in SUGGESTED_TEST_FIXTURES: + # The user probably has a missing definition in a test fixture. Let's verify. + fullname = 'builtins.{}'.format(name) + if self.lookup_fully_qualified_or_none(fullname) is None: + # Yes. Generate a helpful note. + self.add_fixture_note(fullname, ctx) def name_already_defined(self, name: str, ctx: Context) -> None: self.fail("Name '{}' already defined".format(name), ctx) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index aacf378be1dc..91a818ac0f01 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -75,6 +75,7 @@ 'check-underscores.test', 'check-classvar.test', 'check-enum.test', + 'check-incomplete-fixture.test', ] diff --git a/test-data/unit/check-incomplete-fixture.test b/test-data/unit/check-incomplete-fixture.test new file mode 100644 index 000000000000..68c7c6c9aa0f --- /dev/null +++ b/test-data/unit/check-incomplete-fixture.test @@ -0,0 +1,98 @@ +-- Test cases for reporting errors when a test case uses a fixture with +-- missing definitions. At least in the most common cases this should not +-- result in an uncaught exception. These tests make sure that this behavior +-- does not regress. +-- +-- NOTE: These tests do NOT test behavior of mypy outside tests. + +[case testVariableUndefinedUsingDefaultFixture] +import m +# This used to cause a crash since types.ModuleType is not available +# by default. We fall back to 'object' now. +m.x # E: "object" has no attribute "x" +[file m.py] + +[case testListMissingFromStubs] +from typing import List +def f(x: List[int]) -> None: pass +[out] +main:1: error: Name '__builtins__.list' is not defined +main:1: note: Maybe your test fixture does not define "typing.List"? +main:1: note: Consider adding [builtins fixtures/list.pyi] to your test description + +[case testDictMissingFromStubs] +from typing import Dict +def f(x: Dict[int]) -> None: pass +[out] +main:1: error: Name '__builtins__.dict' is not defined +main:1: note: Maybe your test fixture does not define "typing.Dict"? +main:1: note: Consider adding [builtins fixtures/dict.pyi] to your test description + +[case testSetMissingFromStubs] +from typing import Set +def f(x: Set[int]) -> None: pass +[out] +main:1: error: Name '__builtins__.set' is not defined +main:1: note: Maybe your test fixture does not define "typing.Set"? +main:1: note: Consider adding [builtins fixtures/set.pyi] to your test description + +[case testBoolMissingFromStubs] +x: bool +[out] +main:1: error: Name 'bool' is not defined +main:1: note: Maybe your test fixture does not define "builtins.bool"? +main:1: note: Consider adding [builtins fixtures/bool.pyi] to your test description + +[case testBaseExceptionMissingFromStubs] +e: BaseException +[out] +main:1: error: Name 'BaseException' is not defined +main:1: note: Maybe your test fixture does not define "builtins.BaseException"? +main:1: note: Consider adding [builtins fixtures/exception.pyi] to your test description + +[case testExceptionMissingFromStubs] +e: Exception +[out] +main:1: error: Name 'Exception' is not defined +main:1: note: Maybe your test fixture does not define "builtins.Exception"? +main:1: note: Consider adding [builtins fixtures/exception.pyi] to your test description + +[case testIsinstanceMissingFromStubs] +if isinstance(1, int): + pass +[out] +main:1: error: Name 'isinstance' is not defined +main:1: note: Maybe your test fixture does not define "builtins.isinstance"? +main:1: note: Consider adding [builtins fixtures/isinstancelist.pyi] to your test description + +[case testInvalidTupleDefinitionFromStubs] +from typing import Tuple +x: Tuple[int, ...] +x[0] +for y in x: + pass +[out] +-- These errors are pretty bad, but keeping this test anyway to +-- avoid things getting worse. +main:2: error: "tuple" expects no type arguments, but 1 given +main:3: error: Value of type "tuple" is not indexable +main:4: error: Iterable expected +main:4: error: "tuple" has no attribute "__iter__" + +[case testClassmethodMissingFromStubs] +class A: + @classmethod + def f(cls): pass +[out] +main:2: error: Name 'classmethod' is not defined +main:2: note: Maybe your test fixture does not define "builtins.classmethod"? +main:2: note: Consider adding [builtins fixtures/classmethod.pyi] to your test description + +[case testPropertyMissingFromStubs] +class A: + @property + def f(self): pass +[out] +main:2: error: Name 'property' is not defined +main:2: note: Maybe your test fixture does not define "builtins.property"? +main:2: note: Consider adding [builtins fixtures/property.pyi] to your test description