Skip to content

Commit fffb4b4

Browse files
theodoretliuilevkivskyi
authored andcommitted
Implements module attribute suggestions (python#7971)
Addresses the remainder of issue python#824
1 parent d9dea5f commit fffb4b4

File tree

6 files changed

+72
-15
lines changed

6 files changed

+72
-15
lines changed

mypy/checkexpr.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1904,10 +1904,18 @@ def analyze_ordinary_member_access(self, e: MemberExpr,
19041904
else:
19051905
# This is a reference to a non-module attribute.
19061906
original_type = self.accept(e.expr)
1907+
base = e.expr
1908+
module_symbol_table = None
1909+
1910+
if isinstance(base, RefExpr) and isinstance(base.node, MypyFile):
1911+
module_symbol_table = base.node.names
1912+
19071913
member_type = analyze_member_access(
19081914
e.name, original_type, e, is_lvalue, False, False,
19091915
self.msg, original_type=original_type, chk=self.chk,
1910-
in_literal_context=self.is_literal_context())
1916+
in_literal_context=self.is_literal_context(),
1917+
module_symbol_table=module_symbol_table)
1918+
19111919
return member_type
19121920

19131921
def analyze_external_member_access(self, member: str, base_type: Type,

mypy/checkmember.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
DeletedType, NoneType, TypeType, has_type_vars, get_proper_type, ProperType
1010
)
1111
from mypy.nodes import (
12-
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile, TypeVarExpr,
13-
ARG_POS, ARG_STAR, ARG_STAR2, Decorator, OverloadedFuncDef, TypeAlias, TempNode,
14-
is_final_node, SYMBOL_FUNCBASE_TYPES,
12+
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, SymbolTable, Context,
13+
MypyFile, TypeVarExpr, ARG_POS, ARG_STAR, ARG_STAR2, Decorator,
14+
OverloadedFuncDef, TypeAlias, TempNode, is_final_node,
15+
SYMBOL_FUNCBASE_TYPES,
1516
)
1617
from mypy.messages import MessageBuilder
1718
from mypy.maptype import map_instance_to_supertype
@@ -47,7 +48,8 @@ def __init__(self,
4748
context: Context,
4849
msg: MessageBuilder,
4950
chk: 'mypy.checker.TypeChecker',
50-
self_type: Optional[Type]) -> None:
51+
self_type: Optional[Type],
52+
module_symbol_table: Optional[SymbolTable] = None) -> None:
5153
self.is_lvalue = is_lvalue
5254
self.is_super = is_super
5355
self.is_operator = is_operator
@@ -56,6 +58,7 @@ def __init__(self,
5658
self.context = context # Error context
5759
self.msg = msg
5860
self.chk = chk
61+
self.module_symbol_table = module_symbol_table
5962

6063
def builtin_type(self, name: str) -> Instance:
6164
return self.chk.named_type(name)
@@ -67,7 +70,7 @@ def copy_modified(self, *, messages: Optional[MessageBuilder] = None,
6770
self_type: Optional[Type] = None) -> 'MemberContext':
6871
mx = MemberContext(self.is_lvalue, self.is_super, self.is_operator,
6972
self.original_type, self.context, self.msg, self.chk,
70-
self.self_type)
73+
self.self_type, self.module_symbol_table)
7174
if messages is not None:
7275
mx.msg = messages
7376
if self_type is not None:
@@ -86,7 +89,8 @@ def analyze_member_access(name: str,
8689
chk: 'mypy.checker.TypeChecker',
8790
override_info: Optional[TypeInfo] = None,
8891
in_literal_context: bool = False,
89-
self_type: Optional[Type] = None) -> Type:
92+
self_type: Optional[Type] = None,
93+
module_symbol_table: Optional[SymbolTable] = None) -> Type:
9094
"""Return the type of attribute 'name' of 'typ'.
9195
9296
The actual implementation is in '_analyze_member_access' and this docstring
@@ -105,6 +109,10 @@ def analyze_member_access(name: str,
105109
the initial, non-recursive call. The 'self_type' is a component of 'original_type'
106110
to which generic self should be bound (a narrower type that has a fallback to instance).
107111
Currently this is used only for union types.
112+
113+
'module_symbol_table' is passed to this function if 'typ' is actually a module
114+
and we want to keep track of the available attributes of the module (since they
115+
are not available via the type object directly)
108116
"""
109117
mx = MemberContext(is_lvalue,
110118
is_super,
@@ -113,7 +121,8 @@ def analyze_member_access(name: str,
113121
context,
114122
msg,
115123
chk=chk,
116-
self_type=self_type)
124+
self_type=self_type,
125+
module_symbol_table=module_symbol_table)
117126
result = _analyze_member_access(name, typ, mx, override_info)
118127
possible_literal = get_proper_type(result)
119128
if (in_literal_context and isinstance(possible_literal, Instance) and
@@ -156,7 +165,7 @@ def _analyze_member_access(name: str,
156165
return AnyType(TypeOfAny.from_error)
157166
if mx.chk.should_suppress_optional_error([typ]):
158167
return AnyType(TypeOfAny.from_error)
159-
return mx.msg.has_no_attr(mx.original_type, typ, name, mx.context)
168+
return mx.msg.has_no_attr(mx.original_type, typ, name, mx.context, mx.module_symbol_table)
160169

161170

162171
# The several functions that follow implement analyze_member_access for various
@@ -410,7 +419,9 @@ def analyze_member_var_access(name: str,
410419
else:
411420
if mx.chk and mx.chk.should_suppress_optional_error([itype]):
412421
return AnyType(TypeOfAny.from_error)
413-
return mx.msg.has_no_attr(mx.original_type, itype, name, mx.context)
422+
return mx.msg.has_no_attr(
423+
mx.original_type, itype, name, mx.context, mx.module_symbol_table
424+
)
414425

415426

416427
def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Context) -> None:

mypy/messages.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
FuncDef, reverse_builtin_aliases,
3232
ARG_POS, ARG_OPT, ARG_NAMED, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2,
3333
ReturnStmt, NameExpr, Var, CONTRAVARIANT, COVARIANT, SymbolNode,
34-
CallExpr
34+
CallExpr, SymbolTable
3535
)
3636
from mypy.subtypes import (
3737
is_subtype, find_member, get_member_flags,
@@ -175,7 +175,12 @@ def note_multiline(self, messages: str, context: Context, file: Optional[str] =
175175
# get some information as arguments, and they build an error message based
176176
# on them.
177177

178-
def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Context) -> Type:
178+
def has_no_attr(self,
179+
original_type: Type,
180+
typ: Type,
181+
member: str,
182+
context: Context,
183+
module_symbol_table: Optional[SymbolTable] = None) -> Type:
179184
"""Report a missing or non-accessible member.
180185
181186
original_type is the top-level type on which the error occurred.
@@ -184,6 +189,11 @@ def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Cont
184189
will be the specific item in the union that does not have the member
185190
attribute.
186191
192+
'module_symbol_table' is passed to this function if the type for which we
193+
are trying to get a member was originally a module. The SymbolTable allows
194+
us to look up and suggests attributes of the module since they are not
195+
directly available on original_type
196+
187197
If member corresponds to an operator, use the corresponding operator
188198
name in the messages. Return type Any.
189199
"""
@@ -244,6 +254,15 @@ def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Cont
244254
failed = False
245255
if isinstance(original_type, Instance) and original_type.type.names:
246256
alternatives = set(original_type.type.names.keys())
257+
258+
if module_symbol_table is not None:
259+
alternatives |= {key for key in module_symbol_table.keys()}
260+
261+
# in some situations, the member is in the alternatives set
262+
# but since we're in this function, we shouldn't suggest it
263+
if member in alternatives:
264+
alternatives.remove(member)
265+
247266
matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives]
248267
matches.extend(best_matches(member, alternatives)[:3])
249268
if member == '__aiter__' and matches == ['__iter__']:

test-data/unit/check-columns.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ import m
126126
if int():
127127
from m import foobaz # E:5: Module 'm' has no attribute 'foobaz'; maybe "foobar"?
128128
(1).x # E:2: "int" has no attribute "x"
129-
(m.foobaz()) # E:2: Module has no attribute "foobaz"
129+
(m.foobaz()) # E:2: Module has no attribute "foobaz"; maybe "foobar"?
130130

131131
[file m.py]
132132
def foobar(): pass

test-data/unit/check-modules.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2765,3 +2765,22 @@ x: alias.NonExistent # E: Name 'alias.NonExistent' is not defined
27652765
[file pack/__init__.py]
27662766
[file pack/mod.py]
27672767
class Existent: pass
2768+
2769+
[case testModuleAttributeTwoSuggestions]
2770+
import m
2771+
m.aaaa # E: Module has no attribute "aaaa"; maybe "aaaaa" or "aaa"?
2772+
2773+
[file m.py]
2774+
aaa: int
2775+
aaaaa: int
2776+
[builtins fixtures/module.pyi]
2777+
2778+
[case testModuleAttributeThreeSuggestions]
2779+
import m
2780+
m.aaaaa # E: Module has no attribute "aaaaa"; maybe "aabaa", "aaaba", or "aaaab"?
2781+
2782+
[file m.py]
2783+
aaaab: int
2784+
aaaba: int
2785+
aabaa: int
2786+
[builtins fixtures/module.pyi]

test-data/unit/pythoneval.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -926,8 +926,8 @@ collections.Deque()
926926
typing.deque()
927927

928928
[out]
929-
_testDequeWrongCase.py:4: error: Module has no attribute "Deque"
930-
_testDequeWrongCase.py:5: error: Module has no attribute "deque"
929+
_testDequeWrongCase.py:4: error: Module has no attribute "Deque"; maybe "deque"?
930+
_testDequeWrongCase.py:5: error: Module has no attribute "deque"; maybe "Deque"?
931931

932932
[case testDictUpdateInference]
933933
from typing import Dict, Optional

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy