Skip to content

Commit 41d6aea

Browse files
authored
Fix dmypy run when bad options passed to mypy (#6153)
1 parent 94fe11c commit 41d6aea

File tree

5 files changed

+80
-12
lines changed

5 files changed

+80
-12
lines changed

mypy/dmypy.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Client for mypy daemon mode.
22
3-
Highly experimental! Only supports UNIX-like systems.
4-
53
This manages a daemon process which keeps useful state in memory
64
rather than having to read it back from disk on each run.
75
"""

mypy/dmypy_server.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Server for mypy daemon mode.
22
3-
Only supports UNIX-like systems.
4-
53
This implements a daemon process which keeps useful state in memory
64
to enable fine-grained incremental reprocessing of changes.
75
"""
86

7+
import argparse
98
import base64
9+
import io
1010
import json
1111
import os
1212
import pickle
@@ -29,6 +29,7 @@
2929
from mypy.modulefinder import BuildSource, compute_search_paths
3030
from mypy.options import Options
3131
from mypy.typestate import reset_global_state
32+
from mypy.util import redirect_stderr, redirect_stdout
3233
from mypy.version import __version__
3334

3435

@@ -270,11 +271,19 @@ def cmd_stop(self) -> Dict[str, object]:
270271
def cmd_run(self, version: str, args: Sequence[str]) -> Dict[str, object]:
271272
"""Check a list of files, triggering a restart if needed."""
272273
try:
273-
sources, options = mypy.main.process_options(
274-
['-i'] + list(args),
275-
require_targets=True,
276-
server_options=True,
277-
fscache=self.fscache)
274+
# Process options can exit on improper arguments, so we need to catch that and
275+
# capture stderr so the client can report it
276+
stderr = io.StringIO()
277+
stdout = io.StringIO()
278+
with redirect_stderr(stderr):
279+
with redirect_stdout(stdout):
280+
sources, options = mypy.main.process_options(
281+
['-i'] + list(args),
282+
require_targets=True,
283+
server_options=True,
284+
fscache=self.fscache,
285+
program='mypy-daemon',
286+
header=argparse.SUPPRESS)
278287
# Signal that we need to restart if the options have changed
279288
if self.options_snapshot != options.snapshot():
280289
return {'restart': 'configuration changed'}
@@ -288,6 +297,8 @@ def cmd_run(self, version: str, args: Sequence[str]) -> Dict[str, object]:
288297
return {'restart': 'plugins changed'}
289298
except InvalidSourceList as err:
290299
return {'out': '', 'err': str(err), 'status': 2}
300+
except SystemExit as e:
301+
return {'out': stdout.getvalue(), 'err': stderr.getvalue(), 'status': e.code}
291302
return self.check(sources)
292303

293304
def cmd_check(self, files: Sequence[str]) -> Dict[str, object]:

mypy/main.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,17 @@ def process_options(args: List[str],
297297
require_targets: bool = True,
298298
server_options: bool = False,
299299
fscache: Optional[FileSystemCache] = None,
300+
program: str = 'mypy',
301+
header: str = HEADER,
300302
) -> Tuple[List[BuildSource], Options]:
301303
"""Parse command line arguments.
302304
303305
If a FileSystemCache is passed in, and package_root options are given,
304306
call fscache.set_package_root() to set the cache's package root.
305307
"""
306308

307-
parser = argparse.ArgumentParser(prog='mypy',
308-
usage=HEADER,
309+
parser = argparse.ArgumentParser(prog=program,
310+
usage=header,
309311
description=DESCRIPTION,
310312
epilog=FOOTER,
311313
fromfile_prefix_chars='@',

mypy/util.py

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"""Utility functions with no non-trivial dependencies."""
2+
import contextlib
23
import os
34
import pathlib
45
import re
56
import subprocess
67
import sys
7-
from typing import TypeVar, List, Tuple, Optional, Dict, Sequence
8+
from types import TracebackType
9+
from typing import TypeVar, List, Tuple, Optional, Dict, Sequence, TextIO
810

911
MYPY = False
1012
if MYPY:
@@ -255,3 +257,50 @@ def hard_exit(status: int = 0) -> None:
255257
sys.stdout.flush()
256258
sys.stderr.flush()
257259
os._exit(status)
260+
261+
262+
# The following is a backport of stream redirect utilities from Lib/contextlib.py
263+
# We need this for 3.4 support. They can be removed in March 2019!
264+
265+
266+
class _RedirectStream:
267+
268+
_stream = None # type: str
269+
270+
def __init__(self, new_target: TextIO) -> None:
271+
self._new_target = new_target
272+
# We use a list of old targets to make this CM re-entrant
273+
self._old_targets = [] # type: List[TextIO]
274+
275+
def __enter__(self) -> TextIO:
276+
self._old_targets.append(getattr(sys, self._stream))
277+
setattr(sys, self._stream, self._new_target)
278+
return self._new_target
279+
280+
def __exit__(self,
281+
exc_ty: 'Optional[Type[BaseException]]' = None,
282+
exc_val: Optional[BaseException] = None,
283+
exc_tb: Optional[TracebackType] = None,
284+
) -> bool:
285+
setattr(sys, self._stream, self._old_targets.pop())
286+
return False
287+
288+
289+
class redirect_stdout(_RedirectStream):
290+
"""Context manager for temporarily redirecting stdout to another file.
291+
# How to send help() to stderr
292+
with redirect_stdout(sys.stderr):
293+
help(dir)
294+
# How to write help() to a file
295+
with open('help.txt', 'w') as f:
296+
with redirect_stdout(f):
297+
help(pow)
298+
"""
299+
300+
_stream = "stdout"
301+
302+
303+
class redirect_stderr(_RedirectStream):
304+
"""Context manager for temporarily redirecting stderr to another file."""
305+
306+
_stream = "stderr"

test-data/unit/daemon.test

+8
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,11 @@ Daemon stopped
122122
import bar
123123
[file bar.py]
124124
pass
125+
126+
[case testDaemonRunNoTarget]
127+
$ dmypy run -- --follow-imports=error
128+
Daemon started
129+
mypy-daemon: error: Missing target module, package, files, or command.
130+
== Return code: 2
131+
$ dmypy stop
132+
Daemon stopped

0 commit comments

Comments
 (0)