Skip to content

Commit

Permalink
Avoid slow error message logic if errors not shown to user (#14336)
Browse files Browse the repository at this point in the history
This helps with await-related errors introduced in #12958, in
particular, which are expensive to generate. If errors are ignored (e.g.
in third-party libraries) or we don't care about the error message, use
simpler error message logic. We also often filter out error messages
temporarily, so any effort in constructing a nice error message is
wasted.

We could skip even more logic, but this should cover many of the
important code paths.

This speeds up self check by about 2%.
  • Loading branch information
JukkaL authored Dec 26, 2022
1 parent 01a1bf6 commit 5349f9a
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 57 deletions.
4 changes: 4 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5936,6 +5936,10 @@ def check_subtype(
if isinstance(msg, str):
msg = ErrorMessage(msg, code=code)

if self.msg.prefer_simple_messages():
self.fail(msg, context) # Fast path -- skip all fancy logic
return False

orig_subtype = subtype
subtype = get_proper_type(subtype)
orig_supertype = supertype
Expand Down
3 changes: 2 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2157,7 +2157,8 @@ def check_arg(
self.msg.incompatible_argument_note(
original_caller_type, callee_type, context, code=code
)
self.chk.check_possible_missing_await(caller_type, callee_type, context)
if not self.msg.prefer_simple_messages():
self.chk.check_possible_missing_await(caller_type, callee_type, context)

def check_overload_call(
self,
Expand Down
5 changes: 3 additions & 2 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,9 @@ def report_missing_attribute(
override_info: TypeInfo | None = None,
) -> Type:
res_type = mx.msg.has_no_attr(original_type, typ, name, mx.context, mx.module_symbol_table)
if may_be_awaitable_attribute(name, typ, mx, override_info):
mx.msg.possible_missing_await(mx.context)
if not mx.msg.prefer_simple_messages():
if may_be_awaitable_attribute(name, typ, mx, override_info):
mx.msg.possible_missing_await(mx.context)
return res_type


Expand Down
18 changes: 18 additions & 0 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,24 @@ def is_errors_for_file(self, file: str) -> bool:
"""Are there any errors for the given file?"""
return file in self.error_info_map

def prefer_simple_messages(self) -> bool:
"""Should we generate simple/fast error messages?
Return True if errors are not shown to user, i.e. errors are ignored
or they are collected for internal use only.
If True, we should prefer to generate a simple message quickly.
All normal errors should still be reported.
"""
if self.file in self.ignored_files:
# Errors ignored, so no point generating fancy messages
return True
for _watcher in self._watchers:
if _watcher._filter is True and _watcher._filtered is None:
# Errors are filtered
return True
return False

def raise_error(self, use_stdout: bool = True) -> NoReturn:
"""Raise a CompileError with the generated messages.
Expand Down
133 changes: 79 additions & 54 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ def disable_type_names(self) -> Iterator[None]:
def are_type_names_disabled(self) -> bool:
return len(self._disable_type_names) > 0 and self._disable_type_names[-1]

def prefer_simple_messages(self) -> bool:
"""Should we generate simple/fast error messages?
If errors aren't shown to the user, we don't want to waste cyles producing
complex error messages.
"""
return self.errors.prefer_simple_messages()

def report(
self,
msg: str,
Expand Down Expand Up @@ -685,64 +693,69 @@ def incompatible_argument(
actual_type_str, expected_type_str
)
else:
try:
expected_type = callee.arg_types[m - 1]
except IndexError: # Varargs callees
expected_type = callee.arg_types[-1]
arg_type_str, expected_type_str = format_type_distinctly(
arg_type, expected_type, bare=True
)
if arg_kind == ARG_STAR:
arg_type_str = "*" + arg_type_str
elif arg_kind == ARG_STAR2:
arg_type_str = "**" + arg_type_str

# For function calls with keyword arguments, display the argument name rather than the
# number.
arg_label = str(n)
if isinstance(outer_context, CallExpr) and len(outer_context.arg_names) >= n:
arg_name = outer_context.arg_names[n - 1]
if arg_name is not None:
arg_label = f'"{arg_name}"'
if (
arg_kind == ARG_STAR2
and isinstance(arg_type, TypedDictType)
and m <= len(callee.arg_names)
and callee.arg_names[m - 1] is not None
and callee.arg_kinds[m - 1] != ARG_STAR2
):
arg_name = callee.arg_names[m - 1]
assert arg_name is not None
arg_type_str, expected_type_str = format_type_distinctly(
arg_type.items[arg_name], expected_type, bare=True
)
arg_label = f'"{arg_name}"'
if isinstance(outer_context, IndexExpr) and isinstance(outer_context.index, StrExpr):
msg = 'Value of "{}" has incompatible type {}; expected {}'.format(
outer_context.index.value,
quote_type_string(arg_type_str),
quote_type_string(expected_type_str),
)
if self.prefer_simple_messages():
msg = "Argument has incompatible type"
else:
msg = "Argument {} {}has incompatible type {}; expected {}".format(
arg_label,
target,
quote_type_string(arg_type_str),
quote_type_string(expected_type_str),
try:
expected_type = callee.arg_types[m - 1]
except IndexError: # Varargs callees
expected_type = callee.arg_types[-1]
arg_type_str, expected_type_str = format_type_distinctly(
arg_type, expected_type, bare=True
)
if arg_kind == ARG_STAR:
arg_type_str = "*" + arg_type_str
elif arg_kind == ARG_STAR2:
arg_type_str = "**" + arg_type_str

# For function calls with keyword arguments, display the argument name rather
# than the number.
arg_label = str(n)
if isinstance(outer_context, CallExpr) and len(outer_context.arg_names) >= n:
arg_name = outer_context.arg_names[n - 1]
if arg_name is not None:
arg_label = f'"{arg_name}"'
if (
arg_kind == ARG_STAR2
and isinstance(arg_type, TypedDictType)
and m <= len(callee.arg_names)
and callee.arg_names[m - 1] is not None
and callee.arg_kinds[m - 1] != ARG_STAR2
):
arg_name = callee.arg_names[m - 1]
assert arg_name is not None
arg_type_str, expected_type_str = format_type_distinctly(
arg_type.items[arg_name], expected_type, bare=True
)
arg_label = f'"{arg_name}"'
if isinstance(outer_context, IndexExpr) and isinstance(
outer_context.index, StrExpr
):
msg = 'Value of "{}" has incompatible type {}; expected {}'.format(
outer_context.index.value,
quote_type_string(arg_type_str),
quote_type_string(expected_type_str),
)
else:
msg = "Argument {} {}has incompatible type {}; expected {}".format(
arg_label,
target,
quote_type_string(arg_type_str),
quote_type_string(expected_type_str),
)
expected_type = get_proper_type(expected_type)
if isinstance(expected_type, UnionType):
expected_types = list(expected_type.items)
else:
expected_types = [expected_type]
for type in get_proper_types(expected_types):
if isinstance(arg_type, Instance) and isinstance(type, Instance):
notes = append_invariance_notes(notes, arg_type, type)
object_type = get_proper_type(object_type)
if isinstance(object_type, TypedDictType):
code = codes.TYPEDDICT_ITEM
else:
code = codes.ARG_TYPE
expected_type = get_proper_type(expected_type)
if isinstance(expected_type, UnionType):
expected_types = list(expected_type.items)
else:
expected_types = [expected_type]
for type in get_proper_types(expected_types):
if isinstance(arg_type, Instance) and isinstance(type, Instance):
notes = append_invariance_notes(notes, arg_type, type)
self.fail(msg, context, code=code)
if notes:
for note_msg in notes:
Expand All @@ -756,6 +769,8 @@ def incompatible_argument_note(
context: Context,
code: ErrorCode | None,
) -> None:
if self.prefer_simple_messages():
return
if isinstance(
original_caller_type, (Instance, TupleType, TypedDictType, TypeType, CallableType)
):
Expand Down Expand Up @@ -832,7 +847,9 @@ def invalid_index_type(
def too_few_arguments(
self, callee: CallableType, context: Context, argument_names: Sequence[str | None] | None
) -> None:
if argument_names is not None:
if self.prefer_simple_messages():
msg = "Too few arguments"
elif argument_names is not None:
num_positional_args = sum(k is None for k in argument_names)
arguments_left = callee.arg_names[num_positional_args : callee.min_args]
diff = [k for k in arguments_left if k not in argument_names]
Expand All @@ -856,7 +873,10 @@ def missing_named_argument(self, callee: CallableType, context: Context, name: s
self.fail(msg, context, code=codes.CALL_ARG)

def too_many_arguments(self, callee: CallableType, context: Context) -> None:
msg = "Too many arguments" + for_function(callee)
if self.prefer_simple_messages():
msg = "Too many arguments"
else:
msg = "Too many arguments" + for_function(callee)
self.fail(msg, context, code=codes.CALL_ARG)
self.maybe_note_about_special_args(callee, context)

Expand All @@ -874,11 +894,16 @@ def too_many_arguments_from_typed_dict(
self.fail(msg, context)

def too_many_positional_arguments(self, callee: CallableType, context: Context) -> None:
msg = "Too many positional arguments" + for_function(callee)
if self.prefer_simple_messages():
msg = "Too many positional arguments"
else:
msg = "Too many positional arguments" + for_function(callee)
self.fail(msg, context)
self.maybe_note_about_special_args(callee, context)

def maybe_note_about_special_args(self, callee: CallableType, context: Context) -> None:
if self.prefer_simple_messages():
return
# https://github.com/python/mypy/issues/11309
first_arg = callee.def_extras.get("first_arg")
if first_arg and first_arg not in {"self", "cls", "mcs"}:
Expand Down

0 comments on commit 5349f9a

Please sign in to comment.