Repository: python/mypy

Issue:
Make `--no-implicit-reexport` friendlier
Currently, users have to pay double jeopardy if they rely on an implicit reexport. First, they get an error "module does not explicitly export attribute [attr-defined]", but then the type of the thing also becomes Any.

I think it would be friendlier to have mypy just use the type of the hidden thing. This is useful in cases where you can't change the module you're importing from. In some cases, you can have a per-module implicit_reexport config (although I'd argue most users would struggle to find that solution), but in some "consenting adults" situations this isn't quite what you want.

Here's what the patch could look like for the semanal portion of this (edit: merged!). We'd also need to modify attribute access logic.

<details>

```
diff --git a/mypy/semanal.py b/mypy/semanal.py
index b37c9b2a5..06d648a16 100644
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -2242,10 +2242,20 @@ class SemanticAnalyzer(
                     )
                     continue
 
-            if node and not node.module_hidden:
+            if node:
                 self.process_imported_symbol(
                     node, module_id, id, imported_id, fullname, module_public, context=imp
                 )
+                if node.module_hidden:
+                    self.report_missing_module_attribute(
+                        module_id,
+                        id,
+                        imported_id,
+                        module_public=module_public,
+                        module_hidden=not module_public,
+                        context=imp,
+                        add_unknown_imported_symbol=False,
+                    )
             elif module and not missing_submodule:
                 # Target module exists but the imported name is missing or hidden.
                 self.report_missing_module_attribute(
@@ -2333,6 +2343,7 @@ class SemanticAnalyzer(
         module_public: bool,
         module_hidden: bool,
         context: Node,
+        add_unknown_imported_symbol: bool = True,
     ) -> None:
         # Missing attribute.
         if self.is_incomplete_namespace(import_id):
@@ -2357,13 +2368,14 @@ class SemanticAnalyzer(
                     suggestion = f"; maybe {pretty_seq(matches, 'or')}?"
                     message += f"{suggestion}"
         self.fail(message, context, code=codes.ATTR_DEFINED)
-        self.add_unknown_imported_symbol(
-            imported_id,
-            context,
-            target_name=None,
-            module_public=module_public,
-            module_hidden=not module_public,
-        )
+        if add_unknown_imported_symbol:
+            self.add_unknown_imported_symbol(
+                imported_id,
+                context,
+                target_name=None,
+                module_public=module_public,
+                module_hidden=not module_public,
+            )
 
         if import_id == "typing":
             # The user probably has a missing definition in a test fixture. Let's verify.
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
index 5a075dd6e..9beffd2f9 100644
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -1606,14 +1606,18 @@ strict_equality = false
 
 
 [case testNoImplicitReexport]
-# flags: --no-implicit-reexport
-from other_module_2 import a
+# flags: --no-implicit-reexport --show-error-codes
+from other_module_2 import a  # E: Module "other_module_2" does not explicitly export attribute "a"  [attr-defined]
+reveal_type(a)  # N: Revealed type is "builtins.int"
+
+import other_module_2
+reveal_type(other_module_2.a)  # E: "object" does not explicitly export attribute "a"  [attr-defined] \
+                               # N: Revealed type is "builtins.int"
+
 [file other_module_1.py]
 a = 5
 [file other_module_2.py]
 from other_module_1 import a
-[out]
-main:2: error: Module "other_module_2" does not explicitly export attribute "a"
 
 [case testNoImplicitReexportRespectsAll]
 # flags: --no-implicit-reexport
```

</details>


Discussion:
This is the same thought process we went through in pyright. In earlier versions, all implicitly re-exported symbols from a "py.typed" library evaluated to `Unknown` (which is what pyright calls an implicit `Any`). Pyright users and library authors complained about this behavior, so we modified it to work the way that you're proposing above. The imported type is now resolved to its hidden type, but an error is emitted indicating that it is a private symbol.
#13933 is a good example of the perils of the current behavior. I agree with your suggestion.
Out of curiosity, is it a recent behavior that `--implicit-reexport` does not work on `from .<file> import <attr>`. It says not explictly exported even though I would low to ignore that.
@emirkmo please file a separate issue with details about what you're doing

Resolving patch:
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -24,6 +24,7 @@
     FuncDef,
     IndexExpr,
     MypyFile,
+    NameExpr,
     OverloadedFuncDef,
     SymbolNode,
     SymbolTable,
@@ -608,7 +609,19 @@ def analyze_member_var_access(
         mx.msg.undefined_in_superclass(name, mx.context)
         return AnyType(TypeOfAny.from_error)
     else:
-        return report_missing_attribute(mx.original_type, itype, name, mx)
+        ret = report_missing_attribute(mx.original_type, itype, name, mx)
+        # Avoid paying double jeopardy if we can't find the member due to --no-implicit-reexport
+        if (
+            mx.module_symbol_table is not None
+            and name in mx.module_symbol_table
+            and not mx.module_symbol_table[name].module_public
+        ):
+            v = mx.module_symbol_table[name].node
+            e = NameExpr(name)
+            e.set_line(mx.context)
+            e.node = v
+            return mx.chk.expr_checker.analyze_ref_expr(e, lvalue=mx.is_lvalue)
+        return ret
 
 
 def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Context) -> None:


Test patch:
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -1611,14 +1611,22 @@ from other_module_2 import a  # E: Module "other_module_2" does not explicitly e
 reveal_type(a)  # N: Revealed type is "builtins.int"
 
 import other_module_2
-# TODO: this should also reveal builtins.int, see #13965
-reveal_type(other_module_2.a)  # E: "object" does not explicitly export attribute "a"  [attr-defined] \
-                               # N: Revealed type is "Any"
+reveal_type(other_module_2.a)  # E: Module "other_module_2" does not explicitly export attribute "a"  [attr-defined] \
+                               # N: Revealed type is "builtins.int"
+
+from other_module_2 import b  # E: Module "other_module_2" does not explicitly export attribute "b"  [attr-defined]
+reveal_type(b)  # N: Revealed type is "def (a: builtins.int) -> builtins.str"
+
+import other_module_2
+reveal_type(other_module_2.b)  # E: Module "other_module_2" does not explicitly export attribute "b"  [attr-defined] \
+                               # N: Revealed type is "def (a: builtins.int) -> builtins.str"
 
 [file other_module_1.py]
 a = 5
+def b(a: int) -> str: ...
 [file other_module_2.py]
-from other_module_1 import a
+from other_module_1 import a, b
+[builtins fixtures/module.pyi]
 
 [case testNoImplicitReexportRespectsAll]
 # flags: --no-implicit-reexport
@@ -1649,11 +1657,15 @@ __all__ = ('b',)
 [case testNoImplicitReexportGetAttr]
 # flags: --no-implicit-reexport --python-version 3.7
 from other_module_2 import a  # E: Module "other_module_2" does not explicitly export attribute "a"
+reveal_type(a)  # N: Revealed type is "builtins.int"
+from other_module_2 import b  # E: Module "other_module_2" does not explicitly export attribute "b"
+reveal_type(b)  # N: Revealed type is "builtins.str"
 [file other_module_1.py]
-from typing import Any
-def __getattr__(name: str) -> Any: ...
+b: str = "asdf"
+def __getattr__(name: str) -> int: ...
 [file other_module_2.py]
-from other_module_1 import a
+from other_module_1 import a, b
+def __getattr__(name: str) -> bytes: ...
 [builtins fixtures/tuple.pyi]
 
 [case textNoImplicitReexportSuggestions]
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
@@ -1862,7 +1862,8 @@ import stub
 
 reveal_type(stub.y)  # N: Revealed type is "builtins.int"
 reveal_type(stub.z)  # E: Module "stub" does not explicitly export attribute "z" \
-                     # N: Revealed type is "Any"
+                     # N: Revealed type is "builtins.int"
+
 
 [file stub.pyi]
 from substub import y as y