Skip to content

Commit 8bd3c39

Browse files
authored
Merge pull request #24 from jpneverwas/overrides
Add option to override command line
2 parents 07a5bc2 + d1e32d5 commit 8bd3c39

File tree

3 files changed

+125
-4
lines changed

3 files changed

+125
-4
lines changed

README.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ Configuration
3333
``strict`` (default is False) refers to the ``strict`` option of ``mypy``.
3434
This option often is too strict to be useful.
3535

36+
``overrides`` (default is ``[True]``) specifies a list of alternate or supplemental command-line options.
37+
This modifies the options passed to ``mypy`` or the mypy-specific ones passed to ``dmypy run``. When present, the special boolean member ``True`` is replaced with the command-line options that would've been passed had ``overrides`` not been specified. Later options take precedence, which allows for replacing or negating individual default options (see ``mypy.main:process_options`` and ``mypy --help | grep inverse``).
38+
3639
Depending on your editor, the configuration (found in a file called pylsp-mypy.cfg in your workspace or a parent directory) should be roughly like this for a standard configuration:
3740

3841
::
@@ -54,6 +57,16 @@ With ``dmypy`` enabled your config should look like this:
5457
"strict": False
5558
}
5659

60+
With ``overrides`` specified (for example to tell mypy to use a different python than the currently active venv), your config could look like this:
61+
62+
::
63+
64+
{
65+
"enabled": True,
66+
"overrides": ["--python-executable", "/home/me/bin/python", True]
67+
}
68+
69+
5770
Developing
5871
-------------
5972

pylsp_mypy/plugin.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
9898
return None
9999

100100

