Skip to content

Commit 9bba377

Browse files
authored
Mention implicit export on missing attribute access (#13917)
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)
1 parent 4ccfca1 commit 9bba377

File tree

2 files changed

+34
-23
lines changed

2 files changed

+34
-23
lines changed

mypy/messages.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -409,32 +409,43 @@ def has_no_attr(
409409
if not self.are_type_names_disabled():
410410
failed = False
411411
if isinstance(original_type, Instance) and original_type.type.names:
412-
alternatives = set(original_type.type.names.keys())
413-
414-
if module_symbol_table is not None:
415-
alternatives |= {key for key in module_symbol_table.keys()}
416-
417-
# in some situations, the member is in the alternatives set
418-
# but since we're in this function, we shouldn't suggest it
419-
if member in alternatives:
420-
alternatives.remove(member)
421-
422-
matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives]
423-
matches.extend(best_matches(member, alternatives)[:3])
424-
if member == "__aiter__" and matches == ["__iter__"]:
425-
matches = [] # Avoid misleading suggestion
426-
if matches:
412+
if (
413+
module_symbol_table is not None
414+
and member in module_symbol_table
415+
and not module_symbol_table[member].module_public
416+
):
427417
self.fail(
428-
'{} has no attribute "{}"; maybe {}?{}'.format(
429-
format_type(original_type),
430-
member,
431-
pretty_seq(matches, "or"),
432-
extra,
433-
),
418+
f"{format_type(original_type, module_names=True)} does not "
419+
f'explicitly export attribute "{member}"',
434420
context,
435421
code=codes.ATTR_DEFINED,
436422
)
437423
failed = True
424+
else:
425+
alternatives = set(original_type.type.names.keys())
426+
if module_symbol_table is not None:
427+
alternatives |= {
428+
k for k, v in module_symbol_table.items() if v.module_public
429+
}
430+
# Rare but possible, see e.g. testNewAnalyzerCyclicDefinitionCrossModule
431+
alternatives.discard(member)
432+
433+
matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives]
434+
matches.extend(best_matches(member, alternatives)[:3])
435+
if member == "__aiter__" and matches == ["__iter__"]:
436+
matches = [] # Avoid misleading suggestion
437+
if matches:
438+
self.fail(
439+
'{} has no attribute "{}"; maybe {}?{}'.format(
440+
format_type(original_type),
441+
member,
442+
pretty_seq(matches, "or"),
443+
extra,
444+
),
445+
context,
446+
code=codes.ATTR_DEFINED,
447+
)
448+
failed = True
438449
if not failed:
439450
self.fail(
440451
'{} has no attribute "{}"{}'.format(

test-data/unit/check-modules.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,7 +1833,7 @@ class C:
18331833
import stub
18341834

18351835
reveal_type(stub.y) # N: Revealed type is "builtins.int"
1836-
reveal_type(stub.z) # E: Module has no attribute "z" \
1836+
reveal_type(stub.z) # E: "Module stub" does not explicitly export attribute "z" \
18371837
# N: Revealed type is "Any"
18381838

18391839
[file stub.pyi]
@@ -1925,7 +1925,7 @@ import mod
19251925
from mod import C, D # E: Module "mod" has no attribute "C"
19261926

19271927
reveal_type(mod.x) # N: Revealed type is "mod.submod.C"
1928-
mod.C # E: Module has no attribute "C"
1928+
mod.C # E: "Module mod" does not explicitly export attribute "C"
19291929
y = mod.D()
19301930
reveal_type(y.a) # N: Revealed type is "builtins.str"
19311931

0 commit comments

Comments
 (0)