From fdacaa98d3dc827a06d44bb4405ca49385278f1e Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 17 Oct 2022 01:22:55 -0700 Subject: [PATCH 1/2] Mention explicit export on attribute access Also only suggest public API for attribute access suggestions Fixes #13908 Accomplishes a similar thing to #9084 (the logic from there could be improved too, will send a PR for that next) --- mypy/messages.py | 49 ++++++++++++++++++------------- test-data/unit/check-modules.test | 4 +-- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 6cc40d5a13ec..33f1e1f002ff 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -408,32 +408,39 @@ 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: + assert 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 + } + + 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 f90bd4a3c68d..9b41692e52e6 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" From a69b1c4a5ffad7813f5ee9d258ebd12aa70f2f39 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 17 Oct 2022 21:41:24 -0700 Subject: [PATCH 2/2] fix tests --- mypy/messages.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 33f1e1f002ff..06638240daed 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -408,10 +408,13 @@ def has_no_attr( if not self.are_type_names_disabled(): failed = False if isinstance(original_type, Instance) and original_type.type.names: - if module_symbol_table is not None and member in module_symbol_table: - assert not module_symbol_table[member].module_public + if ( + module_symbol_table is not None + and member in module_symbol_table + and not module_symbol_table[member].module_public + ): self.fail( - f'{format_type(original_type, module_names=True)} does not ' + f"{format_type(original_type, module_names=True)} does not " f'explicitly export attribute "{member}"', context, code=codes.ATTR_DEFINED, @@ -419,11 +422,12 @@ def has_no_attr( 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])