Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions pylsp_mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import tempfile
from configparser import ConfigParser
from pathlib import Path
from typing import IO, Any, Dict, List, Optional
from typing import IO, Any, Optional, TypedDict

try:
import tomllib
Expand Down Expand Up @@ -47,9 +47,9 @@
log = logging.getLogger(__name__)

# A mapping from workspace path to config file path
mypyConfigFileMap: Dict[str, Optional[str]] = {}
mypyConfigFileMap: dict[str, Optional[str]] = {}

settingsCache: Dict[str, Dict[str, Any]] = {}
settingsCache: dict[str, dict[str, Any]] = {}

tmpFile: Optional[IO[bytes]] = None

Expand All @@ -58,18 +58,25 @@
# so store a cache of last diagnostics for each file a-la the pylint plugin,
# so we can return some potentially-stale diagnostics.
# https://github.com/python-lsp/python-lsp-server/blob/v1.0.1/pylsp/plugins/pylint_lint.py#L55-L62
last_diagnostics: Dict[str, List[Dict[str, Any]]] = collections.defaultdict(list)
last_diagnostics: dict[str, list[dict[str, Any]]] = collections.defaultdict(list)


# Windows started opening opening a cmd-like window for every subprocess call
# This flag prevents that.
# This flag is new in python 3.7
# This flag only exists on Windows
windows_flag: Dict[str, int] = (
{"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {} # type: ignore
# This flag only exists on Windows, hence the 'type: ignore[attr-defined]' below.
class WindowsFlag(TypedDict, total=False):
creationflags: int


windows_flag: WindowsFlag = (
{"creationflags": subprocess.CREATE_NO_WINDOW} # type: ignore[attr-defined]
if os.name == "nt"
else {}
)


def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[str, Any]]:
def parse_line(line: str, document: Optional[Document] = None) -> Optional[dict[str, Any]]:
"""
Return a language-server diagnostic from a line of the Mypy error report.

Expand Down Expand Up @@ -128,7 +135,7 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
return diag


def apply_overrides(args: List[str], overrides: List[Any]) -> List[str]:
def apply_overrides(args: list[str], overrides: list[Any]) -> list[str]:
"""Replace or combine default command-line options with overrides."""
overrides_iterator = iter(overrides)
if True not in overrides_iterator:
Expand All @@ -140,7 +147,7 @@ def apply_overrides(args: List[str], overrides: List[Any]) -> List[str]:
return overrides[: -(len(rest) + 1)] + args + rest


def didSettingsChange(workspace: str, settings: Dict[str, Any]) -> None:
def didSettingsChange(workspace: str, settings: dict[str, Any]) -> None:
"""Handle relevant changes to the settings between runs."""
configSubPaths = settings.get("config_sub_paths", [])
if settingsCache[workspace].get("config_sub_paths", []) != configSubPaths:
Expand All @@ -154,7 +161,7 @@ def didSettingsChange(workspace: str, settings: Dict[str, Any]) -> None:
settingsCache[workspace] = settings.copy()


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

Expand All @@ -169,14 +176,14 @@ def match_exclude_patterns(document_path: str, exclude_patterns: list) -> bool:
return False


def get_cmd(settings: Dict[str, Any], cmd: str) -> List[str]:
def get_cmd(settings: dict[str, Any], cmd: str) -> list[str]:
"""
Get the command to run from settings, falling back to searching the PATH.
If the command is not found in the settings and is not available on the PATH, an
empty list is returned.
"""
command_key = f"{cmd}_command"
command: List[str] = settings.get(command_key, [])
command: list[str] = settings.get(command_key, [])

if not (command and os.getenv("PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION")):
# env var is required to allow command from settings
Expand All @@ -196,7 +203,7 @@ def get_cmd(settings: Dict[str, Any], cmd: str) -> List[str]:
@hookimpl
def pylsp_lint(
config: Config, workspace: Workspace, document: Document, is_saved: bool
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""
Call the linter.

Expand Down Expand Up @@ -254,9 +261,9 @@ def pylsp_lint(
def get_diagnostics(
workspace: Workspace,
document: Document,
settings: Dict[str, Any],
settings: dict[str, Any],
is_saved: bool,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""
Lints.

Expand Down Expand Up @@ -332,7 +339,7 @@ def get_diagnostics(
args.extend(["--incremental", "--follow-imports", settings.get("follow-imports", "silent")])
args = apply_overrides(args, overrides)

mypy_command: List[str] = get_cmd(settings, "mypy")
mypy_command: list[str] = get_cmd(settings, "mypy")

if mypy_command:
# mypy exists on PATH or was provided by settings
Expand All @@ -357,7 +364,7 @@ def get_diagnostics(
# If daemon is dead/absent, kill will no-op.
# In either case, reset to fresh state

dmypy_command: List[str] = get_cmd(settings, "dmypy")
dmypy_command: list[str] = get_cmd(settings, "dmypy")

if dmypy_command:
# dmypy exists on PATH or was provided by settings
Expand Down Expand Up @@ -449,7 +456,7 @@ def get_diagnostics(


@hookimpl
def pylsp_settings(config: Config) -> Dict[str, Dict[str, Dict[str, str]]]:
def pylsp_settings(config: Config) -> dict[str, dict[str, dict[str, str]]]:
"""
Read the settings.

Expand All @@ -468,7 +475,7 @@ def pylsp_settings(config: Config) -> Dict[str, Dict[str, Dict[str, str]]]:
return {"plugins": {"pylsp_mypy": configuration}}


def init(workspace: str) -> Dict[str, str]:
def init(workspace: str) -> dict[str, str]:
"""
Find plugin and mypy config files and creates the temp file should it be used.

Expand Down Expand Up @@ -509,7 +516,7 @@ def init(workspace: str) -> Dict[str, str]:


def findConfigFile(
path: str, configSubPaths: List[str], names: List[str], mypy: bool
path: str, configSubPaths: list[str], names: list[str], mypy: bool
) -> Optional[str]:
"""
Search for a config file.
Expand Down Expand Up @@ -580,9 +587,9 @@ def pylsp_code_actions(
config: Config,
workspace: Workspace,
document: Document,
range: Dict,
context: Dict,
) -> List[Dict]:
range: dict[str, Any],
context: dict[str, Any],
) -> list[dict[str, Any]]:
"""
Provide code actions to ignore errors.

Expand Down
8 changes: 1 addition & 7 deletions test/test_plugin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import collections
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Dict
from unittest.mock import Mock, patch

import pytest
Expand All @@ -31,10 +29,6 @@
'test_plugin.py:124:1:129:77: note: Use "-> None" if function does not return a value'
)

windows_flag: Dict[str, int] = (
{"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {} # type: ignore
)


@pytest.fixture
def last_diagnostics_monkeypatch(monkeypatch):
Expand Down Expand Up @@ -252,7 +246,7 @@ def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace):
"--no-pretty",
document.path,
]
m.assert_called_with(expected, capture_output=True, **windows_flag, encoding="utf-8")
m.assert_called_with(expected, capture_output=True, **plugin.windows_flag, encoding="utf-8")


def test_dmypy_status_file(tmpdir, last_diagnostics_monkeypatch, workspace):
Expand Down
Loading