|
15 | 15 | import re
|
16 | 16 | from contextlib import contextmanager
|
17 | 17 | 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 |
19 | 19 | from typing_extensions import Final
|
20 | 20 |
|
21 | 21 | from mypy import errorcodes as codes, message_registry
|
@@ -440,7 +440,7 @@ def has_no_attr(
|
440 | 440 | alternatives.discard(member)
|
441 | 441 |
|
442 | 442 | 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)) |
444 | 444 | if member == "__aiter__" and matches == ["__iter__"]:
|
445 | 445 | matches = [] # Avoid misleading suggestion
|
446 | 446 | if matches:
|
@@ -928,11 +928,11 @@ def unexpected_keyword_argument(
|
928 | 928 | matching_type_args.append(callee_arg_name)
|
929 | 929 | else:
|
930 | 930 | 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) |
932 | 932 | if not matches:
|
933 |
| - matches = best_matches(name, not_matching_type_args) |
| 933 | + matches = best_matches(name, not_matching_type_args, n=3) |
934 | 934 | if matches:
|
935 |
| - msg += f"; did you mean {pretty_seq(matches[:3], 'or')}?" |
| 935 | + msg += f"; did you mean {pretty_seq(matches, 'or')}?" |
936 | 936 | self.fail(msg, context, code=codes.CALL_ARG)
|
937 | 937 | module = find_defining_module(self.modules, callee)
|
938 | 938 | if module:
|
@@ -1695,10 +1695,10 @@ def typeddict_key_not_found(
|
1695 | 1695 | context,
|
1696 | 1696 | code=codes.TYPEDDICT_ITEM,
|
1697 | 1697 | )
|
1698 |
| - matches = best_matches(item_name, typ.items.keys()) |
| 1698 | + matches = best_matches(item_name, typ.items.keys(), n=3) |
1699 | 1699 | if matches:
|
1700 | 1700 | self.note(
|
1701 |
| - "Did you mean {}?".format(pretty_seq(matches[:3], "or")), |
| 1701 | + "Did you mean {}?".format(pretty_seq(matches, "or")), |
1702 | 1702 | context,
|
1703 | 1703 | code=codes.TYPEDDICT_ITEM,
|
1704 | 1704 | )
|
@@ -2798,11 +2798,24 @@ def find_defining_module(modules: dict[str, MypyFile], typ: CallableType) -> Myp
|
2798 | 2798 | COMMON_MISTAKES: Final[dict[str, Sequence[str]]] = {"add": ("append", "extend")}
|
2799 | 2799 |
|
2800 | 2800 |
|
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] |
2806 | 2819 |
|
2807 | 2820 |
|
2808 | 2821 | def pretty_seq(args: Sequence[str], conjunction: str) -> str:
|
|
0 commit comments