Skip to content

Handle custom overrides which may not include show_column_numbers or show_error_end settings #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
23 changes: 20 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Install into the same virtualenv as python-lsp-server itself.
Configuration
-------------

Options
~~~~~~~

``live_mode`` (default is True) provides type checking as you type.
This writes to a tempfile every time a check is done. Turning off ``live_mode`` means you must save your changes for mypy diagnostics to update correctly.

Expand All @@ -33,8 +36,8 @@ Configuration
``strict`` (default is False) refers to the ``strict`` option of ``mypy``.
This option often is too strict to be useful.

``overrides`` (default is ``[True]``) specifies a list of alternate or supplemental command-line options.
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``).
``overrides`` (default is ``[]``) specifies a list of supplemental command-line options.
This modifies the options passed to ``mypy`` or the mypy-specific ones passed to ``dmypy run``. Later options take precedence, which allows for replacing or negating individual default options (see ``mypy.main:process_options`` and ``mypy --help | grep inverse``).

``dmypy_status_file`` (Default is ``.dmypy.json``) specifies which status file dmypy should use.
This modifies the ``--status-file`` option passed to ``dmypy`` given ``dmypy`` is active.
Expand All @@ -48,6 +51,20 @@ Configuration
``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.

Overrides
~~~~~~~~~

The plugin invokes both ``mypy`` and ``dmypy`` with the following options

- ``--show-error-end`` which allows us to report which characters need to be highlighted by the LSP.
- ``--config-file`` with the resolved configuration file

For ``mypy``, the plugin additionally adds ``--incremental`` and ``--follow-imports=silent`` options. These can be overriden using the ``overrides`` configuration option.


Configuration file
~~~~~~~~~~~~~~~~~~

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 Down Expand Up @@ -87,7 +104,7 @@ With ``overrides`` specified (for example to tell mypy to use a different python

{
"enabled": True,
"overrides": ["--python-executable", "/home/me/bin/python", True]
"overrides": ["--python-executable", "/home/me/bin/python"]
}

With ``dmypy_status_file`` your config could look like this:
Expand Down
56 changes: 20 additions & 36 deletions pylsp_mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,13 @@
from pylsp.workspace import Document, Workspace

line_pattern = re.compile(
(
r"^(?P<file>.+):(?P<start_line>\d+):(?P<start_col>\d*):(?P<end_line>\d*):(?P<end_col>\d*): "
r"(?P<severity>\w+): (?P<message>.+?)(?: +\[(?P<code>.+)\])?$"
)
r"^(?P<file>.+):(?P<start_line>\d+):(?P<start_col>\d*):(?P<end_line>\d*):(?P<end_col>\d*): "
r"(?P<severity>\w+): (?P<message>.+?)(?: +\[(?P<code>.+)\])?$"
)

whole_line_pattern = re.compile( # certain mypy warnings do not report start-end ranges
(
r"^(?P<file>.+):(?P<start_line>\d+): "
r"(?P<severity>\w+): (?P<message>.+?)(?: +\[(?P<code>.+)\])?$"
)
r"^(?P<file>.+?):(?P<start_line>\d+):(?:(?P<start_col>\d+):)? "
r"(?P<severity>\w+): (?P<message>.+?)(?: +\[(?P<code>.+)\])?$"
)

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -89,11 +85,13 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
The dict with the lint data.

"""
result = line_pattern.match(line) or whole_line_pattern.match(line)
line_match = line_pattern.match(line) or whole_line_pattern.match(line)

if not result:
if not line_match:
return None

result = line_match.groupdict()

file_path = result["file"]
if file_path != "<string>": # live mode
# results from other files can be included, but we cannot return
Expand All @@ -103,9 +101,9 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
return None

lineno = int(result["start_line"]) - 1 # 0-based line number
offset = int(result.groupdict().get("start_col", 1)) - 1 # 0-based offset
end_lineno = int(result.groupdict().get("end_line", lineno + 1)) - 1
end_offset = int(result.groupdict().get("end_col", 1)) # end is exclusive
offset = int(result.get("start_col", 1)) - 1 # 0-based offset
end_lineno = int(result.get("end_line", lineno + 1)) - 1
end_offset = int(result.get("end_col", 1)) # end is exclusive

severity = result["severity"]
if severity not in ("error", "note"):
Expand All @@ -124,18 +122,6 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
}


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:
return overrides
# If True is in the list, the if above leaves the iterator at the element after True,
# therefore, the list below only contains the elements after the True
rest = list(overrides_iterator)
# slice of the True and the rest, add the args, add the rest
return overrides[: -(len(rest) + 1)] + args + rest


def didSettingsChange(workspace: str, settings: Dict[str, Any]) -> None:
"""Handle relevant changes to the settings between runs."""
configSubPaths = settings.get("config_sub_paths", [])
Expand Down Expand Up @@ -266,7 +252,7 @@ def get_diagnostics(
if dmypy:
dmypy_status_file = settings.get("dmypy_status_file", ".dmypy.json")

args = ["--show-error-end", "--no-error-summary"]
args = ["--show-error-end"]

global tmpFile
if live_mode and not is_saved:
Expand Down Expand Up @@ -297,12 +283,11 @@ def get_diagnostics(
if settings.get("strict", False):
args.append("--strict")

overrides = settings.get("overrides", [True])
args.extend(settings.get("overrides", []))
exit_status = 0

if not dmypy:
args.extend(["--incremental", "--follow-imports", "silent"])
args = apply_overrides(args, overrides)
args = ["--incremental", "--follow-imports", "silent", *args]

if shutil.which("mypy"):
# mypy exists on path
Expand All @@ -326,11 +311,12 @@ def get_diagnostics(
# If daemon is dead/absent, kill will no-op.
# In either case, reset to fresh state

dmypy_args = ["--status-file", dmypy_status_file]
if shutil.which("dmypy"):
# dmypy exists on path
# -> use dmypy on path
completed_process = subprocess.run(
["dmypy", "--status-file", dmypy_status_file, "status"],
["dmypy", *dmypy_args, "status"],
capture_output=True,
**windows_flag,
encoding="utf-8",
Expand All @@ -344,27 +330,25 @@ def get_diagnostics(
errors.strip(),
)
subprocess.run(
["dmypy", "--status-file", dmypy_status_file, "restart"],
["dmypy", *dmypy_args, "restart"],
capture_output=True,
**windows_flag,
encoding="utf-8",
)
else:
# dmypy does not exist on path, but must exist in the env pylsp-mypy is installed in
# -> use dmypy via api
_, errors, exit_status = mypy_api.run_dmypy(
["--status-file", dmypy_status_file, "status"]
)
_, errors, exit_status = mypy_api.run_dmypy([*dmypy_args, "status"])
if exit_status != 0:
log.info(
"restarting dmypy from status: %s message: %s via api",
exit_status,
errors.strip(),
)
mypy_api.run_dmypy(["--status-file", dmypy_status_file, "restart"])
mypy_api.run_dmypy([*dmypy_args, "restart"])

# run to use existing daemon or restart if required
args = ["--status-file", dmypy_status_file, "run", "--"] + apply_overrides(args, overrides)
args = [*dmypy_args, "run", "--", *args]
if shutil.which("dmypy"):
# dmypy exists on path
# -> use mypy on path
Expand Down