Repository: python/mypy

Issue:
Importing from non-py.typed module -> "Module has no attribute" with --strict; mypy still suggests the names it doesn't accept
To me, this issue has a vague resemblance to #10360. I do not know if it is really related.

With `--strict`, referring to variables imported from a non-py.typed module in another module causes mypy to output two error messages of the form `Module has no attribute A; maybe B?` and `Module has no attribute B; maybe A?`, for the same module. This obviously does not make a whole lot of sense.

I do not know if this applies generally to non-py.typed packages or if there's something special about the diffusers package I used.

**How to reproduce**

```shell
$ pip install diffusers==0.5.1
```

(see the Environment section for the precise packages installed by this)

```python
# bug.py
from diffusers import StableDiffusionImg2ImgPipeline, StableDiffusionPipeline

# bug2.py
import bug

print(bug.StableDiffusionImg2ImgPipeline)
print(bug.StableDiffusionPipeline)
```

**Actual Behavior**

```
$ mypy --strict bug2.py 
bug.py:1: error: Skipping analyzing "diffusers": module is installed, but missing library stubs or py.typed marker  [import]
bug.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
bug2.py:3: error: Module has no attribute "StableDiffusionImg2ImgPipeline"; maybe "StableDiffusionPipeline"?  [attr-defined]
bug2.py:4: error: Module has no attribute "StableDiffusionPipeline"; maybe "StableDiffusionImg2ImgPipeline"?  [attr-defined]
Found 3 errors in 2 files (checked 1 source file)
```

Without `--strict`, the "Skipping analyzing" error is the only one produced. With `--ignore-missing-imports` or `# type: ignore[import]` for the `import` in bug.py, only the "Module has no attribute" errors are produced.

**Expected Behavior**

Mypy should

1. Understand that the two attributes exist in the `bug` module;
2. Even failing (1), not suggest options it thinks don't exist.

So I would expect the "Skipping analyzing" to be the only error message mypy outputs.

**Your Environment**

- Mypy version used: `mypy 0.982 (compiled: yes)`
- Mypy command-line flags: `--config-file= --strict`
- Mypy configuration options from `mypy.ini` (and other config files): None (`--config-file=`)
- Python version used: 3.10.8

```
$ pip freeze
certifi==2022.9.24
charset-normalizer==2.1.1
diffusers==0.5.1
filelock==3.8.0
huggingface-hub==0.10.1
idna==3.4
importlib-metadata==5.0.0
mypy==0.982
mypy-extensions==0.4.3
numpy==1.23.4
packaging==21.3
Pillow==9.2.0
pyparsing==3.0.9
PyYAML==6.0
regex==2022.9.13
requests==2.28.1
tomli==2.0.1
tqdm==4.64.1
typing_extensions==4.4.0
urllib3==1.26.12
zipp==3.9.0
```


<!-- You can freely edit this text, please remove all the lines you believe are unnecessary. -->



Discussion:

Resolving patch:
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -408,32 +408,43 @@ def has_no_attr(
             if not self.are_type_names_disabled():
                 failed = False
                 if isinstance(original_type, Instance) and original_type.type.names:
-                    alternatives = set(original_type.type.names.keys())
-
-                    if module_symbol_table is not None:
-                        alternatives |= {key for key in module_symbol_table.keys()}
-
-                    # in some situations, the member is in the alternatives set
-                    # but since we're in this function, we shouldn't suggest it
-                    if member in alternatives:
-                        alternatives.remove(member)
-
-                    matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives]
-                    matches.extend(best_matches(member, alternatives)[:3])
-                    if member == "__aiter__" and matches == ["__iter__"]:
-                        matches = []  # Avoid misleading suggestion
-                    if matches:
+                    if (
+                        module_symbol_table is not None
+                        and member in module_symbol_table
+                        and not module_symbol_table[member].module_public
+                    ):
                         self.fail(
-                            '{} has no attribute "{}"; maybe {}?{}'.format(
-                                format_type(original_type),
-                                member,
-                                pretty_seq(matches, "or"),
-                                extra,
-                            ),
+                            f"{format_type(original_type, module_names=True)} does not "
+                            f'explicitly export attribute "{member}"',
                             context,
                             code=codes.ATTR_DEFINED,
                         )
                         failed = True
+                    else:
+                        alternatives = set(original_type.type.names.keys())
+                        if module_symbol_table is not None:
+                            alternatives |= {
+                                k for k, v in module_symbol_table.items() if v.module_public
+                            }
+                        # Rare but possible, see e.g. testNewAnalyzerCyclicDefinitionCrossModule
+                        alternatives.discard(member)
+
+                        matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives]
+                        matches.extend(best_matches(member, alternatives)[:3])
+                        if member == "__aiter__" and matches == ["__iter__"]:
+                            matches = []  # Avoid misleading suggestion
+                        if matches:
+                            self.fail(
+                                '{} has no attribute "{}"; maybe {}?{}'.format(
+                                    format_type(original_type),
+                                    member,
+                                    pretty_seq(matches, "or"),
+                                    extra,
+                                ),
+                                context,
+                                code=codes.ATTR_DEFINED,
+                            )
+                            failed = True
                 if not failed:
                     self.fail(
                         '{} has no attribute "{}"{}'.format(


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
@@ -1833,7 +1833,7 @@ class C:
 import stub
 
 reveal_type(stub.y)  # N: Revealed type is "builtins.int"
-reveal_type(stub.z)  # E: Module has no attribute "z" \
+reveal_type(stub.z)  # E: "Module stub" does not explicitly export attribute "z" \
                      # N: Revealed type is "Any"
 
 [file stub.pyi]
@@ -1925,7 +1925,7 @@ import mod
 from mod import C, D  # E: Module "mod" has no attribute "C"
 
 reveal_type(mod.x)  # N: Revealed type is "mod.submod.C"
-mod.C  # E: Module has no attribute "C"
+mod.C  # E: "Module mod" does not explicitly export attribute "C"
 y = mod.D()
 reveal_type(y.a)  # N: Revealed type is "builtins.str"