Skip to content

Commit

Permalink
Implements module attribute suggestions (#7971)
Browse files Browse the repository at this point in the history
Addresses the remainder of issue #824
  • Loading branch information
theodoretliu authored and ilevkivskyi committed Nov 23, 2019
1 parent d9dea5f commit fffb4b4
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 15 deletions.
10 changes: 9 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1904,10 +1904,18 @@ def analyze_ordinary_member_access(self, e: MemberExpr,
else:
# This is a reference to a non-module attribute.
original_type = self.accept(e.expr)
base = e.expr
module_symbol_table = None

if isinstance(base, RefExpr) and isinstance(base.node, MypyFile):
module_symbol_table = base.node.names

member_type = analyze_member_access(
e.name, original_type, e, is_lvalue, False, False,
self.msg, original_type=original_type, chk=self.chk,
in_literal_context=self.is_literal_context())
in_literal_context=self.is_literal_context(),
module_symbol_table=module_symbol_table)

return member_type

def analyze_external_member_access(self, member: str, base_type: Type,
Expand Down
29 changes: 20 additions & 9 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
DeletedType, NoneType, TypeType, has_type_vars, get_proper_type, ProperType
)
from mypy.nodes import (
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile, TypeVarExpr,
ARG_POS, ARG_STAR, ARG_STAR2, Decorator, OverloadedFuncDef, TypeAlias, TempNode,
is_final_node, SYMBOL_FUNCBASE_TYPES,
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, SymbolTable, Context,
MypyFile, TypeVarExpr, ARG_POS, ARG_STAR, ARG_STAR2, Decorator,
OverloadedFuncDef, TypeAlias, TempNode, is_final_node,
SYMBOL_FUNCBASE_TYPES,
)
from mypy.messages import MessageBuilder
from mypy.maptype import map_instance_to_supertype
Expand Down Expand Up @@ -47,7 +48,8 @@ def __init__(self,
context: Context,
msg: MessageBuilder,
chk: 'mypy.checker.TypeChecker',
self_type: Optional[Type]) -> None:
self_type: Optional[Type],
module_symbol_table: Optional[SymbolTable] = None) -> None:
self.is_lvalue = is_lvalue
self.is_super = is_super
self.is_operator = is_operator
Expand All @@ -56,6 +58,7 @@ def __init__(self,
self.context = context # Error context
self.msg = msg
self.chk = chk
self.module_symbol_table = module_symbol_table

def builtin_type(self, name: str) -> Instance:
return self.chk.named_type(name)
Expand All @@ -67,7 +70,7 @@ def copy_modified(self, *, messages: Optional[MessageBuilder] = None,
self_type: Optional[Type] = None) -> 'MemberContext':
mx = MemberContext(self.is_lvalue, self.is_super, self.is_operator,
self.original_type, self.context, self.msg, self.chk,
self.self_type)
self.self_type, self.module_symbol_table)
if messages is not None:
mx.msg = messages
if self_type is not None:
Expand All @@ -86,7 +89,8 @@ def analyze_member_access(name: str,
chk: 'mypy.checker.TypeChecker',
override_info: Optional[TypeInfo] = None,
in_literal_context: bool = False,
self_type: Optional[Type] = None) -> Type:
self_type: Optional[Type] = None,
module_symbol_table: Optional[SymbolTable] = None) -> Type:
"""Return the type of attribute 'name' of 'typ'.
The actual implementation is in '_analyze_member_access' and this docstring
Expand All @@ -105,6 +109,10 @@ def analyze_member_access(name: str,
the initial, non-recursive call. The 'self_type' is a component of 'original_type'
to which generic self should be bound (a narrower type that has a fallback to instance).
Currently this is used only for union types.
'module_symbol_table' is passed to this function if 'typ' is actually a module
and we want to keep track of the available attributes of the module (since they
are not available via the type object directly)
"""
mx = MemberContext(is_lvalue,
is_super,
Expand All @@ -113,7 +121,8 @@ def analyze_member_access(name: str,
context,
msg,
chk=chk,
self_type=self_type)
self_type=self_type,
module_symbol_table=module_symbol_table)
result = _analyze_member_access(name, typ, mx, override_info)
possible_literal = get_proper_type(result)
if (in_literal_context and isinstance(possible_literal, Instance) and
Expand Down Expand Up @@ -156,7 +165,7 @@ def _analyze_member_access(name: str,
return AnyType(TypeOfAny.from_error)
if mx.chk.should_suppress_optional_error([typ]):
return AnyType(TypeOfAny.from_error)
return mx.msg.has_no_attr(mx.original_type, typ, name, mx.context)
return mx.msg.has_no_attr(mx.original_type, typ, name, mx.context, mx.module_symbol_table)


# The several functions that follow implement analyze_member_access for various
Expand Down Expand Up @@ -410,7 +419,9 @@ def analyze_member_var_access(name: str,
else:
if mx.chk and mx.chk.should_suppress_optional_error([itype]):
return AnyType(TypeOfAny.from_error)
return mx.msg.has_no_attr(mx.original_type, itype, name, mx.context)
return mx.msg.has_no_attr(
mx.original_type, itype, name, mx.context, mx.module_symbol_table
)


def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Context) -> None:
Expand Down
23 changes: 21 additions & 2 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
FuncDef, reverse_builtin_aliases,
ARG_POS, ARG_OPT, ARG_NAMED, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2,
ReturnStmt, NameExpr, Var, CONTRAVARIANT, COVARIANT, SymbolNode,
CallExpr
CallExpr, SymbolTable
)
from mypy.subtypes import (
is_subtype, find_member, get_member_flags,
Expand Down Expand Up @@ -175,7 +175,12 @@ def note_multiline(self, messages: str, context: Context, file: Optional[str] =
# get some information as arguments, and they build an error message based
# on them.

def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Context) -> Type:
def has_no_attr(self,
original_type: Type,
typ: Type,
member: str,
context: Context,
module_symbol_table: Optional[SymbolTable] = None) -> Type:
"""Report a missing or non-accessible member.
original_type is the top-level type on which the error occurred.
Expand All @@ -184,6 +189,11 @@ def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Cont
will be the specific item in the union that does not have the member
attribute.
'module_symbol_table' is passed to this function if the type for which we
are trying to get a member was originally a module. The SymbolTable allows
us to look up and suggests attributes of the module since they are not
directly available on original_type
If member corresponds to an operator, use the corresponding operator
name in the messages. Return type Any.
"""
Expand Down Expand Up @@ -244,6 +254,15 @@ def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Cont
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__']:
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-columns.test
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ import m
if int():
from m import foobaz # E:5: Module 'm' has no attribute 'foobaz'; maybe "foobar"?
(1).x # E:2: "int" has no attribute "x"
(m.foobaz()) # E:2: Module has no attribute "foobaz"
(m.foobaz()) # E:2: Module has no attribute "foobaz"; maybe "foobar"?

[file m.py]
def foobar(): pass
Expand Down
19 changes: 19 additions & 0 deletions test-data/unit/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -2765,3 +2765,22 @@ x: alias.NonExistent # E: Name 'alias.NonExistent' is not defined
[file pack/__init__.py]
[file pack/mod.py]
class Existent: pass

[case testModuleAttributeTwoSuggestions]
import m
m.aaaa # E: Module has no attribute "aaaa"; maybe "aaaaa" or "aaa"?

[file m.py]
aaa: int
aaaaa: int
[builtins fixtures/module.pyi]

[case testModuleAttributeThreeSuggestions]
import m
m.aaaaa # E: Module has no attribute "aaaaa"; maybe "aabaa", "aaaba", or "aaaab"?

[file m.py]
aaaab: int
aaaba: int
aabaa: int
[builtins fixtures/module.pyi]
4 changes: 2 additions & 2 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -926,8 +926,8 @@ collections.Deque()
typing.deque()

[out]
_testDequeWrongCase.py:4: error: Module has no attribute "Deque"
_testDequeWrongCase.py:5: error: Module has no attribute "deque"
_testDequeWrongCase.py:4: error: Module has no attribute "Deque"; maybe "deque"?
_testDequeWrongCase.py:5: error: Module has no attribute "deque"; maybe "Deque"?

[case testDictUpdateInference]
from typing import Dict, Optional
Expand Down

0 comments on commit fffb4b4

Please sign in to comment.