101+
def apply_overrides(args: List[str], overrides: List[Any]) -> List[str]:
102+
"""Replace or combine default command-line options with overrides."""
103+
overrides_iterator = iter(overrides)
104+
if True not in overrides_iterator:
105+
return overrides
106+
# If True is in the list, the if above leaves the iterator at the element after True,
107+
# therefore, the list below only contains the elements after the True
108+
rest = list(overrides_iterator)
109+
# slice of the True and the rest, add the args, add the rest
110+
return overrides[: -(len(rest) + 1)] + args + rest
111+
112+
101113
@hookimpl
102114
def pylsp_lint(
103115
config: Config, workspace: Workspace, document: Document, is_saved: bool
@@ -189,8 +201,11 @@ def pylsp_lint(
189201
if settings.get("strict", False):
190202
args.append("--strict")
191203

204+
overrides = settings.get("overrides", [True])
205+
192206
if not dmypy:
193207
args.extend(["--incremental", "--follow-imports", "silent"])
208+
args = apply_overrides(args, overrides)
194209

195210
if shutil.which("mypy"):
196211
# mypy exists on path
@@ -212,10 +227,13 @@ def pylsp_lint(
212227
# If daemon is hung, kill will reset
213228
# If daemon is dead/absent, kill will no-op.
214229
# In either case, reset to fresh state
230+
215231
if shutil.which("dmypy"):
216232
# dmypy exists on path
217233
# -> use mypy on path
218-
completed_process = subprocess.run(["dmypy", *args], stderr=subprocess.PIPE)
234+
completed_process = subprocess.run(
235+
["dmypy", *apply_overrides(args, overrides)], stderr=subprocess.PIPE
236+
)
219237
_err = completed_process.stderr.decode()
220238
_status = completed_process.returncode
221239
if _status != 0:
@@ -234,7 +252,7 @@ def pylsp_lint(
234252
mypy_api.run_dmypy(["kill"])
235253

236254
# run to use existing daemon or restart if required
237-
args = ["run", "--"] + args
255+
args = ["run", "--"] + apply_overrides(args, overrides)
238256

239257
if shutil.which("dmypy"):
240258
# dmypy exists on path

test/test_plugin.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pylsp import uris
66
from unittest.mock import Mock
77
from pylsp_mypy import plugin
8+
import collections
89

910
DOC_URI = __file__
1011
DOC_TYPE_ERR = """{}.append(3)
@@ -16,6 +17,13 @@
1617
TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"'
1718

1819

20+
@pytest.fixture
21+
def last_diagnostics_monkeypatch(monkeypatch):
22+
# gets called before every test altering last_diagnostics in order to reset it
23+
monkeypatch.setattr(plugin, "last_diagnostics", collections.defaultdict(list))
24+
return monkeypatch
25+
26+
1927
@pytest.fixture
2028
def workspace(tmpdir):
2129
"""Return a workspace."""
@@ -38,7 +46,7 @@ def test_settings():
3846
assert settings == {"plugins": {"pylsp_mypy": {}}}
3947

4048

41-
def test_plugin(workspace):
49+
def test_plugin(workspace, last_diagnostics_monkeypatch):
4250
config = FakeConfig()
4351
doc = Document(DOC_URI, workspace, DOC_TYPE_ERR)
4452
plugin.pylsp_settings(config)
@@ -85,7 +93,7 @@ def test_parse_line_with_context(monkeypatch, word, bounds, workspace):
8593
assert diag["range"]["end"] == {"line": 278, "character": bounds[1]}
8694

8795

88-
def test_multiple_workspaces(tmpdir):
96+
def test_multiple_workspaces(tmpdir, last_diagnostics_monkeypatch):
8997
DOC_SOURCE = """
9098
def foo():
9199
return
@@ -120,3 +128,85 @@ def foo():
120128
doc2 = Document(DOC_URI, ws2, DOC_SOURCE)
121129
diags = plugin.pylsp_lint(ws2._config, ws2, doc2, is_saved=False)
122130
assert len(diags) == 0
131+
132+
133+
def test_apply_overrides():
134+
assert plugin.apply_overrides(["1", "2"], []) == []
135+
assert plugin.apply_overrides(["1", "2"], ["a"]) == ["a"]
136+
assert plugin.apply_overrides(["1", "2"], ["a", True]) == ["a", "1", "2"]
137+
assert plugin.apply_overrides(["1", "2"], [True, "a"]) == ["1", "2", "a"]
138+
assert plugin.apply_overrides(["1"], ["a", True, "b"]) == ["a", "1", "b"]
139+
140+
141+
def test_option_overrides(tmpdir, last_diagnostics_monkeypatch, workspace):
142+
import sys
143+
from textwrap import dedent
144+
from stat import S_IRWXU
145+
146+
sentinel = tmpdir / "ran"
147+
148+
source = dedent(
149+
"""\
150+
#!{}
151+
import os, sys, pathlib
152+
pathlib.Path({!r}).touch()
153+
os.execv({!r}, sys.argv)
154+
"""
155+
).format(sys.executable, str(sentinel), sys.executable)
156+
157+
wrapper = tmpdir / "bin/wrapper"
158+
wrapper.write(source, ensure=True)
159+
wrapper.chmod(S_IRWXU)
160+
161+
overrides = ["--python-executable", wrapper.strpath, True]
162+
last_diagnostics_monkeypatch.setattr(
163+
FakeConfig,
164+
"plugin_settings",
165+
lambda _, p: {"overrides": overrides} if p == "pylsp_mypy" else {},
166+
)
167+
168+
assert not sentinel.exists()
169+
170+
diags = plugin.pylsp_lint(
171+
config=FakeConfig(),
172+
workspace=workspace,
173+
document=Document(DOC_URI, workspace, DOC_TYPE_ERR),
174+
is_saved=False,
175+
)
176+
assert len(diags) == 1
177+
assert sentinel.exists()
178+
179+
180+
def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace):
181+
overrides = ["--python-executable", "/tmp/fake", True]
182+
last_diagnostics_monkeypatch.setattr(
183+
FakeConfig,
184+
"plugin_settings",
185+
lambda _, p: {
186+
"overrides": overrides,
187+
"dmypy": True,
188+
"live_mode": False,
189+
}
190+
if p == "pylsp_mypy"
191+
else {},
192+
)
193+
194+
m = Mock(wraps=lambda a, **_: Mock(returncode=0, **{"stdout.decode": lambda: ""}))
195+
last_diagnostics_monkeypatch.setattr(plugin.subprocess, "run", m)
196+
197+
plugin.pylsp_lint(
198+
config=FakeConfig(),
199+
workspace=workspace,
200+
document=Document(DOC_URI, workspace, DOC_TYPE_ERR),
201+
is_saved=False,
202+
)
203+
expected = [
204+
"dmypy",
205+
"run",
206+
"--",
207+
"--python-executable",
208+
"/tmp/fake",
209+
"--show-column-numbers",
210+
__file__,
211+
]
212+
m.assert_called_with(expected, stderr=-1, stdout=-1)

0 commit comments

Comments
 (0)