Skip to content

Commit 5b17584

Browse files
committed
Allow more fine-grained filtering based on the any-ness of a type
1 parent 3153ae6 commit 5b17584

File tree

5 files changed

+119
-20
lines changed

5 files changed

+119
-20
lines changed

mypy/dmypy/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def __init__(self, prog: str) -> None:
108108
help="Only produce suggestions that cause no errors")
109109
p.add_argument('--no-any', action='store_true',
110110
help="Only produce suggestions that don't contain Any")
111+
p.add_argument('--flex-any', type=float,
112+
help="Allow anys in types if they go above a certain score")
111113
p.add_argument('--try-text', action='store_true',
112114
help="Try using unicode wherever str is inferred")
113115
p.add_argument('--callsites', action='store_true',
@@ -365,7 +367,7 @@ def do_suggest(args: argparse.Namespace) -> None:
365367
"""
366368
response = request(args.status_file, 'suggest', function=args.function,
367369
json=args.json, callsites=args.callsites, no_errors=args.no_errors,
368-
no_any=args.no_any, try_text=args.try_text)
370+
no_any=args.no_any, flex_any=args.flex_any, try_text=args.try_text)
369371
check_output(response, verbose=False, junit_xml=None, perf_stats_file=None)
370372

371373

mypy/dmypy_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ def _find_changed(self, sources: List[BuildSource],
515515
def cmd_suggest(self,
516516
function: str,
517517
callsites: bool,
518-
**kwargs: bool) -> Dict[str, object]:
518+
**kwargs: Any) -> Dict[str, object]:
519519
"""Suggest a signature for a function."""
520520
if not self.fine_grained_manager:
521521
return {'error': "Command 'suggest' is only valid after a 'check' command"}

mypy/suggestions.py

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -145,19 +145,23 @@ class SuggestionEngine:
145145
"""Engine for finding call sites and suggesting signatures."""
146146

147147
def __init__(self, fgmanager: FineGrainedBuildManager,
148+
*,
148149
json: bool,
149150
no_errors: bool = False,
150151
no_any: bool = False,
151-
try_text: bool = False) -> None:
152+
try_text: bool = False,
153+
flex_any: Optional[float] = None) -> None:
152154
self.fgmanager = fgmanager
153155
self.manager = fgmanager.manager
154156
self.plugin = self.manager.plugin
155157
self.graph = fgmanager.graph
156158

157159
self.give_json = json
158160
self.no_errors = no_errors
159-
self.no_any = no_any
160161
self.try_text = try_text
162+
self.flex_any = flex_any
163+
if no_any:
164+
self.flex_any = 1.0
161165

162166
self.max_guesses = 16
163167

@@ -285,13 +289,13 @@ def get_callsites(self, func: FuncDef) -> Tuple[List[Callsite], List[str]]:
285289

286290
return collector_plugin.mystery_hits, errors
287291

288-
def filter_options(self, guesses: List[CallableType]) -> List[CallableType]:
292+
def filter_options(self, guesses: List[CallableType], is_method: bool) -> List[CallableType]:
289293
"""Apply any configured filters to the possible guesses.
290294
291-
Currently the only option is disabling Anys."""
295+
Currently the only option is filtering based on Any prevalance."""
292296
return [
293297
t for t in guesses
294-
if not self.no_any or not callable_has_any(t)
298+
if self.flex_any is None or self.any_score_callable(t, is_method) >= self.flex_any
295299
]
296300

297301
def find_best(self, func: FuncDef, guesses: List[CallableType]) -> Tuple[CallableType, int]:
@@ -329,7 +333,7 @@ def get_suggestion(self, function: str) -> str:
329333
self.get_trivial_type(node),
330334
self.get_default_arg_types(graph[mod], node),
331335
callsites)
332-
guesses = self.filter_options(guesses)
336+
guesses = self.filter_options(guesses, is_method)
333337
if len(guesses) > self.max_guesses:
334338
raise SuggestionFailure("Too many possibilities!")
335339
best, _ = self.find_best(node, guesses)
@@ -344,7 +348,7 @@ def get_suggestion(self, function: str) -> str:
344348
ret_types = [NoneType()]
345349

346350
guesses = [best.copy_modified(ret_type=t) for t in ret_types]
347-
guesses = self.filter_options(guesses)
351+
guesses = self.filter_options(guesses, is_method)
348352
best, errors = self.find_best(node, guesses)
349353

350354
if self.no_errors and errors:
@@ -511,14 +515,16 @@ def format_callable(self,
511515
def format_type(self, cur_module: Optional[str], typ: Type) -> str:
512516
return typ.accept(TypeFormatter(cur_module, self.graph))
513517

514-
def score_type(self, t: Type) -> int:
518+
def score_type(self, t: Type, arg_pos: bool) -> int:
515519
"""Generate a score for a type that we use to pick which type to use.
516520
517521
Lower is better, prefer non-union/non-any types. Don't penalize optionals.
518522
"""
519523
t = get_proper_type(t)
520524
if isinstance(t, AnyType):
521525
return 20
526+
if arg_pos and isinstance(t, NoneType):
527+
return 20
522528
if isinstance(t, UnionType):
523529
if any(isinstance(x, AnyType) for x in t.items):
524530
return 20
@@ -529,7 +535,41 @@ def score_type(self, t: Type) -> int:
529535
return 0
530536

531537
def score_callable(self, t: CallableType) -> int:
532-
return sum([self.score_type(x) for x in t.arg_types])
538+
return (sum([self.score_type(x, arg_pos=True) for x in t.arg_types]) +
539+
self.score_type(t.ret_type, arg_pos=False))
540+
541+
def any_score_type(self, ut: Type, arg_pos: bool) -> float:
542+
"""Generate a very made up number representing the Anyness of a type.
543+
544+
Higher is better, 1.0 is max
545+
"""
546+
t = get_proper_type(ut)
547+
if isinstance(t, AnyType) and t.type_of_any != TypeOfAny.special_form:
548+
return 0
549+
if isinstance(t, NoneType) and arg_pos:
550+
return 0.5
551+
if isinstance(t, UnionType):
552+
if any(isinstance(x, AnyType) for x in t.items):
553+
return 0.5
554+
if any(has_any_type(x) for x in t.items):
555+
return 0.25
556+
if has_any_type(t):
557+
return 0.5
558+
559+
return 1.0
560+
561+
def any_score_callable(self, t: CallableType, is_method: bool) -> float:
562+
# Ignore the first argument of methods
563+
scores = [self.any_score_type(x, arg_pos=True) for x in t.arg_types[int(is_method):]]
564+
# Return type counts twice (since it spreads type information), unless it is
565+
# None in which case it does not count at all. (Though it *does* still count
566+
# if there are no arguments.)
567+
if not isinstance(get_proper_type(t.ret_type), NoneType) or not scores:
568+
ret = self.any_score_type(t.ret_type, arg_pos=False)
569+
scores += [ret, ret]
570+
571+
# print(scores, t)
572+
return sum(scores) / len(scores)
533573

534574

535575
class TypeFormatter(TypeStrVisitor):
@@ -611,13 +651,6 @@ def count_errors(msgs: List[str]) -> int:
611651
return len([x for x in msgs if ' error: ' in x])
612652

613653

614-
def callable_has_any(t: CallableType) -> int:
615-
# We count a bare None in argument position as Any, since
616-
# pyannotate turns it into Optional[Any]
617-
return (any(isinstance(at, NoneType) for at in get_proper_types(t.arg_types))
618-
or has_any_type(t))
619-
620-
621654
T = TypeVar('T')
622655

623656

mypy/test/testfinegrained.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,12 @@ def maybe_suggest(self, step: int, server: Server, src: str) -> List[str]:
276276
no_any = '--no-any' in flags
277277
no_errors = '--no-errors' in flags
278278
try_text = '--try-text' in flags
279+
m = re.match('--flex-any=([0-9.]+)', flags)
280+
flex_any = float(m[1]) if m else None
279281
res = cast(Dict[str, Any],
280282
server.cmd_suggest(
281283
target.strip(), json=json, no_any=no_any, no_errors=no_errors,
282-
try_text=try_text,
284+
try_text=try_text, flex_any=flex_any,
283285
callsites=callsites))
284286
val = res['error'] if 'error' in res else res['out'] + res['err']
285287
output.extend(val.strip().split('\n'))
@@ -288,7 +290,7 @@ def maybe_suggest(self, step: int, server: Server, src: str) -> List[str]:
288290
def get_suggest(self, program_text: str,
289291
incremental_step: int) -> List[Tuple[str, str]]:
290292
step_bit = '1?' if incremental_step == 1 else str(incremental_step)
291-
regex = '# suggest{}: (--[a-zA-Z0-9_\\-./?^ ]+ )*([a-zA-Z0-9_./?^ ]+)$'.format(step_bit)
293+
regex = '# suggest{}: (--[a-zA-Z0-9_\\-./=?^ ]+ )*([a-zA-Z0-9_./?^ ]+)$'.format(step_bit)
292294
m = re.findall(regex, program_text, flags=re.MULTILINE)
293295
return m
294296

test-data/unit/fine-grained-suggest.test

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,3 +484,65 @@ def bar() -> None:
484484
(int, Any) -> int
485485
(int, Any) -> int
486486
==
487+
488+
[case testSuggestFlexAny]
489+
# flags: --strict-optional
490+
# suggest: --flex-any=0.4 m.foo
491+
# suggest: --flex-any=0.7 m.foo
492+
# suggest: --flex-any=0.4 m.bar
493+
# suggest: --flex-any=0.6 m.bar
494+
# suggest: --flex-any=0.5 m.baz
495+
# suggest: --flex-any=0.5 m.F.foo
496+
# suggest: --flex-any=0.7 m.F.foo
497+
# suggest: --flex-any=0.7 m.noargs
498+
# suggest2: --flex-any=0.4 m.foo
499+
# suggest2: --flex-any=0.7 m.foo
500+
[file m.py]
501+
from typing import Any
502+
any: Any
503+
504+
def foo(arg):
505+
return 0
506+
def bar(x, y):
507+
return any
508+
509+
def baz(x):
510+
pass
511+
512+
class F:
513+
def foo(self, x):
514+
return 0
515+
516+
def noargs():
517+
pass
518+
519+
[file n.py]
520+
from typing import Any
521+
any: Any
522+
523+
from m import foo, bar
524+
def wtvr() -> None:
525+
foo(any)
526+
bar(1, 2)
527+
528+
[file n.py.2]
529+
from typing import Any
530+
any: Any
531+
532+
from m import foo, bar
533+
def wtvr() -> None:
534+
foo([any])
535+
536+
[builtins fixtures/isinstancelist.pyi]
537+
[out]
538+
(Any) -> int
539+
No guesses that match criteria!
540+
(int, int) -> Any
541+
No guesses that match criteria!
542+
No guesses that match criteria!
543+
(Any) -> int
544+
No guesses that match criteria!
545+
() -> None
546+
==
547+
(typing.List[Any]) -> int
548+
(typing.List[Any]) -> int

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy