Skip to content

Commit 2f569f6

Browse files
maerteijnMrGreenTeaRichardk2n
authored
Add exclude option to pylsp-mypy configuration (#71)
* add regex pattern matching to exclude documents from type checking * Move exclude code to pylsp_lint so mypy is not invoked unnecessarily * Use re.search so pattern is matched across the whole path * Improve test so we are sure the exclude section has this side-effect * Document the exclude config option * Make the match_exclude_patterns logic os independent Just convert windows paths to unix (a ilke) paths, so d:\My Documents\my_file.py becomes d:/My Documents/my_file.py Then you can reuse the configured exclude patterns for both windows and unix (a like) platforms. * Update the TYPE_ERR_MSG to be compatible with mypy 1.7 --------- Co-authored-by: Jonas Bulik <[email protected]> Co-authored-by: Richard Kellnberger <[email protected]>
1 parent d72a3c6 commit 2f569f6

File tree

3 files changed

+86
-4
lines changed

3 files changed

+86
-4
lines changed

README.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ Configuration
4545
``report_progress`` (default is ``False``) report basic progress to the LSP client.
4646
With this option, pylsp-mypy will report when mypy is running, given your editor supports LSP progress reporting. For small files this might produce annoying flashing in your editor, especially in with ``live_mode``. For large projects, enabling this can be helpful to assure yourself whether mypy is still running.
4747

48+
``exclude`` (default is ``[]``) A list of regular expressions which should be ignored.
49+
The ``mypy`` runner wil not be invoked when a document path is matched by one of the expressions. Note that this differs from the ``exclude`` directive of a ``mypy`` config which is only used for recursively discovering files when mypy is invoked on a whole directory. For both windows or unix platforms you should use forward slashes (``/``) to indicate paths.
50+
4851
This project supports the use of ``pyproject.toml`` for configuration. It is in fact the preferred way. Using that your configuration could look like this:
4952

5053
::
@@ -53,6 +56,7 @@ This project supports the use of ``pyproject.toml`` for configuration. It is in
5356
enabled = true
5457
live_mode = true
5558
strict = true
59+
exclude = ["tests/*"]
5660

5761
A ``pyproject.toml`` does not conflict with the legacy config file given that it does not contain a ``pylsp-mypy`` section. The following explanation uses the syntax of the legacy config file. However, all these options also apply to the ``pyproject.toml`` configuration (note the lowercase bools).
5862
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:
@@ -62,7 +66,8 @@ Depending on your editor, the configuration (found in a file called pylsp-mypy.c
6266
{
6367
"enabled": True,
6468
"live_mode": True,
65-
"strict": False
69+
"strict": False,
70+
"exclude": ["tests/*"]
6671
}
6772

6873
With ``dmypy`` enabled your config should look like this:

pylsp_mypy/plugin.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,21 @@ def didSettingsChange(workspace: str, settings: Dict[str, Any]) -> None:
142142
settingsCache[workspace] = settings.copy()
143143

144144

145+
def match_exclude_patterns(document_path: str, exclude_patterns: list) -> bool:
146+
"""Check if the current document path matches any of the configures exlude patterns."""
147+
document_path = document_path.replace(os.sep, "/")
148+
149+
for pattern in exclude_patterns:
150+
try:
151+
if re.search(pattern, document_path):
152+
log.debug(f"{document_path} matches " f"exclude pattern '{pattern}'")
153+
return True
154+
except re.error as e:
155+
log.error(f"pattern {pattern} is not a valid regular expression: {e}")
156+
157+
return False
158+
159+
145160
@hookimpl
146161
def pylsp_lint(
147162
config: Config, workspace: Workspace, document: Document, is_saved: bool
@@ -181,6 +196,18 @@ def pylsp_lint(
181196

182197
didSettingsChange(workspace.root_path, settings)
183198

199+
# Running mypy with a single file (document) ignores any exclude pattern
200+
# configured with mypy. We can now add our own exclude section like so:
201+
# [tool.pylsp-mypy]
202+
# exclude = ["tests/*"]
203+
exclude_patterns = settings.get("exclude", [])
204+
205+
if match_exclude_patterns(document_path=document.path, exclude_patterns=exclude_patterns):
206+
log.debug(
207+
f"Not running because {document.path} matches " f"exclude patterns '{exclude_patterns}'"
208+
)
209+
return []
210+
184211
if settings.get("report_progress", False):
185212
with workspace.report_progress("lint: mypy"):
186213
return get_diagnostics(workspace, document, settings, is_saved)

test/test_plugin.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import collections
22
import os
3+
import re
34
import subprocess
45
import sys
56
from pathlib import Path
67
from typing import Dict
7-
from unittest.mock import Mock
8+
from unittest.mock import Mock, patch
89

910
import pytest
1011
from mypy import api as mypy_api
@@ -18,7 +19,12 @@
1819
DOC_URI = f"file:/{Path(__file__)}"
1920
DOC_TYPE_ERR = """{}.append(3)
2021
"""
21-
TYPE_ERR_MSG = '"Dict[<nothing>, <nothing>]" has no attribute "append"'
22+
23+
# Mypy 1.7 changed <nothing> into "Never", so make this a regex to be compatible
24+
# with multiple versions of mypy
25+
TYPE_ERR_MSG_REGEX = (
26+
r'"Dict\[(?:(?:<nothing>)|(?:Never)), (?:(?:<nothing>)|(?:Never))\]" has no attribute "append"'
27+
)
2228

2329
TEST_LINE = 'test_plugin.py:279:8:279:16: error: "Request" has no attribute "id" [attr-defined]'
2430
TEST_LINE_NOTE = (
@@ -66,7 +72,7 @@ def test_plugin(workspace, last_diagnostics_monkeypatch):
6672

6773
assert len(diags) == 1
6874
diag = diags[0]
69-
assert diag["message"] == TYPE_ERR_MSG
75+
assert re.fullmatch(TYPE_ERR_MSG_REGEX, diag["message"])
7076
assert diag["range"]["start"] == {"line": 0, "character": 0}
7177
# Running mypy in 3.7 produces wrong error ends this can be removed when 3.7 reaches EOL
7278
if sys.version_info < (3, 8):
@@ -328,3 +334,47 @@ def foo():
328334
diag = diags[0]
329335
assert diag["message"] == DOC_ERR_MSG
330336
assert diag["code"] == "unreachable"
337+
338+
339+
@pytest.mark.parametrize(
340+
"document_path,pattern,os_sep,pattern_matched",
341+
(
342+
("/workspace/my-file.py", "/someting-else", "/", False),
343+
("/workspace/my-file.py", "^/workspace$", "/", False),
344+
("/workspace/my-file.py", "/workspace", "/", True),
345+
("/workspace/my-file.py", "^/workspace(.*)$", "/", True),
346+
# This is a broken regex (missing ')'), but should not choke
347+
("/workspace/my-file.py", "/((workspace)", "/", False),
348+
# Windows paths are tricky with all those \\ and unintended escape,
349+
# characters but they should 'just' work
350+
("d:\\a\\my-file.py", "/a", "\\", True),
351+
(
352+
"d:\\a\\pylsp-mypy\\pylsp-mypy\\test\\test_plugin.py",
353+
"/a/pylsp-mypy/pylsp-mypy/test/test_plugin.py",
354+
"\\",
355+
True,
356+
),
357+
),
358+
)
359+
def test_match_exclude_patterns(document_path, pattern, os_sep, pattern_matched):
360+
with patch("os.sep", new=os_sep):
361+
assert (
362+
plugin.match_exclude_patterns(document_path=document_path, exclude_patterns=[pattern])
363+
is pattern_matched
364+
)
365+
366+
367+
def test_config_exclude(tmpdir, workspace):
368+
"""When exclude is set in config then mypy should not run for that file."""
369+
doc = Document(DOC_URI, workspace, DOC_TYPE_ERR)
370+
371+
plugin.pylsp_settings(workspace._config)
372+
workspace.update_config({"pylsp": {"plugins": {"pylsp_mypy": {}}}})
373+
diags = plugin.pylsp_lint(workspace._config, workspace, doc, is_saved=False)
374+
assert re.search(TYPE_ERR_MSG_REGEX, diags[0]["message"])
375+
376+
# Add the path of our document to the exclude patterns
377+
exclude_path = doc.path.replace(os.sep, "/")
378+
workspace.update_config({"pylsp": {"plugins": {"pylsp_mypy": {"exclude": [exclude_path]}}}})
379+
diags = plugin.pylsp_lint(workspace._config, workspace, doc, is_saved=False)
380+
assert diags == []

0 commit comments

Comments
 (0)