Skip to content

Commit 2a4ed1e

Browse files
committed
Add dmypy support via mypy.api.run_dmypy
palantir/python-language-server#391 Update plugin flow for non-live-mode dmypy invocation via run_dmypy. Minor fix to update detect `.mypy.ini` as well as `mypy.ini`.
1 parent 6ed04c1 commit 2a4ed1e

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed

README.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,18 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg
3535
"live_mode": True,
3636
"strict": False
3737
}
38+
39+
``dmypy`` (default is False) executes via `dmypy run` rather than `mypy`.
40+
41+
This uses the `dmypy` daemon and may dramatically improve the responsiveness of the `pyls` server.
42+
43+
Depending on your editor, the configuration (found in a file called mypy-ls.cfg in your workspace or a parent directory) should be roughly like this:
44+
45+
::
46+
47+
{
48+
"enabled": True,
49+
"live_mode": False,
50+
"dmypy": True,
51+
"strict": False
52+
}

mypy_ls/plugin.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from pyls.config.config import Config
1818
from typing import Optional, Dict, Any, IO, List
1919
import atexit
20+
import collections
2021

2122
line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)"
2223

@@ -26,6 +27,12 @@
2627

2728
tmpFile: Optional[IO[str]] = None
2829

30+
# In non-live-mode the file contents aren't updated.
31+
# Returning an empty diagnostic clears the diagnostic result,
32+
# so store a cache of last diagnostics for each file a-la the pylint plugin,
33+
# so we can return some potentially-stale diagnostics.
34+
# https://github.com/palantir/python-language-server/blob/0.36.2/pyls/plugins/pylint_lint.py#L52-L59
35+
last_diagnostics : Dict[str, List] = collections.defaultdict(list)
2936

3037
def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[str, Any]]:
3138
"""
@@ -111,35 +118,77 @@ def pyls_lint(config: Config, workspace: Workspace, document: Document,
111118
112119
"""
113120
settings = config.plugin_settings('mypy-ls')
121+
log.info(
122+
"lint settings = %s document.path = %s is_saved = %s",
123+
settings,
124+
document.path,
125+
is_saved
126+
)
127+
114128
live_mode = settings.get('live_mode', True)
115-
args = ['--incremental',
116-
'--show-column-numbers',
117-
'--follow-imports', 'silent']
129+
dmypy = settings.get("dmypy", False)
130+
131+
if dmypy and live_mode:
132+
# dmypy can only be efficiently run on files that have been saved, see:
133+
# https://github.com/python/mypy/issues/9309
134+
log.warning("live_mode is not supported with dmypy, disabling")
135+
live_mode = False
136+
137+
args = ['--show-column-numbers']
118138

119139
global tmpFile
120140
if live_mode and not is_saved and tmpFile:
141+
log.info("live_mode tmpFile = %s", live_mode)
121142
tmpFile = open(tmpFile.name, "w")
122143
tmpFile.write(document.source)
123144
tmpFile.close()
124145
args.extend(['--shadow-file', document.path, tmpFile.name])
125-
elif not is_saved:
126-
return []
146+
elif not is_saved and document.path in last_diagnostics:
147+
# On-launch the document isn't marked as saved, so fall through and run
148+
# the diagnostics anyway even if the file contents may be out of date.
149+
log.info(
150+
"non-live, returning cached diagnostics len(cached) = %s",
151+
last_diagnostics[document.path]
152+
)
153+
return last_diagnostics[document.path]
127154

128155
if mypyConfigFile:
129156
args.append('--config-file')
130157
args.append(mypyConfigFile)
158+
131159
args.append(document.path)
160+
132161
if settings.get('strict', False):
133162
args.append('--strict')
134163

135-
report, errors, _ = mypy_api.run(args)
164+
if not dmypy:
165+
args.extend([
166+
"--incremental",
167+
"--follow-imports",
168+
"silent"
169+
])
170+
171+
log.info(f"executing mypy {args=}")
172+
report, errors, _ = mypy_api.run(args)
173+
else:
174+
args = ["run", "--"] + args
175+
176+
log.info(f"executing dmypy {args=}")
177+
report, errors, _ = mypy_api.run_dmypy(args)
178+
179+
log.debug("report: \n" + report)
180+
log.debug("errors: \n" + errors)
136181

137182
diagnostics = []
138183
for line in report.splitlines():
184+
log.debug(f"parsing: {line=}")
139185
diag = parse_line(line, document)
140186
if diag:
141187
diagnostics.append(diag)
142188

189+
logging.info("mypy-ls len(diagnostics) = %s", len(diagnostics))
190+
191+
last_diagnostics[document.path] = diagnostics
143192
return diagnostics
144193

145194

@@ -179,19 +228,27 @@ def init(workspace: str) -> Dict[str, str]:
179228
180229
"""
181230
# On windows the path contains \\ on linux it contains / all the code works with /
231+
log.info(f"init {workspace=}")
182232
workspace = workspace.replace("\\", "/")
233+
183234
configuration = {}
184235
path = findConfigFile(workspace, "mypy-ls.cfg")
185236
if path:
186237
with open(path) as file:
187238
configuration = eval(file.read())
239+
188240
global mypyConfigFile
189241
mypyConfigFile = findConfigFile(workspace, "mypy.ini")
242+
if not mypyConfigFile:
243+
mypyConfigFile = findConfigFile(workspace, ".mypy.ini")
244+
190245
if (("enabled" not in configuration or configuration["enabled"])
191246
and ("live_mode" not in configuration or configuration["live_mode"])):
192247
global tmpFile
193248
tmpFile = tempfile.NamedTemporaryFile('w', delete=False)
194249
tmpFile.close()
250+
251+
log.info(f"{mypyConfigFile=} {configuration=}")
195252
return configuration
196253

197254

0 commit comments

Comments
 (0)