13
13
import socket
14
14
import sys
15
15
import time
16
+ import traceback
16
17
17
- from typing import Any , Callable , Dict , List , Mapping , Optional , Sequence
18
+ from typing import Any , Callable , Dict , List , Mapping , Optional , Sequence , Tuple
18
19
19
20
import mypy .build
20
21
import mypy .errors
21
22
import mypy .main
23
+ import mypy .server .update
22
24
from mypy .dmypy_util import STATUS_FILE , receive
23
25
from mypy .gclogger import GcLogger
24
26
@@ -82,6 +84,12 @@ class Server:
82
84
def __init__ (self , flags : List [str ]) -> None :
83
85
"""Initialize the server with the desired mypy flags."""
84
86
self .saved_cache = {} # type: mypy.build.SavedCache
87
+ if '--experimental' in flags :
88
+ self .fine_grained = True
89
+ self .fine_grained_initialized = False
90
+ flags .remove ('--experimental' )
91
+ else :
92
+ self .fine_grained = False
85
93
sources , options = mypy .main .process_options (['-i' ] + flags , False )
86
94
if sources :
87
95
sys .exit ("dmypy: start/restart does not accept sources" )
@@ -94,6 +102,10 @@ def __init__(self, flags: List[str]) -> None:
94
102
self .options = options
95
103
if os .path .isfile (STATUS_FILE ):
96
104
os .unlink (STATUS_FILE )
105
+ if self .fine_grained :
106
+ options .incremental = True
107
+ options .show_traceback = True
108
+ options .cache_dir = os .devnull
97
109
98
110
def serve (self ) -> None :
99
111
"""Serve requests, synchronously (no thread or fork)."""
@@ -128,6 +140,9 @@ def serve(self) -> None:
128
140
os .unlink (STATUS_FILE )
129
141
finally :
130
142
os .unlink (self .sockname )
143
+ exc_info = sys .exc_info ()
144
+ if exc_info [0 ]:
145
+ traceback .print_exception (* exc_info ) # type: ignore
131
146
132
147
def create_listening_socket (self ) -> socket .socket :
133
148
"""Create the socket and set it up for listening."""
@@ -190,6 +205,14 @@ def cmd_recheck(self) -> Dict[str, object]:
190
205
191
206
def check (self , sources : List [mypy .build .BuildSource ],
192
207
alt_lib_path : Optional [str ] = None ) -> Dict [str , Any ]:
208
+ if self .fine_grained :
209
+ return self .check_fine_grained (sources )
210
+ else :
211
+ return self .check_default (sources , alt_lib_path )
212
+
213
+ def check_default (self , sources : List [mypy .build .BuildSource ],
214
+ alt_lib_path : Optional [str ] = None ) -> Dict [str , Any ]:
215
+ """Check using the default (per-file) incremental mode."""
193
216
self .last_manager = None
194
217
with GcLogger () as gc_result :
195
218
try :
@@ -212,6 +235,73 @@ def check(self, sources: List[mypy.build.BuildSource],
212
235
response .update (self .last_manager .stats_summary ())
213
236
return response
214
237
238
+ def check_fine_grained (self , sources : List [mypy .build .BuildSource ]) -> Dict [str , Any ]:
239
+ """Check using fine-grained incremental mode."""
240
+ if not self .fine_grained_initialized :
241
+ return self .initialize_fine_grained (sources )
242
+ else :
243
+ return self .fine_grained_increment (sources )
244
+
245
+ def initialize_fine_grained (self , sources : List [mypy .build .BuildSource ]) -> Dict [str , Any ]:
246
+ self .file_modified = {} # type: Dict[str, float]
247
+ for source in sources :
248
+ assert source .path
249
+ self .file_modified [source .path ] = os .stat (source .path ).st_mtime
250
+ try :
251
+ # TODO: alt_lib_path
252
+ result = mypy .build .build (sources = sources ,
253
+ options = self .options )
254
+ except mypy .errors .CompileError as e :
255
+ output = '' .join (s + '\n ' for s in e .messages )
256
+ if e .use_stdout :
257
+ out , err = output , ''
258
+ else :
259
+ out , err = '' , output
260
+ return {'out' : out , 'err' : err , 'status' : 2 }
261
+ messages = result .errors
262
+ manager = result .manager
263
+ graph = result .graph
264
+ self .fine_grained_manager = mypy .server .update .FineGrainedBuildManager (manager , graph )
265
+ status = 1 if messages else 0
266
+ self .previous_messages = messages [:]
267
+ self .fine_grained_initialized = True
268
+ self .previous_sources = sources
269
+ return {'out' : '' .join (s + '\n ' for s in messages ), 'err' : '' , 'status' : status }
270
+
271
+ def fine_grained_increment (self , sources : List [mypy .build .BuildSource ]) -> Dict [str , Any ]:
272
+ changed = self .find_changed (sources )
273
+ if not changed :
274
+ # Nothing changed -- just produce the same result as before.
275
+ messages = self .previous_messages
276
+ else :
277
+ messages = self .fine_grained_manager .update (changed )
278
+ status = 1 if messages else 0
279
+ self .previous_messages = messages [:]
280
+ self .previous_sources = sources
281
+ return {'out' : '' .join (s + '\n ' for s in messages ), 'err' : '' , 'status' : status }
282
+
283
+ def find_changed (self , sources : List [mypy .build .BuildSource ]) -> List [Tuple [str , str ]]:
284
+ changed = []
285
+ for source in sources :
286
+ path = source .path
287
+ assert path
288
+ mtime = os .stat (path ).st_mtime
289
+ if path not in self .file_modified or self .file_modified [path ] != mtime :
290
+ self .file_modified [path ] = mtime
291
+ changed .append ((source .module , path ))
292
+ modules = {source .module for source in sources }
293
+ omitted = [source for source in self .previous_sources if source .module not in modules ]
294
+ for source in omitted :
295
+ path = source .path
296
+ assert path
297
+ # Note that a file could be removed from the list of root sources but still continue
298
+ # to exist on the file system.
299
+ if not os .path .isfile (path ):
300
+ changed .append ((source .module , path ))
301
+ if source .path in self .file_modified :
302
+ del self .file_modified [source .path ]
303
+ return changed
304
+
215
305
def cmd_hang (self ) -> Dict [str , object ]:
216
306
"""Hang for 100 seconds, as a debug hack."""
217
307
time .sleep (100 )
0 commit comments