Skip to content

Show type aliases in error messages #12835

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

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
31 changes: 20 additions & 11 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
from mypy import nodes
from mypy import operators
from mypy.literals import literal, literal_hash, Key
from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any, make_optional_type
from mypy.typeanal import (
has_any_from_unimported_type, check_for_explicit_any, make_optional_type,
maybe_expand_unimported_type_becomes_any,
)
from mypy.types import (
Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType,
Instance, NoneType, strip_type, TypeType, TypeOfAny,
Expand Down Expand Up @@ -892,11 +895,15 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
if fdef.type and isinstance(fdef.type, CallableType):
ret_type = fdef.type.ret_type
if has_any_from_unimported_type(ret_type):
self.msg.unimported_type_becomes_any("Return type", ret_type, fdef)
maybe_expand_unimported_type_becomes_any(
"Return type", ret_type, fdef, self.msg
)
for idx, arg_type in enumerate(fdef.type.arg_types):
if has_any_from_unimported_type(arg_type):
prefix = f'Argument {idx + 1} to "{fdef.name}"'
self.msg.unimported_type_becomes_any(prefix, arg_type, fdef)
maybe_expand_unimported_type_becomes_any(
prefix, arg_type, fdef, self.msg
)
check_for_explicit_any(fdef.type, self.options, self.is_typeshed_stub,
self.msg, context=fdef)

Expand Down Expand Up @@ -2227,10 +2234,10 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
if isinstance(s.lvalues[-1], TupleExpr):
# This is a multiple assignment. Instead of figuring out which type is problematic,
# give a generic error message.
self.msg.unimported_type_becomes_any("A type on this line",
AnyType(TypeOfAny.special_form), s)
maybe_expand_unimported_type_becomes_any("A type on this line",
AnyType(TypeOfAny.special_form), s, self.msg)
else:
self.msg.unimported_type_becomes_any("Type of variable", s.type, s)
maybe_expand_unimported_type_becomes_any("Type of variable", s.type, s, self.msg)
check_for_explicit_any(s.type, self.options, self.is_typeshed_stub, self.msg, context=s)

if len(s.lvalues) > 1:
Expand Down Expand Up @@ -3301,6 +3308,7 @@ def check_simple_assignment(self, lvalue_type: Optional[Type], rvalue: Expressio
# '...' is always a valid initializer in a stub.
return AnyType(TypeOfAny.special_form)
else:
orig_lvalue_type = lvalue_type
lvalue_type = get_proper_type(lvalue_type)
always_allow_any = lvalue_type is not None and not isinstance(lvalue_type, AnyType)
rvalue_type = self.expr_checker.accept(rvalue, lvalue_type,
Expand All @@ -3310,8 +3318,8 @@ def check_simple_assignment(self, lvalue_type: Optional[Type], rvalue: Expressio
self.msg.deleted_as_rvalue(rvalue_type, context)
if isinstance(lvalue_type, DeletedType):
self.msg.deleted_as_lvalue(lvalue_type, context)
elif lvalue_type:
self.check_subtype(rvalue_type, lvalue_type, context, msg,
elif orig_lvalue_type:
self.check_subtype(rvalue_type, orig_lvalue_type, context, msg,
f'{rvalue_name} has type',
f'{lvalue_name} has type', code=code)
return rvalue_type
Expand Down Expand Up @@ -5243,6 +5251,7 @@ def check_subtype(self,
else:
msg_text = msg
subtype = get_proper_type(subtype)
orig_supertype = supertype
supertype = get_proper_type(supertype)
if self.msg.try_report_long_tuple_assignment_error(subtype, supertype, context, msg_text,
subtype_label, supertype_label, code=code):
Expand All @@ -5253,13 +5262,13 @@ def check_subtype(self,
note_msg = ''
notes: List[str] = []
if subtype_label is not None or supertype_label is not None:
subtype_str, supertype_str = format_type_distinctly(subtype, supertype)
subtype_str, supertype_str = format_type_distinctly(subtype, orig_supertype)
if subtype_label is not None:
extra_info.append(subtype_label + ' ' + subtype_str)
if supertype_label is not None:
extra_info.append(supertype_label + ' ' + supertype_str)
note_msg = make_inferred_type_note(outer_context or context, subtype,
supertype, supertype_str)
orig_supertype, supertype_str)
if isinstance(subtype, Instance) and isinstance(supertype, Instance):
notes = append_invariance_notes([], subtype, supertype)
if extra_info:
Expand All @@ -5282,7 +5291,7 @@ def check_subtype(self,
if supertype.type.is_protocol and supertype.type.protocol_members == ['__call__']:
call = find_member('__call__', supertype, subtype, is_operator=True)
assert call is not None
self.msg.note_call(supertype, call, context, code=code)
self.msg.note_call(orig_supertype, call, context, code=code)
return False

def contains_none(self, t: Type) -> bool:
Expand Down
12 changes: 8 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mypy.errors import report_internal_error, ErrorWatcher
from mypy.typeanal import (
has_any_from_unimported_type, check_for_explicit_any, set_any_tvars, expand_type_alias,
make_optional_type,
make_optional_type, maybe_expand_unimported_type_becomes_any,
)
from mypy.semanal_enum import ENUM_BASES
from mypy.types import (
Expand Down Expand Up @@ -1539,7 +1539,6 @@ def check_arg(self,
outer_context: Context) -> None:
"""Check the type of a single argument in a call."""
caller_type = get_proper_type(caller_type)
original_caller_type = get_proper_type(original_caller_type)
callee_type = get_proper_type(callee_type)

if isinstance(caller_type, DeletedType):
Expand All @@ -1562,6 +1561,7 @@ def check_arg(self,
object_type=object_type,
context=context,
outer_context=outer_context)
original_caller_type = get_proper_type(original_caller_type)
self.msg.incompatible_argument_note(original_caller_type, callee_type, context,
code=code)

Expand Down Expand Up @@ -3110,7 +3110,9 @@ def visit_cast_expr(self, expr: CastExpr) -> Type:
and is_same_type(source_type, target_type)):
self.msg.redundant_cast(target_type, expr)
if options.disallow_any_unimported and has_any_from_unimported_type(target_type):
self.msg.unimported_type_becomes_any("Target type of cast", target_type, expr)
maybe_expand_unimported_type_becomes_any(
"Target type of cast", target_type, expr, self.msg
)
check_for_explicit_any(target_type, self.chk.options, self.chk.is_typeshed_stub, self.msg,
context=expr)
return target_type
Expand Down Expand Up @@ -4167,7 +4169,9 @@ def visit_namedtuple_expr(self, e: NamedTupleExpr) -> Type:
if tuple_type:
if (self.chk.options.disallow_any_unimported and
has_any_from_unimported_type(tuple_type)):
self.msg.unimported_type_becomes_any("NamedTuple type", tuple_type, e)
maybe_expand_unimported_type_becomes_any(
"NamedTuple type", tuple_type, e, self.msg
)
check_for_explicit_any(tuple_type, self.chk.options, self.chk.is_typeshed_stub,
self.msg, context=e)
return AnyType(TypeOfAny.special_form)
Expand Down
80 changes: 51 additions & 29 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType, LiteralType,
UnionType, NoneType, AnyType, Overloaded, FunctionLike, DeletedType, TypeType,
UninhabitedType, TypeOfAny, UnboundType, PartialType, get_proper_type, ProperType,
ParamSpecType, Parameters, get_proper_types
ParamSpecType, Parameters, get_proper_types, TypeAliasType
)
from mypy.typetraverser import TypeTraverserVisitor
from mypy.nodes import (
Expand Down Expand Up @@ -405,7 +405,6 @@ def incompatible_argument(self,
Return the error code that used for the argument (multiple error
codes are possible).
"""
arg_type = get_proper_type(arg_type)

target = ''
callee_name = callable_name(callee)
Expand Down Expand Up @@ -469,6 +468,7 @@ def incompatible_argument(self,
elif callee_name == '<dict>':
name = callee_name[1:-1]
n -= 1
arg_type = get_proper_type(arg_type)
key_type, value_type = cast(TupleType, arg_type).items
expected_key_type, expected_value_type = cast(TupleType, callee.arg_types[0]).items

Expand Down Expand Up @@ -534,6 +534,7 @@ def incompatible_argument(self,
arg_name = outer_context.arg_names[n - 1]
if arg_name is not None:
arg_label = f'"{arg_name}"'
arg_type = get_proper_type(arg_type)
if (arg_kind == ARG_STAR2
and isinstance(arg_type, TypedDictType)
and m <= len(callee.arg_names)
Expand Down Expand Up @@ -1186,6 +1187,10 @@ def assert_type_fail(self, source_type: Type, target_type: Type, context: Contex
code=codes.ASSERT_TYPE)

def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> None:
"""Print an error about an unfollowed import turning a type into Any

Using typeanal.maybe_expand_unimported_type_becomes_any is preferred because
it will expand type aliases to make errors clearer."""
self.fail(f"{prefix} becomes {format_type(typ)} due to an unfollowed import",
ctx, code=codes.NO_ANY_UNIMPORTED)

Expand Down Expand Up @@ -1696,32 +1701,42 @@ def format_literal_value(typ: LiteralType) -> str:
else:
return typ.value_repr()

# TODO: show type alias names in errors.
typ = get_proper_type(typ)
def format_from_names(name: str, fullname: str, args: Sequence[Type]) -> str:
"""Format a type given its short name, full name, and type arguments"""

if isinstance(typ, Instance):
itype = typ
# Get the short name of the type.
if itype.type.fullname in ('types.ModuleType', '_importlib_modulespec.ModuleType'):
# Some of this only really makes sense for instances, but it's easier
# to include it here than try to only use that code when formatting
# an Instance
if fullname in ('types.ModuleType', '_importlib_modulespec.ModuleType'):
# Make some common error messages simpler and tidier.
return 'Module'
if verbosity >= 2 or (fullnames and itype.type.fullname in fullnames):
base_str = itype.type.fullname
if verbosity >= 2 or (fullnames and fullname in fullnames):
base_str = fullname
else:
base_str = itype.type.name
if not itype.args:
base_str = name
if not args:
# No type arguments, just return the type name
return base_str
elif itype.type.fullname == 'builtins.tuple':
item_type_str = format(itype.args[0])
elif fullname == 'builtins.tuple':
item_type_str = format(args[0])
return f'Tuple[{item_type_str}, ...]'
elif itype.type.fullname in reverse_builtin_aliases:
alias = reverse_builtin_aliases[itype.type.fullname]
elif fullname in reverse_builtin_aliases:
alias = reverse_builtin_aliases[fullname]
alias = alias.split('.')[-1]
return f'{alias}[{format_list(itype.args)}]'
return f'{alias}[{format_list(args)}]'
else:
# There are type arguments. Convert the arguments to strings.
return f'{base_str}[{format_list(itype.args)}]'
return f'{base_str}[{format_list(args)}]'

if isinstance(typ, TypeAliasType):
# typ.alias should only ever be None during creation
assert typ.alias is not None
return format_from_names(typ.alias.name, typ.alias.fullname, typ.args)
# get_proper_type doesn't do anything here but we need it to make mypy happy
typ = get_proper_type(typ)

if isinstance(typ, Instance):
return format_from_names(typ.type.name, typ.type.fullname, typ.args)
elif isinstance(typ, TypeVarType):
# This is similar to non-generic instance types.
return typ.name
Expand Down Expand Up @@ -1842,25 +1857,32 @@ def format_literal_value(typ: LiteralType) -> str:
return 'object'


def collect_all_instances(t: Type) -> List[Instance]:
"""Return all instances that `t` contains (including `t`).
def collect_all_names(t: Type) -> List[Tuple[str, str]]:
"""Return all (shortname, fullname) pairs for all instances and
type aliases that show up when printing `t` in an error message.

This is similar to collect_all_inner_types from typeanal but only
returns instances and will recurse into fallbacks.
TODO: extend this to include all name pairs that will show up in
the error message
"""
visitor = CollectAllInstancesQuery()
visitor = CollectAllNamesQuery()
t.accept(visitor)
return visitor.instances
return visitor.names


class CollectAllInstancesQuery(TypeTraverserVisitor):
class CollectAllNamesQuery(TypeTraverserVisitor):
def __init__(self) -> None:
self.instances: List[Instance] = []
# list of (shortname, fullname) pairs
self.names: List[Tuple[str, str]] = []

def visit_instance(self, t: Instance) -> None:
self.instances.append(t)
self.names.append((t.type.name, t.type.fullname))
super().visit_instance(t)

def visit_type_alias_type(self, t: TypeAliasType) -> None:
assert t.alias is not None
self.names.append((t.alias.name, t.alias.fullname))
super().visit_type_alias_type(t)


def find_type_overlaps(*types: Type) -> Set[str]:
"""Return a set of fullnames that share a short name and appear in either type.
Expand All @@ -1870,8 +1892,8 @@ def find_type_overlaps(*types: Type) -> Set[str]:
"""
d: Dict[str, Set[str]] = {}
for type in types:
for inst in collect_all_instances(type):
d.setdefault(inst.type.name, set()).add(inst.type.fullname)
for name, fullname in collect_all_names(type):
d.setdefault(name, set()).add(fullname)
for shortname in d.keys():
if f'typing.{shortname}' in TYPES_FOR_UNIMPORTED_HINTS:
d[shortname].add(f'typing.{shortname}')
Expand Down
9 changes: 5 additions & 4 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@
from mypy.typeanal import (
TypeAnalyser, analyze_type_alias, no_subscript_builtin_alias,
TypeVarLikeQuery, TypeVarLikeList, remove_dups, has_any_from_unimported_type,
check_for_explicit_any, type_constructors, fix_instance_types
check_for_explicit_any, type_constructors, fix_instance_types,
maybe_expand_unimported_type_becomes_any,
)
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
from mypy.options import Options
Expand Down Expand Up @@ -1593,7 +1594,7 @@ def configure_base_classes(self,
prefix = f"Base type {base_expr.name}"
else:
prefix = "Base type"
self.msg.unimported_type_becomes_any(prefix, base, base_expr)
maybe_expand_unimported_type_becomes_any(prefix, base, base_expr, self.msg)
check_for_explicit_any(base, self.options, self.is_typeshed_stub_file, self.msg,
context=base_expr)

Expand Down Expand Up @@ -3133,11 +3134,11 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool:
for idx, constraint in enumerate(values, start=1):
if has_any_from_unimported_type(constraint):
prefix = f"Constraint {idx}"
self.msg.unimported_type_becomes_any(prefix, constraint, s)
maybe_expand_unimported_type_becomes_any(prefix, constraint, s, self.msg)

if has_any_from_unimported_type(upper_bound):
prefix = "Upper bound of type variable"
self.msg.unimported_type_becomes_any(prefix, upper_bound, s)
maybe_expand_unimported_type_becomes_any(prefix, upper_bound, s, self.msg)

for t in values + [upper_bound]:
check_for_explicit_any(t, self.options, self.is_typeshed_stub_file, self.msg,
Expand Down
8 changes: 6 additions & 2 deletions mypy/semanal_newtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from mypy.semanal_shared import SemanticAnalyzerInterface
from mypy.options import Options
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type
from mypy.typeanal import (
check_for_explicit_any, has_any_from_unimported_type, maybe_expand_unimported_type_becomes_any,
)
from mypy.messages import MessageBuilder, format_type
from mypy.errorcodes import ErrorCode
from mypy import errorcodes as codes
Expand Down Expand Up @@ -92,7 +94,9 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
context=s)

if self.options.disallow_any_unimported and has_any_from_unimported_type(old_type):
self.msg.unimported_type_becomes_any("Argument 2 to NewType(...)", old_type, s)
maybe_expand_unimported_type_becomes_any(
"Argument 2 to NewType(...)", old_type, s, self.msg
)

# If so, add it to the symbol table.
assert isinstance(call.analyzed, NewTypeExpr)
Expand Down
8 changes: 6 additions & 2 deletions mypy/semanal_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
from mypy.semanal_shared import SemanticAnalyzerInterface
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
from mypy.options import Options
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type
from mypy.typeanal import (
check_for_explicit_any, has_any_from_unimported_type, maybe_expand_unimported_type_becomes_any,
)
from mypy.messages import MessageBuilder
from mypy.errorcodes import ErrorCode
from mypy import errorcodes as codes
Expand Down Expand Up @@ -308,7 +310,9 @@ def parse_typeddict_args(
if self.options.disallow_any_unimported:
for t in types:
if has_any_from_unimported_type(t):
self.msg.unimported_type_becomes_any("Type of a TypedDict key", t, dictexpr)
maybe_expand_unimported_type_becomes_any(
"Type of a TypedDict key", t, dictexpr, self.msg
)
assert total is not None
return args[0].value, items, types, total, ok

Expand Down
Loading