Skip to content

Commit

Permalink
Improve performance for errors on class with many attributes (#14379)
Browse files Browse the repository at this point in the history
When checking manticore with `--check-untyped-defs`, this is a 4x total
speedup from master, from ~320s to ~80s (uncompiled).

I looked into this because of
python/typeshed#9443 (comment)
  • Loading branch information
hauntsaninja committed Jan 3, 2023
1 parent 5f480f3 commit f104914
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 19 deletions.
37 changes: 25 additions & 12 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import re
from contextlib import contextmanager
from textwrap import dedent
from typing import Any, Callable, Iterable, Iterator, List, Sequence, cast
from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast
from typing_extensions import Final

from mypy import errorcodes as codes, message_registry
Expand Down Expand Up @@ -440,7 +440,7 @@ def has_no_attr(
alternatives.discard(member)

matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives]
matches.extend(best_matches(member, alternatives)[:3])
matches.extend(best_matches(member, alternatives, n=3))
if member == "__aiter__" and matches == ["__iter__"]:
matches = [] # Avoid misleading suggestion
if matches:
Expand Down Expand Up @@ -928,11 +928,11 @@ def unexpected_keyword_argument(
matching_type_args.append(callee_arg_name)
else:
not_matching_type_args.append(callee_arg_name)
matches = best_matches(name, matching_type_args)
matches = best_matches(name, matching_type_args, n=3)
if not matches:
matches = best_matches(name, not_matching_type_args)
matches = best_matches(name, not_matching_type_args, n=3)
if matches:
msg += f"; did you mean {pretty_seq(matches[:3], 'or')}?"
msg += f"; did you mean {pretty_seq(matches, 'or')}?"
self.fail(msg, context, code=codes.CALL_ARG)
module = find_defining_module(self.modules, callee)
if module:
Expand Down Expand Up @@ -1695,10 +1695,10 @@ def typeddict_key_not_found(
context,
code=codes.TYPEDDICT_ITEM,
)
matches = best_matches(item_name, typ.items.keys())
matches = best_matches(item_name, typ.items.keys(), n=3)
if matches:
self.note(
"Did you mean {}?".format(pretty_seq(matches[:3], "or")),
"Did you mean {}?".format(pretty_seq(matches, "or")),
context,
code=codes.TYPEDDICT_ITEM,
)
Expand Down Expand Up @@ -2798,11 +2798,24 @@ def find_defining_module(modules: dict[str, MypyFile], typ: CallableType) -> Myp
COMMON_MISTAKES: Final[dict[str, Sequence[str]]] = {"add": ("append", "extend")}


def best_matches(current: str, options: Iterable[str]) -> list[str]:
ratios = {v: difflib.SequenceMatcher(a=current, b=v).ratio() for v in options}
return sorted(
(o for o in options if ratios[o] > 0.75), reverse=True, key=lambda v: (ratios[v], v)
)
def _real_quick_ratio(a: str, b: str) -> float:
# this is an upper bound on difflib.SequenceMatcher.ratio
# similar to difflib.SequenceMatcher.real_quick_ratio, but faster since we don't instantiate
al = len(a)
bl = len(b)
return 2.0 * min(al, bl) / (al + bl)


def best_matches(current: str, options: Collection[str], n: int) -> list[str]:
# narrow down options cheaply
assert current
options = [o for o in options if _real_quick_ratio(current, o) > 0.75]
if len(options) >= 50:
options = [o for o in options if abs(len(o) - len(current)) <= 1]

ratios = {option: difflib.SequenceMatcher(a=current, b=option).ratio() for option in options}
options = [option for option, ratio in ratios.items() if ratio > 0.75]
return sorted(options, key=lambda v: (-ratios[v], v))[:n]


def pretty_seq(args: Sequence[str], conjunction: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2531,7 +2531,7 @@ def report_missing_module_attribute(
)
else:
alternatives = set(module.names.keys()).difference({source_id})
matches = best_matches(source_id, alternatives)[:3]
matches = best_matches(source_id, alternatives, n=3)
if matches:
suggestion = f"; maybe {pretty_seq(matches, 'or')}?"
message += f"{suggestion}"
Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-kwargs.test
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class A: pass

[case testMultipleKeywordsForMisspelling]
def f(thing : 'A', other: 'A', atter: 'A', btter: 'B') -> None: pass # N: "f" defined here
f(otter=A()) # E: Unexpected keyword argument "otter" for "f"; did you mean "other" or "atter"?
f(otter=A()) # E: Unexpected keyword argument "otter" for "f"; did you mean "atter" or "other"?
class A: pass
class B: pass

Expand All @@ -99,15 +99,15 @@ class B: pass

[case testKeywordMisspellingInheritance]
def f(atter: 'A', btter: 'B', ctter: 'C') -> None: pass # N: "f" defined here
f(otter=B()) # E: Unexpected keyword argument "otter" for "f"; did you mean "btter" or "atter"?
f(otter=B()) # E: Unexpected keyword argument "otter" for "f"; did you mean "atter" or "btter"?
class A: pass
class B(A): pass
class C: pass

[case testKeywordMisspellingFloatInt]
def f(atter: float, btter: int) -> None: pass # N: "f" defined here
x: int = 5
f(otter=x) # E: Unexpected keyword argument "otter" for "f"; did you mean "btter" or "atter"?
f(otter=x) # E: Unexpected keyword argument "otter" for "f"; did you mean "atter" or "btter"?

[case testKeywordMisspellingVarArgs]
def f(other: 'A', *atter: 'A') -> None: pass # N: "f" defined here
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -2871,7 +2871,7 @@ aaaaa: int

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

[file m.py]
aaaab: int
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/semanal-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ def somef_unction():
[file f.py]
from m.x import somefunction
[out]
tmp/f.py:1: error: Module "m.x" has no attribute "somefunction"; maybe "somef_unction" or "some_function"?
tmp/f.py:1: error: Module "m.x" has no attribute "somefunction"; maybe "some_function" or "somef_unction"?

[case testImportMisspellingMultipleCandidatesTruncated]
import f
Expand All @@ -831,7 +831,7 @@ def somefun_ction():
[file f.py]
from m.x import somefunction
[out]
tmp/f.py:1: error: Module "m.x" has no attribute "somefunction"; maybe "somefun_ction", "somefu_nction", or "somef_unction"?
tmp/f.py:1: error: Module "m.x" has no attribute "somefunction"; maybe "some_function", "somef_unction", or "somefu_nction"?

[case testFromImportAsInStub]
from m import *
Expand Down

0 comments on commit f104914

Please sign in to comment.