Skip to content

Commit f104914

Browse files
authored
Improve performance for errors on class with many attributes (#14379)
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)
1 parent 5f480f3 commit f104914

File tree

5 files changed

+32
-19
lines changed

5 files changed

+32
-19
lines changed

mypy/messages.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import re
1616
from contextlib import contextmanager
1717
from textwrap import dedent
18-
from typing import Any, Callable, Iterable, Iterator, List, Sequence, cast
18+
from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast
1919
from typing_extensions import Final
2020

2121
from mypy import errorcodes as codes, message_registry
@@ -440,7 +440,7 @@ def has_no_attr(
440440
alternatives.discard(member)
441441

442442
matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives]
443-
matches.extend(best_matches(member, alternatives)[:3])
443+
matches.extend(best_matches(member, alternatives, n=3))
444444
if member == "__aiter__" and matches == ["__iter__"]:
445445
matches = [] # Avoid misleading suggestion
446446
if matches:
@@ -928,11 +928,11 @@ def unexpected_keyword_argument(
928928
matching_type_args.append(callee_arg_name)
929929
else:
930930
not_matching_type_args.append(callee_arg_name)
931-
matches = best_matches(name, matching_type_args)
931+
matches = best_matches(name, matching_type_args, n=3)
932932
if not matches:
933-
matches = best_matches(name, not_matching_type_args)
933+
matches = best_matches(name, not_matching_type_args, n=3)
934934
if matches:
935-
msg += f"; did you mean {pretty_seq(matches[:3], 'or')}?"
935+
msg += f"; did you mean {pretty_seq(matches, 'or')}?"
936936
self.fail(msg, context, code=codes.CALL_ARG)
937937
module = find_defining_module(self.modules, callee)
938938
if module:
@@ -1695,10 +1695,10 @@ def typeddict_key_not_found(
16951695
context,
16961696
code=codes.TYPEDDICT_ITEM,
16971697
)
1698-
matches = best_matches(item_name, typ.items.keys())
1698+
matches = best_matches(item_name, typ.items.keys(), n=3)
16991699
if matches:
17001700
self.note(
1701-
"Did you mean {}?".format(pretty_seq(matches[:3], "or")),
1701+
"Did you mean {}?".format(pretty_seq(matches, "or")),
17021702
context,
17031703
code=codes.TYPEDDICT_ITEM,
17041704
)
@@ -2798,11 +2798,24 @@ def find_defining_module(modules: dict[str, MypyFile], typ: CallableType) -> Myp
27982798
COMMON_MISTAKES: Final[dict[str, Sequence[str]]] = {"add": ("append", "extend")}
27992799

28002800

2801-
def best_matches(current: str, options: Iterable[str]) -> list[str]:
2802-
ratios = {v: difflib.SequenceMatcher(a=current, b=v).ratio() for v in options}
2803-
return sorted(
2804-
(o for o in options if ratios[o] > 0.75), reverse=True, key=lambda v: (ratios[v], v)
2805-
)
2801+
def _real_quick_ratio(a: str, b: str) -> float:
2802+
# this is an upper bound on difflib.SequenceMatcher.ratio
2803+
# similar to difflib.SequenceMatcher.real_quick_ratio, but faster since we don't instantiate
2804+
al = len(a)
2805+
bl = len(b)
2806+
return 2.0 * min(al, bl) / (al + bl)
2807+
2808+
2809+
def best_matches(current: str, options: Collection[str], n: int) -> list[str]:
2810+
# narrow down options cheaply
2811+
assert current
2812+
options = [o for o in options if _real_quick_ratio(current, o) > 0.75]
2813+
if len(options) >= 50:
2814+
options = [o for o in options if abs(len(o) - len(current)) <= 1]
2815+
2816+
ratios = {option: difflib.SequenceMatcher(a=current, b=option).ratio() for option in options}
2817+
options = [option for option, ratio in ratios.items() if ratio > 0.75]
2818+
return sorted(options, key=lambda v: (-ratios[v], v))[:n]
28062819

28072820

28082821
def pretty_seq(args: Sequence[str], conjunction: str) -> str:

mypy/semanal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2531,7 +2531,7 @@ def report_missing_module_attribute(
25312531
)
25322532
else:
25332533
alternatives = set(module.names.keys()).difference({source_id})
2534-
matches = best_matches(source_id, alternatives)[:3]
2534+
matches = best_matches(source_id, alternatives, n=3)
25352535
if matches:
25362536
suggestion = f"; maybe {pretty_seq(matches, 'or')}?"
25372537
message += f"{suggestion}"

test-data/unit/check-kwargs.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class A: pass
8787

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

@@ -99,15 +99,15 @@ class B: pass
9999

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

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

112112
[case testKeywordMisspellingVarArgs]
113113
def f(other: 'A', *atter: 'A') -> None: pass # N: "f" defined here

test-data/unit/check-modules.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2871,7 +2871,7 @@ aaaaa: int
28712871

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

28762876
[file m.py]
28772877
aaaab: int

test-data/unit/semanal-modules.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ def somef_unction():
814814
[file f.py]
815815
from m.x import somefunction
816816
[out]
817-
tmp/f.py:1: error: Module "m.x" has no attribute "somefunction"; maybe "somef_unction" or "some_function"?
817+
tmp/f.py:1: error: Module "m.x" has no attribute "somefunction"; maybe "some_function" or "somef_unction"?
818818

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

836836
[case testFromImportAsInStub]
837837
from m import *

0 commit comments

Comments
 (0)