-
Notifications
You must be signed in to change notification settings - Fork 285
/
Copy pathmypy_lint.py
158 lines (125 loc) · 5.25 KB
/
mypy_lint.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# Copyright 2017 Palantir Technologies, Inc.
import hashlib
import logging
import threading
import re
import sys
import time
from mypy import dmypy, dmypy_server, fscache, main, version
from pyls import hookimpl, lsp, uris
log = logging.getLogger(__name__)
MYPY_RE = re.compile(r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)")
@hookimpl
def pyls_initialize(workspace):
log.info("Launching mypy server")
thread = threading.Thread(target=launch_daemon, args=([], workspace))
thread.daemon = True
thread.start()
@hookimpl
def pyls_lint(document):
args = _parse_daemon_args([document.path])
log.debug("Sending request to mypy daemon", args)
response = dmypy.request('run', version=version.__version__, args=args.flags)
log.debug("Got response from mypy daemon: %s", response)
# If the daemon signals that a restart is necessary, do it
if 'restart' in response:
# TODO(gatesn): figure out how to restart daemon
log.error("Need to restart daemon")
sys.exit("Need to restart mypy daemon")
# print('Restarting: {}'.format(response['restart']))
# restart_server(args, allow_sources=True)
# response = request('run', version=version.__version__, args=args.flags)
try:
stdout, stderr, status_code = response['out'], response['err'], response['status']
if stderr:
log.warning("Mypy stderr: %s", stderr)
return _process_mypy_output(stdout, document)
except KeyError:
log.error("Unknown mypy daemon response: %s", response)
def _process_mypy_output(stdout, document):
for line in stdout.splitlines():
result = re.match(MYPY_RE, line)
if not result:
log.warning("Failed to parse mypy output: %s", line)
continue
_, lineno, offset, level, msg = result.groups()
lineno = (int(lineno) or 1) - 1
offset = (int(offset) or 1) - 1 # mypy says column numbers are zero-based, but they seem not to be
if level == "error":
severity = lsp.DiagnosticSeverity.Error
elif level == "warning":
severity = lsp.DiagnosticSeverity.Warning
elif level == "note":
severity = lsp.DiagnosticSeverity.Information
else:
log.warning("Unknown mypy severity: %s", level)
continue
diag = {
'source': 'mypy',
'range': {
'start': {'line': lineno, 'character': offset},
# There may be a better solution, but mypy does not provide end
'end': {'line': lineno, 'character': offset + 1}
},
'message': msg,
'severity': severity
}
# Try and guess the end of the word that mypy is highlighting
word = document.word_at_position(diag['range']['start'])
if word:
diag['range']['end']['character'] = offset + len(word)
yield diag
def launch_daemon(raw_args, workspace):
"""Launch the mypy daemon in-process."""
args = _parse_daemon_args(raw_args)
_sources, options = main.process_options(
['-i'] + args.flags, require_targets=False, server_options=True
)
server = dmypy_server.Server(options)
server.fscache = PylsFileSystemCache(workspace)
server.serve()
log.error("mypy daemon stopped serving requests")
def _parse_daemon_args(raw_args):
# TODO(gatesn): Take extra arguments from pyls config
return dmypy.parser.parse_args([
'run', '--',
'--show-traceback',
'--follow-imports=skip',
'--show-column-numbers',
] + raw_args)
class PylsFileSystemCache(fscache.FileSystemCache):
"""Patched implementation of FileSystemCache to read from workspace."""
def __init__(self, workspace):
self._workspace = workspace
self._checksums = {}
self._mtimes = {}
super(PylsFileSystemCache, self).__init__()
def stat(self, path):
stat = super(PylsFileSystemCache, self).stat(path)
uri = uris.from_fs_path(path)
document = self._workspace.documents.get(uri)
if document:
size = len(document.source.encode('utf-8'))
mtime = self._workspace.get_document_mtime(uri)
log.debug("Patching os.stat response with size %s and mtime %s", size, mtime)
return MutableOsState(stat, {'st_size': size, 'st_mtime': mtime})
return stat
def read(self, path):
document = self._workspace.documents.get(uris.from_fs_path(path))
if document:
return document.source.encode('utf-8') # Workspace returns unicode, we need bytes
return super(PylsFileSystemCache, self).read(path)
def md5(self, path):
document = self._workspace.documents.get(uris.from_fs_path(path))
if document:
return hashlib.md5(document.source.encode('utf-8')).hexdigest()
return super(PylsFileSystemCache, self).read(path)
class MutableOsState(object):
"""Wrapper around a stat_result that allows us to override values."""
def __init__(self, stat_result, overrides):
self._stat_result = stat_result
self._overrides = overrides
def __getattr__(self, item):
if item in self._overrides:
return self._overrides[item]
return getattr(self._stat_result, item)