Repository: python/mypy

Issue:
Alias to qualified module does not work
This program generates an unexpected error:

```py
import os.path
x = os.path
x.isdir('/') # Module has no attribute "isdir"
```

However, this works:

```py
from os import path
x = path
x.isdir('/') # Ok
```

Module aliasing was implemented in #3435.


Discussion:
I think this might be an one-line fix. Just use ``RefExpr`` instead of ``NameExpr`` in ``semanal.process_module_assignment``. I can take a look at it this evening.

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -2574,8 +2574,8 @@ def process_module_assignment(self, lvals: List[Expression], rval: Expression,
             elementwise_assignments = zip(seq_rval.items, *[v.items for v in seq_lvals])
             for rv, *lvs in elementwise_assignments:
                 self.process_module_assignment(lvs, rv, ctx)
-        elif isinstance(rval, NameExpr):
-            rnode = self.lookup(rval.name, ctx)
+        elif isinstance(rval, RefExpr):
+            rnode = self.lookup_type_node(rval)
             if rnode and rnode.kind == MODULE_REF:
                 for lval in lvals:
                     if not isinstance(lval, NameExpr):
@@ -3276,7 +3276,7 @@ def lookup_type_node(self, expr: Expression) -> Optional[SymbolTableNode]:
         except TypeTranslationError:
             return None
         if isinstance(t, UnboundType):
-            n = self.lookup_qualified(t.name, expr)
+            n = self.lookup_qualified(t.name, expr, suppress_errors=True)
             return n
         return None
 
@@ -3394,23 +3394,25 @@ def tvar_scope_frame(self, frame: TypeVarScope) -> Iterator[None]:
         yield
         self.tvar_scope = old_scope
 
-    def lookup(self, name: str, ctx: Context) -> SymbolTableNode:
+    def lookup(self, name: str, ctx: Context,
+               suppress_errors: bool = False) -> Optional[SymbolTableNode]:
         """Look up an unqualified name in all active namespaces."""
         implicit_name = False
         # 1a. Name declared using 'global x' takes precedence
         if name in self.global_decls[-1]:
             if name in self.globals:
                 return self.globals[name]
-            else:
+            if not suppress_errors:
                 self.name_not_defined(name, ctx)
-                return None
+            return None
         # 1b. Name declared using 'nonlocal x' takes precedence
         if name in self.nonlocal_decls[-1]:
             for table in reversed(self.locals[:-1]):
                 if table is not None and name in table:
                     return table[name]
             else:
-                self.name_not_defined(name, ctx)
+                if not suppress_errors:
+                    self.name_not_defined(name, ctx)
                 return None
         # 2. Class attributes (if within class definition)
         if self.is_class_scope() and name in self.type.names:
@@ -3433,16 +3435,18 @@ def lookup(self, name: str, ctx: Context) -> SymbolTableNode:
             table = b.node.names
             if name in table:
                 if name[0] == "_" and name[1] != "_":
-                    self.name_not_defined(name, ctx)
+                    if not suppress_errors:
+                        self.name_not_defined(name, ctx)
                     return None
                 node = table[name]
                 return node
         # Give up.
-        if not implicit_name:
+        if not implicit_name and not suppress_errors:
             self.name_not_defined(name, ctx)
             self.check_for_obsolete_short_name(name, ctx)
         else:
-            return implicit_node
+            if implicit_name:
+                return implicit_node
         return None
 
     def check_for_obsolete_short_name(self, name: str, ctx: Context) -> None:
@@ -3452,12 +3456,13 @@ def check_for_obsolete_short_name(self, name: str, ctx: Context) -> None:
         if len(matches) == 1:
             self.note("(Did you mean '{}'?)".format(obsolete_name_mapping[matches[0]]), ctx)
 
-    def lookup_qualified(self, name: str, ctx: Context) -> SymbolTableNode:
+    def lookup_qualified(self, name: str, ctx: Context,
+                         suppress_errors: bool = False) -> Optional[SymbolTableNode]:
         if '.' not in name:
-            return self.lookup(name, ctx)
+            return self.lookup(name, ctx, suppress_errors=suppress_errors)
         else:
             parts = name.split('.')
-            n = self.lookup(parts[0], ctx)  # type: SymbolTableNode
+            n = self.lookup(parts[0], ctx, suppress_errors=suppress_errors)
             if n:
                 for i in range(1, len(parts)):
                     if isinstance(n.node, TypeInfo):
@@ -3480,7 +3485,8 @@ def lookup_qualified(self, name: str, ctx: Context) -> SymbolTableNode:
                         n = n.node.names.get(parts[i], None)
                     # TODO: What if node is Var or FuncDef?
                     if not n:
-                        self.name_not_defined(name, ctx)
+                        if not suppress_errors:
+                            self.name_not_defined(name, ctx)
                         break
                 if n:
                     n = self.normalize_type_alias(n, ctx)


Test patch:
diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test
--- a/test-data/unit/check-modules.test
+++ b/test-data/unit/check-modules.test
@@ -1641,6 +1641,34 @@ m = n  # E: Cannot assign multiple modules to name 'm' without explicit 'types.M
 
 [builtins fixtures/module.pyi]
 
+[case testModuleAliasToQualifiedImport]
+import package.module
+alias = package.module
+reveal_type(alias.whatever('/'))  # E: Revealed type is 'builtins.str*'
+[file package/__init__.py]
+
+[file package/module.py]
+from typing import TypeVar
+T = TypeVar('T')
+def whatever(x: T) -> T: pass
+[builtins fixtures/module.pyi]
+
+[case testModuleAliasToQualifiedImport2]
+import mod
+import othermod
+alias = mod.submod
+reveal_type(alias.whatever('/'))  # E: Revealed type is 'builtins.str*'
+alias = othermod  # E: Cannot assign multiple modules to name 'alias' without explicit 'types.ModuleType' annotation
+[file mod.py]
+import submod
+[file submod.py]
+from typing import TypeVar
+T = TypeVar('T')
+def whatever(x: T) -> T: pass
+[file othermod.py]
+
+[builtins fixtures/module.pyi]
+
 [case testModuleLevelGetattr]
 import has_getattr