diff --git a/mypy/dmypy.py b/mypy/dmypy.py index d8ca2a34a743..06211e3fe008 100644 --- a/mypy/dmypy.py +++ b/mypy/dmypy.py @@ -1,7 +1,5 @@ """Client for mypy daemon mode. -Highly experimental! Only supports UNIX-like systems. - This manages a daemon process which keeps useful state in memory rather than having to read it back from disk on each run. """ diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index ee1b97cbbdd9..01ee1e8deff1 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -1,12 +1,12 @@ """Server for mypy daemon mode. -Only supports UNIX-like systems. - This implements a daemon process which keeps useful state in memory to enable fine-grained incremental reprocessing of changes. """ +import argparse import base64 +import io import json import os import pickle @@ -29,6 +29,7 @@ from mypy.modulefinder import BuildSource, compute_search_paths from mypy.options import Options from mypy.typestate import reset_global_state +from mypy.util import redirect_stderr, redirect_stdout from mypy.version import __version__ @@ -270,11 +271,19 @@ def cmd_stop(self) -> Dict[str, object]: def cmd_run(self, version: str, args: Sequence[str]) -> Dict[str, object]: """Check a list of files, triggering a restart if needed.""" try: - sources, options = mypy.main.process_options( - ['-i'] + list(args), - require_targets=True, - server_options=True, - fscache=self.fscache) + # Process options can exit on improper arguments, so we need to catch that and + # capture stderr so the client can report it + stderr = io.StringIO() + stdout = io.StringIO() + with redirect_stderr(stderr): + with redirect_stdout(stdout): + sources, options = mypy.main.process_options( + ['-i'] + list(args), + require_targets=True, + server_options=True, + fscache=self.fscache, + program='mypy-daemon', + header=argparse.SUPPRESS) # Signal that we need to restart if the options have changed if self.options_snapshot != options.snapshot(): return {'restart': 'configuration changed'} @@ -288,6 +297,8 @@ def cmd_run(self, version: str, args: Sequence[str]) -> Dict[str, object]: return {'restart': 'plugins changed'} except InvalidSourceList as err: return {'out': '', 'err': str(err), 'status': 2} + except SystemExit as e: + return {'out': stdout.getvalue(), 'err': stderr.getvalue(), 'status': e.code} return self.check(sources) def cmd_check(self, files: Sequence[str]) -> Dict[str, object]: diff --git a/mypy/main.py b/mypy/main.py index 6f6cbf92e51f..87051eaa1d0d 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -297,6 +297,8 @@ def process_options(args: List[str], require_targets: bool = True, server_options: bool = False, fscache: Optional[FileSystemCache] = None, + program: str = 'mypy', + header: str = HEADER, ) -> Tuple[List[BuildSource], Options]: """Parse command line arguments. @@ -304,8 +306,8 @@ def process_options(args: List[str], call fscache.set_package_root() to set the cache's package root. """ - parser = argparse.ArgumentParser(prog='mypy', - usage=HEADER, + parser = argparse.ArgumentParser(prog=program, + usage=header, description=DESCRIPTION, epilog=FOOTER, fromfile_prefix_chars='@', diff --git a/mypy/util.py b/mypy/util.py index 02a05f8231b7..f10d588055e0 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -1,10 +1,12 @@ """Utility functions with no non-trivial dependencies.""" +import contextlib import os import pathlib import re import subprocess import sys -from typing import TypeVar, List, Tuple, Optional, Dict, Sequence +from types import TracebackType +from typing import TypeVar, List, Tuple, Optional, Dict, Sequence, TextIO MYPY = False if MYPY: @@ -255,3 +257,50 @@ def hard_exit(status: int = 0) -> None: sys.stdout.flush() sys.stderr.flush() os._exit(status) + + +# The following is a backport of stream redirect utilities from Lib/contextlib.py +# We need this for 3.4 support. They can be removed in March 2019! + + +class _RedirectStream: + + _stream = None # type: str + + def __init__(self, new_target: TextIO) -> None: + self._new_target = new_target + # We use a list of old targets to make this CM re-entrant + self._old_targets = [] # type: List[TextIO] + + def __enter__(self) -> TextIO: + self._old_targets.append(getattr(sys, self._stream)) + setattr(sys, self._stream, self._new_target) + return self._new_target + + def __exit__(self, + exc_ty: 'Optional[Type[BaseException]]' = None, + exc_val: Optional[BaseException] = None, + exc_tb: Optional[TracebackType] = None, + ) -> bool: + setattr(sys, self._stream, self._old_targets.pop()) + return False + + +class redirect_stdout(_RedirectStream): + """Context manager for temporarily redirecting stdout to another file. + # How to send help() to stderr + with redirect_stdout(sys.stderr): + help(dir) + # How to write help() to a file + with open('help.txt', 'w') as f: + with redirect_stdout(f): + help(pow) + """ + + _stream = "stdout" + + +class redirect_stderr(_RedirectStream): + """Context manager for temporarily redirecting stderr to another file.""" + + _stream = "stderr" diff --git a/test-data/unit/daemon.test b/test-data/unit/daemon.test index 42e74b393106..6a5e6fcf82e1 100644 --- a/test-data/unit/daemon.test +++ b/test-data/unit/daemon.test @@ -122,3 +122,11 @@ Daemon stopped import bar [file bar.py] pass + +[case testDaemonRunNoTarget] +$ dmypy run -- --follow-imports=error +Daemon started +mypy-daemon: error: Missing target module, package, files, or command. +== Return code: 2 +$ dmypy stop +Daemon stopped