diff --git a/mypy/messages.py b/mypy/messages.py index 6cc40d5a13ecd..06638240daed2 100644 --- 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( diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index f90bd4a3c68d3..9b41692e52e63 100644 --- 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"