From d90c145ba19faa284d2e0193e719cf8ef0b4e06c Mon Sep 17 00:00:00 2001 From: Michael Sullivan Date: Mon, 19 Mar 2018 17:45:45 -0700 Subject: [PATCH 1/2] Fix adding a class in a new module and using it We add a field to AnyType indicating---if it was created because of a failed import---what the full name of the imported name is (that is, the name in the importing module). --- mypy/semanal.py | 2 +- mypy/server/deps.py | 2 ++ mypy/typeanal.py | 3 +- mypy/types.py | 16 +++++++-- test-data/unit/deps.test | 8 +++++ test-data/unit/fine-grained-modules.test | 42 ++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index bc9db0c34efb..ca93352c384f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1698,7 +1698,7 @@ def add_unknown_symbol(self, name: str, context: Context, is_import: bool = Fals var._fullname = self.qualified_name(name) var.is_ready = True if is_import: - any_type = AnyType(TypeOfAny.from_unimported_type) + any_type = AnyType(TypeOfAny.from_unimported_type, missing_import_name=var._fullname) else: any_type = AnyType(TypeOfAny.from_error) var.type = any_type diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 91ef49e3a546..efd3a58da871 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -645,6 +645,8 @@ def visit_instance(self, typ: Instance) -> List[str]: return triggers def visit_any(self, typ: AnyType) -> List[str]: + if typ.missing_import_name is not None: + return [make_trigger(typ.missing_import_name)] return [] def visit_none_type(self, typ: NoneTyp) -> List[str]: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 6bafaae0fb11..8b31f86514ea 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -315,7 +315,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type: # context. This is slightly problematic as it allows using the type 'Any' # as a base class -- however, this will fail soon at runtime so the problem # is pretty minor. - return AnyType(TypeOfAny.from_unimported_type) + return AnyType(TypeOfAny.from_unimported_type, + missing_import_name=sym.node.type.missing_import_name) # Allow unbound type variables when defining an alias if not (self.aliasing and sym.kind == TVAR and (not self.tvar_scope or self.tvar_scope.get_binding(sym) is None)): diff --git a/mypy/types.py b/mypy/types.py index b4ad8e68c8f4..96b5849ffee2 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -294,6 +294,7 @@ class AnyType(Type): def __init__(self, type_of_any: TypeOfAny, source_any: Optional['AnyType'] = None, + missing_import_name: Optional[str] = None, line: int = -1, column: int = -1) -> None: super().__init__(line, column) @@ -304,6 +305,14 @@ def __init__(self, if source_any and source_any.source_any: self.source_any = source_any.source_any + if source_any is None: + self.missing_import_name = missing_import_name + else: + self.missing_import_name = source_any.missing_import_name + + # Only unimported type anys and anys from other anys should have an import name + assert (missing_import_name is None or + type_of_any in (TypeOfAny.from_unimported_type, TypeOfAny.from_another_any)) # Only Anys that come from another Any can have source_any. assert type_of_any != TypeOfAny.from_another_any or source_any is not None # We should not have chains of Anys. @@ -321,6 +330,7 @@ def copy_modified(self, if original_any is _dummy: original_any = self.source_any return AnyType(type_of_any=type_of_any, source_any=original_any, + missing_import_name=self.missing_import_name, line=self.line, column=self.column) def __hash__(self) -> int: @@ -331,14 +341,16 @@ def __eq__(self, other: object) -> bool: def serialize(self) -> JsonDict: return {'.class': 'AnyType', 'type_of_any': self.type_of_any.name, - 'source_any': self.source_any.serialize() if self.source_any is not None else None} + 'source_any': self.source_any.serialize() if self.source_any is not None else None, + 'missing_import_name': self.missing_import_name} @classmethod def deserialize(cls, data: JsonDict) -> 'AnyType': assert data['.class'] == 'AnyType' source = data['source_any'] return AnyType(TypeOfAny[data['type_of_any']], - AnyType.deserialize(source) if source is not None else None) + AnyType.deserialize(source) if source is not None else None, + data['missing_import_name']) class UninhabitedType(Type): diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 6245794c3c45..51d423c55688 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -603,3 +603,11 @@ from a import * x = 0 [out] -> m + +[case testMissingModuleClass] +from b import A # type: ignore +def f(x: A) -> None: + x.foo() +[out] + -> , m.f + -> m diff --git a/test-data/unit/fine-grained-modules.test b/test-data/unit/fine-grained-modules.test index 8ab0085216f3..ac87c39d92f5 100644 --- a/test-data/unit/fine-grained-modules.test +++ b/test-data/unit/fine-grained-modules.test @@ -1362,3 +1362,45 @@ def f() -> None: a.py:2: error: "int" not callable a.py:3: error: "str" not callable == + +[case testAddAndUseClass1] +[file a.py] +[file a.py.2] +from b import Foo +def bar(f: Foo) -> None: + f.foo(12) +[file b.py.2] +class Foo: + def foo(self, s: str) -> None: pass +[out] +== +a.py:3: error: Argument 1 to "foo" of "Foo" has incompatible type "int"; expected "str" + +[case testAddAndUseClass2] +[file a.py] +[file a.py.3] +from b import Foo +def bar(f: Foo) -> None: + f.foo(12) +[file b.py.2] +class Foo: + def foo(self, s: str) -> None: pass +[out] +== +== +a.py:3: error: Argument 1 to "foo" of "Foo" has incompatible type "int"; expected "str" + +[case testAddAndUseClass3] +# flags: --ignore-missing-imports +[file a.py] +[file a.py.2] +from b import Foo +def bar(f: Foo) -> None: + f.foo(12) +[file b.py.3] +class Foo: + def foo(self, s: str) -> None: pass +[out] +== +== +a.py:3: error: Argument 1 to "foo" of "Foo" has incompatible type "int"; expected "str" From c6a50357c058fb3b7b53bc4e05c063ce1a2333fc Mon Sep 17 00:00:00 2001 From: Michael Sullivan Date: Tue, 20 Mar 2018 13:08:21 -0700 Subject: [PATCH 2/2] Add some more tests --- test-data/unit/deps.test | 10 +++++- test-data/unit/fine-grained-modules.test | 40 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 51d423c55688..1d4dd789b7e9 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -604,10 +604,18 @@ x = 0 [out] -> m -[case testMissingModuleClass] +[case testMissingModuleClass1] from b import A # type: ignore def f(x: A) -> None: x.foo() [out] -> , m.f -> m + +[case testMissingModuleClass2] +from p.b import A # type: ignore +def f(x: A) -> None: + x.foo() +[out] + -> , m.f + -> m diff --git a/test-data/unit/fine-grained-modules.test b/test-data/unit/fine-grained-modules.test index ac87c39d92f5..ce8cbbc9d369 100644 --- a/test-data/unit/fine-grained-modules.test +++ b/test-data/unit/fine-grained-modules.test @@ -1404,3 +1404,43 @@ class Foo: == == a.py:3: error: Argument 1 to "foo" of "Foo" has incompatible type "int"; expected "str" + +[case testAddAndUseClass4] +[file a.py] +[file a.py.2] +from b import * +def bar(f: Foo) -> None: + f.foo(12) +[file b.py.2] +class Foo: + def foo(self, s: str) -> None: pass +[out] +== +a.py:3: error: Argument 1 to "foo" of "Foo" has incompatible type "int"; expected "str" + +[case testAddAndUseClass4] +[file a.py] +[file a.py.2] +from p.b import * +def bar(f: Foo) -> None: + f.foo(12) +[file p/__init__.py] +[file p/b.py.2] +class Foo: + def foo(self, s: str) -> None: pass +[out] +== +a.py:3: error: Argument 1 to "foo" of "Foo" has incompatible type "int"; expected "str" + +[case testAddAndUseClass5] +[file a.py] +[file a.py.2] +from b import * +def bar(f: Foo) -> None: + f.foo(12) +[file b.py.2] +class Foo: + def foo(self, s: str) -> None: pass +[out] +== +a.py:3: error: Argument 1 to "foo" of "Foo" has incompatible type "int"; expected "str"