Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements module attribute suggestions #7971

Merged
merged 3 commits into from
Nov 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1902,10 +1902,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"?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a test case where there are multiple possible suggestions?


[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