From d305a0db7afd4dac2700c5625c4deb6587687b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 17 Mar 2024 14:16:47 +0000 Subject: [PATCH 1/4] Fix matching column numbers when they are present In projects which configure 'show_column_numbers = true', pylsp would not return anything back to my editor. In the logs I found messages like below ``` ... discarding result for beetsplug/bandcamp/track.py:113 against /home/sarunas/repo/beetcamp/beetsplug/bandcamp/track.py ``` The logic only returns the result if the latter filename ends with the former, thus it seems that the column number (':113') is not supposed to be present here. Having had a look at the matching pattern I found that it does not take into account that the column number may be present. This commit adjusts the pattern to handle the column number correctly. --- pylsp_mypy/plugin.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 0ca74cc..c98c9df 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -31,17 +31,13 @@ from pylsp.workspace import Document, Workspace line_pattern = re.compile( - ( - r"^(?P.+):(?P\d+):(?P\d*):(?P\d*):(?P\d*): " - r"(?P\w+): (?P.+?)(?: +\[(?P.+)\])?$" - ) + r"^(?P.+):(?P\d+):(?P\d*):(?P\d*):(?P\d*): " + r"(?P\w+): (?P.+?)(?: +\[(?P.+)\])?$" ) whole_line_pattern = re.compile( # certain mypy warnings do not report start-end ranges - ( - r"^(?P.+):(?P\d+): " - r"(?P\w+): (?P.+?)(?: +\[(?P.+)\])?$" - ) + r"^(?P.+?):(?P\d+):(?:(?P\d+):)? " + r"(?P\w+): (?P.+?)(?: +\[(?P.+)\])?$" ) log = logging.getLogger(__name__) @@ -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 != "": # live mode # results from other files can be included, but we cannot return @@ -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"): From 197d77290cadff155bbdd469cd8c9e66dcd8a121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Fri, 26 Apr 2024 12:59:09 +0100 Subject: [PATCH 2/4] Always provide the default plugin options The user is able to override them in any case since later options take precedence. --- README.rst | 6 +++--- pylsp_mypy/plugin.py | 19 +++---------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index d51ea0b..7c5e404 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,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. @@ -87,7 +87,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: diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index c98c9df..e3e569c 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -122,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", []) @@ -295,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 @@ -362,7 +349,7 @@ def get_diagnostics( mypy_api.run_dmypy(["--status-file", dmypy_status_file, "restart"]) # run to use existing daemon or restart if required - args = ["--status-file", dmypy_status_file, "run", "--"] + apply_overrides(args, overrides) + args = ["--status-file", dmypy_status_file, "run", "--", *args] if shutil.which("dmypy"): # dmypy exists on path # -> use mypy on path From 88f059720a38e47773aaacef2537d073c6c0c8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Fri, 26 Apr 2024 13:43:09 +0100 Subject: [PATCH 3/4] Document the command line options the plugin uses --- README.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.rst b/README.rst index 7c5e404..5265d47 100644 --- a/README.rst +++ b/README.rst @@ -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. @@ -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: :: From 47ff1c0c5ed06e1454c11860fcac72eab49d102a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Fri, 26 Apr 2024 13:44:23 +0100 Subject: [PATCH 4/4] Clarify dmypy options, remove --no-error-summary --- pylsp_mypy/plugin.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index e3e569c..545e1d8 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -252,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: @@ -311,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", @@ -329,7 +330,7 @@ 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", @@ -337,19 +338,17 @@ def get_diagnostics( 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", "--", *args] + args = [*dmypy_args, "run", "--", *args] if shutil.which("dmypy"): # dmypy exists on path # -> use mypy on path