Repository: python/mypy

Issue:
stubtest reports imported name as not present at runtime even though it's not reexported
* Are you reporting a bug, or opening a feature request?

A bug, unless this is somehow intended behavior.
* Please insert below the code you are checking with mypy,
  or a mock-up repro if the source is private. We would appreciate
  if you try to simplify your case to a minimal repro.

https://github.com/henribru/stubtest-bug
* What is the actual behavior/output?

```
error: stubtest_bug.foo.Bar is not present at runtime
Stub: at line -1
Any
Runtime:
MISSING

error: stubtest_bug.foo.Baz is not present at runtime
Stub: at line -1
Any
Runtime:
MISSING
```
* What is the behavior/output you expect?

Given that Baz is reexported while Bar isn't, I would only expect an error for Baz. Importing a name that's not present at runtime in the stub to use in type annotations is a common use case and shouldn't produce errors, since Mypy won't let you import that name anyway without it being reexported.

Note that this seems to work as expected if there are annotations for the module you import from. The error only occurs if you import from a module without annotations and stick a type ignore comment at the import (the latter is needed to make stubtest run at all).
* What are the versions of mypy and Python you are using?
  Do you see the same issue after installing mypy from Git master?

Python 3.7.3 and Mypy 0.770. I haven't tested master, but seems like there haven't been any changes to stubtest since 0.770 anyway.



Discussion:
cc @hauntsaninja 
Thanks for the report and clear repro!

stubtest relies on the mypy build process for this kind of information, specifically, the population of the `module_public` attribute. It seems like in the case where mypy considers the imported module missing (which is the case here, as your type: ignore evidences), it fails to set the `module_public` attribute correctly, in `add_unknown_imported_symbol`. I'll attempt a PR soon.

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1731,8 +1731,15 @@ def visit_import_from(self, imp: ImportFrom) -> None:
                 # Target module exists but the imported name is missing or hidden.
                 self.report_missing_module_attribute(module_id, id, imported_id, imp)
             else:
+                module_public = (
+                    not self.is_stub_file
+                    and self.options.implicit_reexport
+                    or as_id is not None
+                )
                 # Import of a missing (sub)module.
-                self.add_unknown_imported_symbol(imported_id, imp, target_name=fullname)
+                self.add_unknown_imported_symbol(
+                    imported_id, imp, target_name=fullname, module_public=module_public
+                )
 
     def process_imported_symbol(self,
                                 node: SymbolTableNode,
@@ -4327,7 +4334,9 @@ def add_module_symbol(self,
                             module_public=module_public,
                             module_hidden=module_hidden)
         else:
-            self.add_unknown_imported_symbol(as_id, context, target_name=id)
+            self.add_unknown_imported_symbol(
+                as_id, context, target_name=id, module_public=module_public
+            )
 
     def add_local(self, node: Union[Var, FuncDef, OverloadedFuncDef], context: Context) -> None:
         """Add local variable or function."""
@@ -4352,7 +4361,8 @@ def add_imported_symbol(self,
     def add_unknown_imported_symbol(self,
                                     name: str,
                                     context: Context,
-                                    target_name: Optional[str] = None) -> None:
+                                    target_name: Optional[str] = None,
+                                    module_public: bool = True) -> None:
         """Add symbol that we don't know what it points to because resolving an import failed.
 
         This can happen if a module is missing, or it is present, but doesn't have
@@ -4380,7 +4390,7 @@ def add_unknown_imported_symbol(self,
         any_type = AnyType(TypeOfAny.from_unimported_type, missing_import_name=var._fullname)
         var.type = any_type
         var.is_suppressed_import = True
-        self.add_symbol(name, var, context)
+        self.add_symbol(name, var, context, module_public=module_public)
 
     #
     # Other helpers


Test patch:
diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py
--- a/mypy/test/teststubtest.py
+++ b/mypy/test/teststubtest.py
@@ -571,6 +571,8 @@ def h(x: str): ...
         yield Case("", "__all__ = []", None)  # dummy case
         yield Case(stub="", runtime="__all__ += ['y']\ny = 5", error="y")
         yield Case(stub="", runtime="__all__ += ['g']\ndef g(): pass", error="g")
+        # Here we should only check that runtime has B, since the stub explicitly re-exports it
+        yield Case(stub="from mystery import A, B as B  # type: ignore", runtime="", error="B")
 
     @collect_cases
     def test_name_mangling(self) -> Iterator[Case]: