Skip to content

Commit c953bb6

Browse files
authored
Initial take at "dmypy suggest" (python#6528)
An initial version of a daemon based tool for inferring the type of functions based on types present at call sites. Currently works by collecting all argument types at callsites, synthesizing a list of possible function types from that, trying them all, and picking the one with the fewest errors that we think is the "best". We use the plugin mechanism to collect types at callsites and we manually override unanalyzed_type in function nodes and force the daemon to reprocess them.
1 parent 4a58ff8 commit c953bb6

File tree

10 files changed

+867
-3
lines changed

10 files changed

+867
-3
lines changed

mypy/build.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,9 @@ def __init__(self, data_dir: str,
508508
self.modules = {} # type: Dict[str, MypyFile]
509509
self.missing_modules = set() # type: Set[str]
510510
self.fg_deps_meta = {} # type: Dict[str, FgDepMeta]
511+
# Always convert the plugin to a ChainedPlugin so that it can be manipulated if needed
512+
if not isinstance(plugin, ChainedPlugin):
513+
plugin = ChainedPlugin(options, [plugin])
511514
self.plugin = plugin
512515
if options.new_semantic_analyzer:
513516
# Set of namespaces (module or class) that are being populated during semantic

mypy/checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
# Same as above, but for fine-grained mode targets. Only top-level functions/methods
9595
# and module top levels are allowed as such.
9696
FineGrainedDeferredNode = NamedTuple(
97-
'FineDeferredNode',
97+
'FineGrainedDeferredNode',
9898
[
9999
('node', FineGrainedDeferredNodeType),
100100
('context_type_name', Optional[str]),

mypy/dmypy.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ def __init__(self, prog: str) -> None:
9696
p.add_argument('--remove', metavar='FILE', nargs='*',
9797
help="Files to remove from the run")
9898

99+
suggest_parser = p = subparsers.add_parser('suggest',
100+
help="Suggest a signature or show call sites for a specific function")
101+
p.add_argument('function', metavar='FUNCTION', type=str,
102+
help="Function specified as '[package.]module.[class.]function'")
103+
p.add_argument('--json', action='store_true',
104+
help="Produce json that pyannotate can use to apply a suggestion")
105+
p.add_argument('--callsites', action='store_true',
106+
help="Find callsites instead of suggesting a type")
107+
99108
hang_parser = p = subparsers.add_parser('hang', help="Hang for 100 seconds")
100109

101110
daemon_parser = p = subparsers.add_parser('daemon', help="Run daemon in foreground")
@@ -338,6 +347,18 @@ def do_recheck(args: argparse.Namespace) -> None:
338347
check_output(response, args.verbose, args.junit_xml, args.perf_stats_file)
339348

340349

350+
@action(suggest_parser)
351+
def do_suggest(args: argparse.Namespace) -> None:
352+
"""Ask the daemon for a suggested signature.
353+
354+
This just prints whatever the daemon reports as output.
355+
For now it may be closer to a list of call sites.
356+
"""
357+
response = request(args.status_file, 'suggest', function=args.function,
358+
json=args.json, callsites=args.callsites)
359+
check_output(response, verbose=False, junit_xml=None, perf_stats_file=None)
360+
361+
341362
def check_output(response: Dict[str, Any], verbose: bool,
342363
junit_xml: Optional[str],
343364
perf_stats_file: Optional[str]) -> None:

mypy/dmypy_server.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import time
1616
import traceback
1717

18-
from typing import AbstractSet, Any, Callable, Dict, List, Mapping, Optional, Sequence, Set, Tuple
18+
from typing import AbstractSet, Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
1919

2020
import mypy.build
2121
import mypy.errors
@@ -28,6 +28,7 @@
2828
from mypy.fswatcher import FileSystemWatcher, FileData
2929
from mypy.modulefinder import BuildSource, compute_search_paths
3030
from mypy.options import Options
31+
from mypy.suggestions import SuggestionFailure, SuggestionEngine
3132
from mypy.typestate import reset_global_state
3233
from mypy.util import redirect_stderr, redirect_stdout
3334
from mypy.version import __version__
@@ -513,6 +514,25 @@ def _find_changed(self, sources: List[BuildSource],
513514

514515
return changed, removed
515516

517+
def cmd_suggest(self, function: str, json: bool, callsites: bool) -> Dict[str, object]:
518+
"""Suggest a signature for a function."""
519+
if not self.fine_grained_manager:
520+
return {'error': "Command 'suggest' is only valid after a 'check' command"}
521+
engine = SuggestionEngine(self.fine_grained_manager)
522+
try:
523+
if callsites:
524+
out = engine.suggest_callsites(function)
525+
else:
526+
out = engine.suggest(function, json)
527+
except SuggestionFailure as err:
528+
return {'error': str(err)}
529+
else:
530+
if not out:
531+
out = "No suggestions\n"
532+
elif not out.endswith("\n"):
533+
out += "\n"
534+
return {'out': out, 'err': "", 'status': 0}
535+
516536
def cmd_hang(self) -> Dict[str, object]:
517537
"""Hang for 100 seconds, as a debug hack."""
518538
time.sleep(100)

mypy/plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ class CheckerPluginInterface:
131131

132132
msg = None # type: MessageBuilder
133133
options = None # type: Options
134+
path = None # type: str
134135

135136
@abstractmethod
136137
def fail(self, msg: str, ctx: Context) -> None:

mypy/server/update.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,20 @@ def update(self,
270270
self.previous_messages = messages[:]
271271
return messages
272272

273+
def trigger(self, target: str) -> List[str]:
274+
"""Trigger a specific target explicitly.
275+
276+
This is intended for use by the suggestions engine.
277+
"""
278+
self.manager.errors.reset()
279+
changed_modules = propagate_changes_using_dependencies(
280+
self.manager, self.graph, self.deps, set(), set(),
281+
self.previous_targets_with_errors | {target}, [])
282+
# Preserve state needed for the next update.
283+
self.previous_targets_with_errors = self.manager.errors.targets()
284+
self.previous_messages = self.manager.errors.new_messages()[:]
285+
return self.update(changed_modules, [])
286+
273287
def update_one(self,
274288
changed_modules: List[Tuple[str, str]],
275289
initial_set: Set[str],
@@ -961,6 +975,9 @@ def key(node: FineGrainedDeferredNode) -> int:
961975
if graph[module_id].type_checker().check_second_pass():
962976
more = True
963977

978+
if manager.options.export_types:
979+
manager.all_types.update(graph[module_id].type_map())
980+
964981
new_symbols_snapshot = snapshot_symbol_table(file_node.fullname(), file_node.names)
965982
# Check if any attribute types were changed and need to be propagated further.
966983
changed = compare_symbol_table_snapshots(file_node.fullname(),

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