Skip to content

Commit

Permalink
Add exclude option to pylsp-mypy configuration (#71)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people committed Nov 15, 2023
1 parent d72a3c6 commit 2f569f6
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 4 deletions.
7 changes: 6 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ Configuration
``report_progress`` (default is ``False``) report basic progress to the LSP client.
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.

``exclude`` (default is ``[]``) A list of regular expressions which should be ignored.
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.

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:

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

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).
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:
Expand All @@ -62,7 +66,8 @@ Depending on your editor, the configuration (found in a file called pylsp-mypy.c
{
"enabled": True,
"live_mode": True,
"strict": False
"strict": False,
"exclude": ["tests/*"]
}

With ``dmypy`` enabled your config should look like this:
Expand Down
27 changes: 27 additions & 0 deletions pylsp_mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@ def didSettingsChange(workspace: str, settings: Dict[str, Any]) -> None:
settingsCache[workspace] = settings.copy()


def match_exclude_patterns(document_path: str, exclude_patterns: list) -> bool:
"""Check if the current document path matches any of the configures exlude patterns."""
document_path = document_path.replace(os.sep, "/")

for pattern in exclude_patterns:
try:
if re.search(pattern, document_path):
log.debug(f"{document_path} matches " f"exclude pattern '{pattern}'")
return True
except re.error as e:
log.error(f"pattern {pattern} is not a valid regular expression: {e}")

return False


@hookimpl
def pylsp_lint(
config: Config, workspace: Workspace, document: Document, is_saved: bool
Expand Down Expand Up @@ -181,6 +196,18 @@ def pylsp_lint(

didSettingsChange(workspace.root_path, settings)

# Running mypy with a single file (document) ignores any exclude pattern
# configured with mypy. We can now add our own exclude section like so:
# [tool.pylsp-mypy]
# exclude = ["tests/*"]
exclude_patterns = settings.get("exclude", [])

if match_exclude_patterns(document_path=document.path, exclude_patterns=exclude_patterns):
log.debug(
f"Not running because {document.path} matches " f"exclude patterns '{exclude_patterns}'"
)
return []

if settings.get("report_progress", False):
with workspace.report_progress("lint: mypy"):
return get_diagnostics(workspace, document, settings, is_saved)
Expand Down
56 changes: 53 additions & 3 deletions test/test_plugin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import collections
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Dict
from unittest.mock import Mock
from unittest.mock import Mock, patch

import pytest
from mypy import api as mypy_api
Expand All @@ -18,7 +19,12 @@
DOC_URI = f"file:/{Path(__file__)}"
DOC_TYPE_ERR = """{}.append(3)
"""
TYPE_ERR_MSG = '"Dict[<nothing>, <nothing>]" has no attribute "append"'

# Mypy 1.7 changed <nothing> into "Never", so make this a regex to be compatible
# with multiple versions of mypy
TYPE_ERR_MSG_REGEX = (
r'"Dict\[(?:(?:<nothing>)|(?:Never)), (?:(?:<nothing>)|(?:Never))\]" has no attribute "append"'
)

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

assert len(diags) == 1
diag = diags[0]
assert diag["message"] == TYPE_ERR_MSG
assert re.fullmatch(TYPE_ERR_MSG_REGEX, diag["message"])
assert diag["range"]["start"] == {"line": 0, "character": 0}
# Running mypy in 3.7 produces wrong error ends this can be removed when 3.7 reaches EOL
if sys.version_info < (3, 8):
Expand Down Expand Up @@ -328,3 +334,47 @@ def foo():
diag = diags[0]
assert diag["message"] == DOC_ERR_MSG
assert diag["code"] == "unreachable"


@pytest.mark.parametrize(
"document_path,pattern,os_sep,pattern_matched",
(
("/workspace/my-file.py", "/someting-else", "/", False),
("/workspace/my-file.py", "^/workspace$", "/", False),
("/workspace/my-file.py", "/workspace", "/", True),
("/workspace/my-file.py", "^/workspace(.*)$", "/", True),
# This is a broken regex (missing ')'), but should not choke
("/workspace/my-file.py", "/((workspace)", "/", False),
# Windows paths are tricky with all those \\ and unintended escape,
# characters but they should 'just' work
("d:\\a\\my-file.py", "/a", "\\", True),
(
"d:\\a\\pylsp-mypy\\pylsp-mypy\\test\\test_plugin.py",
"/a/pylsp-mypy/pylsp-mypy/test/test_plugin.py",
"\\",
True,
),
),
)
def test_match_exclude_patterns(document_path, pattern, os_sep, pattern_matched):
with patch("os.sep", new=os_sep):
assert (
plugin.match_exclude_patterns(document_path=document_path, exclude_patterns=[pattern])
is pattern_matched
)


def test_config_exclude(tmpdir, workspace):
"""When exclude is set in config then mypy should not run for that file."""
doc = Document(DOC_URI, workspace, DOC_TYPE_ERR)

plugin.pylsp_settings(workspace._config)
workspace.update_config({"pylsp": {"plugins": {"pylsp_mypy": {}}}})
diags = plugin.pylsp_lint(workspace._config, workspace, doc, is_saved=False)
assert re.search(TYPE_ERR_MSG_REGEX, diags[0]["message"])

# Add the path of our document to the exclude patterns
exclude_path = doc.path.replace(os.sep, "/")
workspace.update_config({"pylsp": {"plugins": {"pylsp_mypy": {"exclude": [exclude_path]}}}})
diags = plugin.pylsp_lint(workspace._config, workspace, doc, is_saved=False)
assert diags == []

0 comments on commit 2f569f6

Please sign in to comment.