diff --git a/README.rst b/README.rst index 6e173a2..d51ea0b 100644 --- a/README.rst +++ b/README.rst @@ -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: :: @@ -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: @@ -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: diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 6558db3..6642d30 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -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 @@ -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) diff --git a/test/test_plugin.py b/test/test_plugin.py index 6f9a3e6..9c08c8d 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -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 @@ -18,7 +19,12 @@ DOC_URI = f"file:/{Path(__file__)}" DOC_TYPE_ERR = """{}.append(3) """ -TYPE_ERR_MSG = '"Dict[, ]" has no attribute "append"' + +# Mypy 1.7 changed into "Never", so make this a regex to be compatible +# with multiple versions of mypy +TYPE_ERR_MSG_REGEX = ( + r'"Dict\[(?:(?:)|(?:Never)), (?:(?:)|(?: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 = ( @@ -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): @@ -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 == []