././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1770048086.6609313 pylsp_mypy-0.7.1/0000755000175100017510000000000015140145127013412 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/LICENSE0000644000175100017510000000212715140145113014414 0ustar00runnerrunnerMIT License Copyright (c) 2017 Tom van Ommeren Copyright (c) 2026 Richard Kellnberger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/MANIFEST.in0000644000175100017510000000002315140145113015136 0ustar00runnerrunnerinclude README.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1770048086.6609313 pylsp_mypy-0.7.1/PKG-INFO0000644000175100017510000002342715140145127014517 0ustar00runnerrunnerMetadata-Version: 2.4 Name: pylsp-mypy Version: 0.7.1 Summary: Mypy linter for the Python LSP Server Home-page: https://github.com/python-lsp/pylsp-mypy Author: Richard Kellnberger, Tom van Ommeren License: 'MIT' Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: python-lsp-server>=1.13.1 Requires-Dist: mypy>=0.981 Requires-Dist: tomli>=1.1.0; python_version < "3.11" Provides-Extra: test Requires-Dist: tox; extra == "test" Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: coverage; extra == "test" Dynamic: description-content-type Dynamic: license-file Mypy plugin for PYLSP ====================== .. image:: https://badge.fury.io/py/pylsp-mypy.svg :target: https://badge.fury.io/py/pylsp-mypy .. image:: https://github.com/python-lsp/pylsp-mypy/workflows/Python%20package/badge.svg?branch=master :target: https://github.com/python-lsp/pylsp-mypy/ This is a plugin for the `Python LSP Server`_. .. _`Python LSP Server`: https://github.com/python-lsp/python-lsp-server It, like `mypy`_, requires Python 3.10 or newer. .. _`mypy`: https://github.com/python/mypy Installation ------------ Install into the same virtualenv as python-lsp-server itself. ``pip install pylsp-mypy`` Configuration ------------- ``pylsp-mypy`` supports the use of ``pyproject.toml`` for configuration. It can also be configuered using configs provided to the LSP server. The configuration keys are listed in the following. .. list-table:: Configuration :header-rows: 1 * - ``pyproject.toml`` key - LSP Configuration Key - Type - Description - Default * - ``live_mode`` - ``pylsp.plugins.pylsp_mypy.live_mode`` - ``boolean`` - **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. - true * - ``dmypy`` - ``pylsp.plugins.pylsp_mypy.dmypy`` - ``boolean`` - **Executes via** ``dmypy run`` **rather than** ``mypy``. This uses the ``dmypy`` daemon and may dramatically improve the responsiveness of the ``pylsp`` server, however this currently does not work in ``live_mode``. Enabling this disables ``live_mode``, even for conflicting configs. - false * - ``strict`` - ``pylsp.plugins.pylsp_mypy.strict`` - ``boolean`` - **Refers to the** ``strict`` **option of** ``mypy``. This option often is too strict to be useful. - false * - ``overrides`` - ``pylsp.plugins.pylsp_mypy.overrides`` - ``array`` of (``string`` items or ``true``) - **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. - ``[true]`` * - ``dmypy_status_file`` - ``pylsp.plugins.pylsp_mypy.dmypy_status_file`` - ``string`` - **Specifies which status file dmypy should use**. This modifies the ``--status-file`` option passed to ``dmypy`` given ``dmypy`` is active. - ``.dmypy.json`` * - ``config_sub_paths`` - ``pylsp.plugins.pylsp_mypy.config_sub_paths`` - ``array`` of ``string`` items - **Specifies sub paths under which the mypy configuration file may be found**. For each directory searched for the mypy config file, this also searches the sub paths specified here. - ``[]`` * - ``report_progress`` - ``pylsp.plugins.pylsp_mypy.report_progress`` - ``boolean`` - **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 ``live_mode``. For large projects, enabling this can be helpful to assure yourself whether mypy is still running. - false * - ``exclude`` - ``pylsp.plugins.pylsp_mypy.exclude`` - ``array`` of ``string`` items - **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. - ``[]`` * - ``follow-imports`` - ``pylsp.plugins.pylsp_mypy.follow-imports`` - ``normal``, ``silent``, ``skip`` or ``error`` - ``mypy`` **parameter** ``follow-imports``. In ``mypy`` this is ``normal`` by default. We set it ``silent``, to sort out unwanted results. This can cause cache invalidation if you also run ``mypy`` in other ways. Setting this to ``normal`` avoids this at the cost of a small performance penalty. - ``silent`` * - ``mypy_command`` - ``pylsp.plugins.pylsp_mypy.mypy_command`` - ``array`` of ``string`` items - **The command to run mypy**. This is useful if you want to run mypy in a specific virtual environment. Requires env variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` to be set. - ``[]`` * - ``dmypy_command`` - ``pylsp.plugins.pylsp_mypy.dmypy_command`` - ``array`` of ``string`` items - **The command to run dmypy**. This is useful if you want to run dmypy in a specific virtual environment. Requires env variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` to be set. - ``[]`` Both ``mypy_command`` and ``dmypy_command`` could be used by a malicious repo to execute arbitrary code by looking at its source with this plugin active. Still users want this feature. For security reasons this is disabled by default. If you really want it and accept the risks set the environment variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` in order to activate it. Using a ``pyproject.toml`` for configuration, which is in fact the preferred way, your configuration could look like this: :: [tool.pylsp-mypy] enabled = true live_mode = true strict = true exclude = ["tests/*"] A ``pyproject.toml`` does not conflict with the legacy config file (deprecated) given that it does not contain a ``pylsp-mypy`` section. The following explanation uses the syntax of the legacy config file (deprecated). 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: :: { "enabled": True, "live_mode": True, "strict": False, "exclude": ["tests/*"] } With ``dmypy`` enabled your config should look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "strict": False } With ``overrides`` specified (for example to tell mypy to use a different python than the currently active venv), your config could look like this: :: { "enabled": True, "overrides": ["--python-executable", "/home/me/bin/python", True] } With ``dmypy_status_file`` your config could look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "strict": False, "dmypy_status_file": ".custom_dmypy_status_file.json" } With ``config_sub_paths`` your config could look like this: :: { "enabled": True, "config_sub_paths": [".config"] } With ``report_progress`` your config could look like this: :: { "enabled": True, "report_progress": True } With ``mypy_command`` your config could look like this: :: { "enabled": True, "mypy_command": ["poetry", "run", "mypy"] } With ``dmypy_command`` your config could look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "dmypy_command": ["/path/to/venv/bin/dmypy"] } Developing ------------- Install development dependencies with (you might want to create a virtualenv first): :: pip install -r requirements.txt The project is formatted with `black`_. You can either configure your IDE to automatically format code with it, run it manually (``black .``) or rely on pre-commit (see below) to format files on git commit. The project is formatted with `isort`_. You can either configure your IDE to automatically sort imports with it, run it manually (``isort .``) or rely on pre-commit (see below) to sort files on git commit. The project uses two rst tests in order to assure uploadability to pypi: `rst-linter`_ as a pre-commit hook and `rstcheck`_ in a GitHub workflow. This does not catch all errors. This project uses `pre-commit`_ to enforce code-quality. After cloning the repository install the pre-commit hooks with: :: pre-commit install After that pre-commit will run `all defined hooks`_ on every ``git commit`` and keep you from committing if there are any errors. .. _black: https://github.com/psf/black .. _isort: https://github.com/PyCQA/isort .. _rst-linter: https://github.com/Lucas-C/pre-commit-hooks-markup .. _rstcheck: https://github.com/myint/rstcheck .. _pre-commit: https://pre-commit.com/ .. _all defined hooks: .pre-commit-config.yaml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/README.rst0000644000175100017510000002132715140145113015101 0ustar00runnerrunnerMypy plugin for PYLSP ====================== .. image:: https://badge.fury.io/py/pylsp-mypy.svg :target: https://badge.fury.io/py/pylsp-mypy .. image:: https://github.com/python-lsp/pylsp-mypy/workflows/Python%20package/badge.svg?branch=master :target: https://github.com/python-lsp/pylsp-mypy/ This is a plugin for the `Python LSP Server`_. .. _`Python LSP Server`: https://github.com/python-lsp/python-lsp-server It, like `mypy`_, requires Python 3.10 or newer. .. _`mypy`: https://github.com/python/mypy Installation ------------ Install into the same virtualenv as python-lsp-server itself. ``pip install pylsp-mypy`` Configuration ------------- ``pylsp-mypy`` supports the use of ``pyproject.toml`` for configuration. It can also be configuered using configs provided to the LSP server. The configuration keys are listed in the following. .. list-table:: Configuration :header-rows: 1 * - ``pyproject.toml`` key - LSP Configuration Key - Type - Description - Default * - ``live_mode`` - ``pylsp.plugins.pylsp_mypy.live_mode`` - ``boolean`` - **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. - true * - ``dmypy`` - ``pylsp.plugins.pylsp_mypy.dmypy`` - ``boolean`` - **Executes via** ``dmypy run`` **rather than** ``mypy``. This uses the ``dmypy`` daemon and may dramatically improve the responsiveness of the ``pylsp`` server, however this currently does not work in ``live_mode``. Enabling this disables ``live_mode``, even for conflicting configs. - false * - ``strict`` - ``pylsp.plugins.pylsp_mypy.strict`` - ``boolean`` - **Refers to the** ``strict`` **option of** ``mypy``. This option often is too strict to be useful. - false * - ``overrides`` - ``pylsp.plugins.pylsp_mypy.overrides`` - ``array`` of (``string`` items or ``true``) - **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. - ``[true]`` * - ``dmypy_status_file`` - ``pylsp.plugins.pylsp_mypy.dmypy_status_file`` - ``string`` - **Specifies which status file dmypy should use**. This modifies the ``--status-file`` option passed to ``dmypy`` given ``dmypy`` is active. - ``.dmypy.json`` * - ``config_sub_paths`` - ``pylsp.plugins.pylsp_mypy.config_sub_paths`` - ``array`` of ``string`` items - **Specifies sub paths under which the mypy configuration file may be found**. For each directory searched for the mypy config file, this also searches the sub paths specified here. - ``[]`` * - ``report_progress`` - ``pylsp.plugins.pylsp_mypy.report_progress`` - ``boolean`` - **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 ``live_mode``. For large projects, enabling this can be helpful to assure yourself whether mypy is still running. - false * - ``exclude`` - ``pylsp.plugins.pylsp_mypy.exclude`` - ``array`` of ``string`` items - **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. - ``[]`` * - ``follow-imports`` - ``pylsp.plugins.pylsp_mypy.follow-imports`` - ``normal``, ``silent``, ``skip`` or ``error`` - ``mypy`` **parameter** ``follow-imports``. In ``mypy`` this is ``normal`` by default. We set it ``silent``, to sort out unwanted results. This can cause cache invalidation if you also run ``mypy`` in other ways. Setting this to ``normal`` avoids this at the cost of a small performance penalty. - ``silent`` * - ``mypy_command`` - ``pylsp.plugins.pylsp_mypy.mypy_command`` - ``array`` of ``string`` items - **The command to run mypy**. This is useful if you want to run mypy in a specific virtual environment. Requires env variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` to be set. - ``[]`` * - ``dmypy_command`` - ``pylsp.plugins.pylsp_mypy.dmypy_command`` - ``array`` of ``string`` items - **The command to run dmypy**. This is useful if you want to run dmypy in a specific virtual environment. Requires env variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` to be set. - ``[]`` Both ``mypy_command`` and ``dmypy_command`` could be used by a malicious repo to execute arbitrary code by looking at its source with this plugin active. Still users want this feature. For security reasons this is disabled by default. If you really want it and accept the risks set the environment variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` in order to activate it. Using a ``pyproject.toml`` for configuration, which is in fact the preferred way, your configuration could look like this: :: [tool.pylsp-mypy] enabled = true live_mode = true strict = true exclude = ["tests/*"] A ``pyproject.toml`` does not conflict with the legacy config file (deprecated) given that it does not contain a ``pylsp-mypy`` section. The following explanation uses the syntax of the legacy config file (deprecated). 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: :: { "enabled": True, "live_mode": True, "strict": False, "exclude": ["tests/*"] } With ``dmypy`` enabled your config should look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "strict": False } With ``overrides`` specified (for example to tell mypy to use a different python than the currently active venv), your config could look like this: :: { "enabled": True, "overrides": ["--python-executable", "/home/me/bin/python", True] } With ``dmypy_status_file`` your config could look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "strict": False, "dmypy_status_file": ".custom_dmypy_status_file.json" } With ``config_sub_paths`` your config could look like this: :: { "enabled": True, "config_sub_paths": [".config"] } With ``report_progress`` your config could look like this: :: { "enabled": True, "report_progress": True } With ``mypy_command`` your config could look like this: :: { "enabled": True, "mypy_command": ["poetry", "run", "mypy"] } With ``dmypy_command`` your config could look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "dmypy_command": ["/path/to/venv/bin/dmypy"] } Developing ------------- Install development dependencies with (you might want to create a virtualenv first): :: pip install -r requirements.txt The project is formatted with `black`_. You can either configure your IDE to automatically format code with it, run it manually (``black .``) or rely on pre-commit (see below) to format files on git commit. The project is formatted with `isort`_. You can either configure your IDE to automatically sort imports with it, run it manually (``isort .``) or rely on pre-commit (see below) to sort files on git commit. The project uses two rst tests in order to assure uploadability to pypi: `rst-linter`_ as a pre-commit hook and `rstcheck`_ in a GitHub workflow. This does not catch all errors. This project uses `pre-commit`_ to enforce code-quality. After cloning the repository install the pre-commit hooks with: :: pre-commit install After that pre-commit will run `all defined hooks`_ on every ``git commit`` and keep you from committing if there are any errors. .. _black: https://github.com/psf/black .. _isort: https://github.com/PyCQA/isort .. _rst-linter: https://github.com/Lucas-C/pre-commit-hooks-markup .. _rstcheck: https://github.com/myint/rstcheck .. _pre-commit: https://pre-commit.com/ .. _all defined hooks: .pre-commit-config.yaml ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1770048086.658931 pylsp_mypy-0.7.1/pylsp_mypy/0000755000175100017510000000000015140145127015637 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/pylsp_mypy/__init__.py0000644000175100017510000000000015140145113017731 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/pylsp_mypy/_version.py0000644000175100017510000000002615140145113020026 0ustar00runnerrunner__version__ = "0.7.1" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/pylsp_mypy/plugin.py0000644000175100017510000006044515140145113017513 0ustar00runnerrunner# -*- coding: utf-8 -*- """ File that contains the python-lsp-server plugin pylsp-mypy. Created on Fri Jul 10 09:53:57 2020 @author: Richard Kellnberger """ import ast import collections import logging import os import os.path import re import shutil import subprocess import tempfile from configparser import ConfigParser from pathlib import Path from typing import IO, Any, Optional, TypedDict try: import tomllib except ModuleNotFoundError: import tomli as tomllib from mypy import api as mypy_api from pylsp import hookimpl from pylsp.config.config import Config 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.+)\])?$" ) ) whole_line_pattern = re.compile( # certain mypy warnings do not report start-end ranges ( r"^(?P.+):(?P\d+): " r"(?P\w+): (?P.+?)(?: +\[(?P.+)\])?$" ) ) log = logging.getLogger(__name__) # A mapping from workspace path to config file path mypyConfigFileMap: dict[str, Optional[str]] = {} settingsCache: dict[str, dict[str, Any]] = {} tmpFile: Optional[IO[bytes]] = None # In non-live-mode the file contents aren't updated. # Returning an empty diagnostic clears the diagnostic result, # 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) # 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, 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]]: """ Return a language-server diagnostic from a line of the Mypy error report. optionally, use the whole document to provide more context on it. Parameters ---------- line : str Line of mypy output to be analysed. document : Optional[Document], optional Document in wich the line is found. The default is None. Returns ------- Optional[Dict[str, Any]] The dict with the lint data. """ result = line_pattern.match(line) or whole_line_pattern.match(line) if not result: return None file_path = result["file"] if file_path != "": # live mode # results from other files can be included, but we cannot return # them. if document and document.path and not document.path.endswith(file_path): log.warning("discarding result for %s against %s", file_path, document.path) 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 severity = result["severity"] if severity not in ("error", "note"): log.warning(f"invalid error severity '{severity}'") errno = 1 if severity == "error" else 3 diag = { "source": "mypy", "range": { "start": {"line": lineno, "character": offset}, "end": {"line": end_lineno, "character": end_offset}, }, "message": result["message"], "severity": errno, } if result["code"]: diag["code"] = result["code"] return diag 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", []) if settingsCache[workspace].get("config_sub_paths", []) != configSubPaths: mypyConfigFile = findConfigFile( workspace, configSubPaths, ["mypy.ini", ".mypy.ini", "pyproject.toml", "setup.cfg"], True, ) mypyConfigFileMap[workspace] = mypyConfigFile settingsCache[workspace] = settings.copy() 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, "/") 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 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, []) if not (command and os.getenv("PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION")): # env var is required to allow command from settings if shutil.which(cmd): # Fallback to PATH log.debug( f"'{command_key}' not found in settings or not allowed, using '{cmd}' from PATH" ) command = [cmd] else: # Fallback to API command = [] log.debug(f"Using {cmd} command: {command}") return command @hookimpl def pylsp_lint( config: Config, workspace: Workspace, document: Document, is_saved: bool ) -> list[dict[str, Any]]: """ Call the linter. Parameters ---------- config : Config The pylsp config. workspace : Workspace The pylsp workspace. document : Document The document to be linted. is_saved : bool Weather the document is saved. Returns ------- List[Dict[str, Any]] List of the linting data. """ settings = config.plugin_settings("pylsp_mypy") oldSettings1 = config.plugin_settings("mypy-ls") oldSettings2 = config.plugin_settings("mypy_ls") if oldSettings1 != {} or oldSettings2 != {}: raise NameError( "Your configuration uses an old namespace (mypy-ls or mypy_ls)." + "This should be changed to pylsp_mypy" ) if settings == {}: settings = oldSettings1 if settings == {}: settings = oldSettings2 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) else: return get_diagnostics(workspace, document, settings, is_saved) def get_diagnostics( workspace: Workspace, document: Document, settings: dict[str, Any], is_saved: bool, ) -> list[dict[str, Any]]: """ Lints. Parameters ---------- workspace : Workspace The pylsp workspace. document : Document The document to be linted. is_saved : bool Weather the document is saved. Returns ------- List[Dict[str, Any]] List of the linting data. """ log.info( "lint settings = %s document.path = %s is_saved = %s", settings, document.path, is_saved, ) live_mode = settings.get("live_mode", True) dmypy = settings.get("dmypy", False) if dmypy and live_mode: # dmypy can only be efficiently run on files that have been saved, see: # https://github.com/python/mypy/issues/9309 log.warning("live_mode is not supported with dmypy, disabling") live_mode = False if dmypy: dmypy_status_file = settings.get("dmypy_status_file", ".dmypy.json") args = ["--show-error-end", "--no-error-summary", "--no-pretty"] global tmpFile if live_mode and not is_saved: if tmpFile: tmpFile = open(tmpFile.name, "wb") else: tmpFile = tempfile.NamedTemporaryFile("wb", delete=False) log.info("live_mode tmpFile = %s", tmpFile.name) tmpFile.write(bytes(document.source, "utf-8")) tmpFile.close() args.extend(["--shadow-file", document.path, tmpFile.name]) elif not is_saved and document.path in last_diagnostics: # On-launch the document isn't marked as saved, so fall through and run # the diagnostics anyway even if the file contents may be out of date. log.info( "non-live, returning cached diagnostics len(cached) = %s", last_diagnostics[document.path], ) return last_diagnostics[document.path] mypyConfigFile = mypyConfigFileMap.get(workspace.root_path) if mypyConfigFile: args.append("--config-file") args.append(mypyConfigFile) args.append(document.path) if settings.get("strict", False): args.append("--strict") overrides = settings.get("overrides", [True]) exit_status = 0 if not dmypy: args.extend(["--incremental", "--follow-imports", settings.get("follow-imports", "silent")]) args = apply_overrides(args, overrides) mypy_command: list[str] = get_cmd(settings, "mypy") if mypy_command: # mypy exists on PATH or was provided by settings # -> use this mypy log.info("executing mypy args = %s on path", args) completed_process = subprocess.run( [*mypy_command, *args], capture_output=True, **windows_flag, encoding="utf-8" ) report = completed_process.stdout errors = completed_process.stderr exit_status = completed_process.returncode else: # mypy does not exist on PATH and was not provided by settings, # but must exist in the env pylsp-mypy is installed in # -> use mypy via api log.info("executing mypy args = %s via api", args) report, errors, exit_status = mypy_api.run(args) else: # If dmypy daemon is non-responsive calls to run will block. # Check daemon status, if non-zero daemon is dead or hung. # If daemon is hung, kill will reset # If daemon is dead/absent, kill will no-op. # In either case, reset to fresh state dmypy_command: list[str] = get_cmd(settings, "dmypy") if dmypy_command: # dmypy exists on PATH or was provided by settings # -> use this dmypy completed_process = subprocess.run( [*dmypy_command, "--status-file", dmypy_status_file, "status"], capture_output=True, **windows_flag, encoding="utf-8", ) errors = completed_process.stderr exit_status = completed_process.returncode if exit_status != 0: log.info( "restarting dmypy from status: %s message: %s via path", exit_status, errors.strip(), ) subprocess.run( ["dmypy", "--status-file", dmypy_status_file, "restart"], capture_output=True, **windows_flag, encoding="utf-8", ) else: # dmypy does not exist on PATH and was not provided by settings, # 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"] ) 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"]) # run to use existing daemon or restart if required args = ["--status-file", dmypy_status_file, "run", "--"] + apply_overrides(args, overrides) if dmypy_command: # dmypy exists on PATH or was provided by settings # -> use this dmypy log.info("dmypy run args = %s via path", args) completed_process = subprocess.run( [*dmypy_command, *args], capture_output=True, **windows_flag, encoding="utf-8" ) report = completed_process.stdout errors = completed_process.stderr exit_status = completed_process.returncode else: # dmypy does not exist on PATH and was not provided by settings, # but must exist in the env pylsp-mypy is installed in # -> use dmypy via api log.info("dmypy run args = %s via api", args) report, errors, exit_status = mypy_api.run_dmypy(args) log.debug("report:\n%s", report) log.debug("errors:\n%s", errors) diagnostics = [] # Expose generic mypy error on the first line. if errors: diagnostics.append( { "source": "mypy", "range": { "start": {"line": 0, "character": 0}, # Client is supposed to clip end column to line length. "end": {"line": 0, "character": 1000}, }, "message": errors, "severity": 1 if exit_status != 0 else 2, # Error if exited with error or warning. } ) for line in report.splitlines(): log.debug("parsing: line = %r", line) diag = parse_line(line, document) if diag: diagnostics.append(diag) log.info("pylsp-mypy len(diagnostics) = %s", len(diagnostics)) last_diagnostics[document.path] = diagnostics return diagnostics @hookimpl def pylsp_settings(config: Config) -> dict[str, dict[str, dict[str, str]]]: """ Read the settings. Parameters ---------- config : Config The pylsp config. Returns ------- Dict[str, Dict[str, Dict[str, str]]] The config dict. """ configuration = init(config._root_path) return {"plugins": {"pylsp_mypy": configuration}} def init(workspace: str) -> dict[str, str]: """ Find plugin and mypy config files and creates the temp file should it be used. Parameters ---------- workspace : str The path to the current workspace. Returns ------- Dict[str, str] The plugin config dict. """ log.info("init workspace = %s", workspace) configuration = {} path = findConfigFile( workspace, [], ["pylsp-mypy.cfg", "mypy-ls.cfg", "mypy_ls.cfg", "pyproject.toml"], False ) if path: if "pyproject.toml" in path: with open(path, "rb") as file: configuration = tomllib.load(file).get("tool").get("pylsp-mypy") else: with open(path) as file: configuration = ast.literal_eval(file.read()) configSubPaths = configuration.get("config_sub_paths", []) mypyConfigFile = findConfigFile( workspace, configSubPaths, ["mypy.ini", ".mypy.ini", "pyproject.toml", "setup.cfg"], True ) mypyConfigFileMap[workspace] = mypyConfigFile settingsCache[workspace] = configuration.copy() log.info("mypyConfigFile = %s configuration = %s", mypyConfigFile, configuration) return configuration def findConfigFile( path: str, configSubPaths: list[str], names: list[str], mypy: bool ) -> Optional[str]: """ Search for a config file. Search for a file of a given name from the directory specifyed by path through all parent directories. The first file found is selected. Parameters ---------- path : str The path where the search starts. configSubPaths : List[str] Additional sub search paths in which mypy configs might be located names : List[str] The file to be found (or alternative names). mypy : bool whether the config file searched is for mypy (plugin otherwise) Returns ------- Optional[str] The path where the file has been found or None if no matching file has been found. """ start = Path(path).joinpath(names[0]) # the join causes the parents to include path for parent in start.parents: for name in names: for subPath in [""] + configSubPaths: file = parent.joinpath(subPath).joinpath(name) if file.is_file(): if file.name in ["mypy-ls.cfg", "mypy_ls.cfg"]: raise NameError( f"{str(file)}: {file.name} is no longer supported, you should rename " "your config file to pylsp-mypy.cfg or preferably use a pyproject.toml " "instead." ) if file.name == "pyproject.toml": with open(file, "rb") as fileO: configPresent = ( tomllib.load(fileO) .get("tool", {}) .get("mypy" if mypy else "pylsp-mypy") is not None ) if not configPresent: continue if file.name == "setup.cfg": config = ConfigParser() config.read(str(file)) if "mypy" not in config: continue return str(file) # No config file found in the whole directory tree # -> check mypy default locations for mypy config if mypy: defaultPaths = ["~/.config/mypy/config", "~/.mypy.ini"] XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") if XDG_CONFIG_HOME: defaultPaths.insert(0, f"{XDG_CONFIG_HOME}/mypy/config") for path in defaultPaths: if Path(path).expanduser().exists(): return str(Path(path).expanduser()) return None @hookimpl def pylsp_code_actions( config: Config, workspace: Workspace, document: Document, range: dict[str, Any], context: dict[str, Any], ) -> list[dict[str, Any]]: """ Provide code actions to ignore errors. Parameters ---------- config : pylsp.config.config.Config Current config. workspace : pylsp.workspace.Workspace Current workspace. document : pylsp.workspace.Document Document to apply code actions on. range : Dict Range argument given by pylsp. context : Dict CodeActionContext given as dict. Returns ------- List of dicts containing the code actions. """ actions = [] # Code actions based on diagnostics for diagnostic in context.get("diagnostics", []): if diagnostic["source"] != "mypy": continue if "code" not in diagnostic: continue code = diagnostic["code"] lineNumberEnd = diagnostic["range"]["end"]["line"] line = document.lines[lineNumberEnd] endOfLine = len(line) - 1 start = {"line": lineNumberEnd, "character": endOfLine} edit_range = {"start": start, "end": start} edit = {"range": edit_range, "newText": f" # type: ignore[{code}]"} action = { "title": f"# type: ignore[{code}]", "kind": "quickfix", "diagnostics": [diagnostic], "edit": {"changes": {document.uri: [edit]}}, } actions.append(action) if context.get("diagnostics", []) != []: return actions # Code actions based on current selected range for diagnostic in last_diagnostics[document.path]: lineNumberStart = diagnostic["range"]["start"]["line"] lineNumberEnd = diagnostic["range"]["end"]["line"] rStart = range["start"]["line"] rEnd = range["end"]["line"] if (rStart <= lineNumberStart and rEnd >= lineNumberStart) or ( rStart <= lineNumberEnd and rEnd >= lineNumberEnd ): code = diagnostic["code"] line = document.lines[lineNumberEnd] endOfLine = len(line) - 1 start = {"line": lineNumberEnd, "character": endOfLine} edit_range = {"start": start, "end": start} edit = {"range": edit_range, "newText": f" # type: ignore[{code}]"} action = { "title": f"# type: ignore[{code}]", "kind": "quickfix", "edit": {"changes": {document.uri: [edit]}}, } actions.append(action) return actions def close_tmpfile() -> None: """ Delete the tmpFile should it exist. Returns ------- None. """ if tmpFile and tmpFile.name: os.unlink(tmpFile.name) def dmypy_stop(settings: dict[str, Any]) -> None: """Possibly stop dmypy.""" dmypy = settings.get("dmypy", False) if not dmypy: return status_file = settings.get("dmypy_status_file", ".dmypy.json") if not os.path.exists(status_file): return dmypy_command: list[str] = get_cmd(settings, "dmypy") if dmypy_command: # dmypy exists on PATH or was provided by settings # -> use this dmypy completed_process = subprocess.run( [*dmypy_command, "--status-file", status_file, "stop"], capture_output=True, **windows_flag, encoding="utf-8", ) output, errors = completed_process.stdout, completed_process.stderr exit_status = completed_process.returncode if exit_status != 0: log.warning( "failed to stop dmypy via path; exit code: %d, message: %s", exit_status, errors.strip(), ) log.warning("killing dmypy via path") subprocess.run( [*dmypy_command, "--status-file", status_file, "kill"], capture_output=True, **windows_flag, encoding="utf-8", check=True, ) else: log.info("dmypy stopped via path: %s", output.strip()) else: # dmypy does not exist on PATH and was not provided by settings, # but must exist in the env pylsp-mypy is installed in # -> use dmypy via api output, errors, exit_status = mypy_api.run_dmypy(["--status-file", status_file, "stop"]) if exit_status != 0: log.warning( "failed to stop dmypy; exit code: %d, message: %s", exit_status, errors.strip(), ) log.warning("killing dmypy") mypy_api.run_dmypy(["--status-file", status_file, "kill"]) else: log.info("dmypy stopped: %s", output.strip()) @hookimpl def pylsp_shutdown(config: Config, workspace: Workspace) -> None: log.info("shutdown requested") close_tmpfile() dmypy_stop(config.plugin_settings("pylsp_mypy")) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1770048086.6609313 pylsp_mypy-0.7.1/pylsp_mypy.egg-info/0000755000175100017510000000000015140145127017331 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048086.0 pylsp_mypy-0.7.1/pylsp_mypy.egg-info/PKG-INFO0000644000175100017510000002342715140145126020435 0ustar00runnerrunnerMetadata-Version: 2.4 Name: pylsp-mypy Version: 0.7.1 Summary: Mypy linter for the Python LSP Server Home-page: https://github.com/python-lsp/pylsp-mypy Author: Richard Kellnberger, Tom van Ommeren License: 'MIT' Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: python-lsp-server>=1.13.1 Requires-Dist: mypy>=0.981 Requires-Dist: tomli>=1.1.0; python_version < "3.11" Provides-Extra: test Requires-Dist: tox; extra == "test" Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: coverage; extra == "test" Dynamic: description-content-type Dynamic: license-file Mypy plugin for PYLSP ====================== .. image:: https://badge.fury.io/py/pylsp-mypy.svg :target: https://badge.fury.io/py/pylsp-mypy .. image:: https://github.com/python-lsp/pylsp-mypy/workflows/Python%20package/badge.svg?branch=master :target: https://github.com/python-lsp/pylsp-mypy/ This is a plugin for the `Python LSP Server`_. .. _`Python LSP Server`: https://github.com/python-lsp/python-lsp-server It, like `mypy`_, requires Python 3.10 or newer. .. _`mypy`: https://github.com/python/mypy Installation ------------ Install into the same virtualenv as python-lsp-server itself. ``pip install pylsp-mypy`` Configuration ------------- ``pylsp-mypy`` supports the use of ``pyproject.toml`` for configuration. It can also be configuered using configs provided to the LSP server. The configuration keys are listed in the following. .. list-table:: Configuration :header-rows: 1 * - ``pyproject.toml`` key - LSP Configuration Key - Type - Description - Default * - ``live_mode`` - ``pylsp.plugins.pylsp_mypy.live_mode`` - ``boolean`` - **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. - true * - ``dmypy`` - ``pylsp.plugins.pylsp_mypy.dmypy`` - ``boolean`` - **Executes via** ``dmypy run`` **rather than** ``mypy``. This uses the ``dmypy`` daemon and may dramatically improve the responsiveness of the ``pylsp`` server, however this currently does not work in ``live_mode``. Enabling this disables ``live_mode``, even for conflicting configs. - false * - ``strict`` - ``pylsp.plugins.pylsp_mypy.strict`` - ``boolean`` - **Refers to the** ``strict`` **option of** ``mypy``. This option often is too strict to be useful. - false * - ``overrides`` - ``pylsp.plugins.pylsp_mypy.overrides`` - ``array`` of (``string`` items or ``true``) - **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. - ``[true]`` * - ``dmypy_status_file`` - ``pylsp.plugins.pylsp_mypy.dmypy_status_file`` - ``string`` - **Specifies which status file dmypy should use**. This modifies the ``--status-file`` option passed to ``dmypy`` given ``dmypy`` is active. - ``.dmypy.json`` * - ``config_sub_paths`` - ``pylsp.plugins.pylsp_mypy.config_sub_paths`` - ``array`` of ``string`` items - **Specifies sub paths under which the mypy configuration file may be found**. For each directory searched for the mypy config file, this also searches the sub paths specified here. - ``[]`` * - ``report_progress`` - ``pylsp.plugins.pylsp_mypy.report_progress`` - ``boolean`` - **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 ``live_mode``. For large projects, enabling this can be helpful to assure yourself whether mypy is still running. - false * - ``exclude`` - ``pylsp.plugins.pylsp_mypy.exclude`` - ``array`` of ``string`` items - **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. - ``[]`` * - ``follow-imports`` - ``pylsp.plugins.pylsp_mypy.follow-imports`` - ``normal``, ``silent``, ``skip`` or ``error`` - ``mypy`` **parameter** ``follow-imports``. In ``mypy`` this is ``normal`` by default. We set it ``silent``, to sort out unwanted results. This can cause cache invalidation if you also run ``mypy`` in other ways. Setting this to ``normal`` avoids this at the cost of a small performance penalty. - ``silent`` * - ``mypy_command`` - ``pylsp.plugins.pylsp_mypy.mypy_command`` - ``array`` of ``string`` items - **The command to run mypy**. This is useful if you want to run mypy in a specific virtual environment. Requires env variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` to be set. - ``[]`` * - ``dmypy_command`` - ``pylsp.plugins.pylsp_mypy.dmypy_command`` - ``array`` of ``string`` items - **The command to run dmypy**. This is useful if you want to run dmypy in a specific virtual environment. Requires env variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` to be set. - ``[]`` Both ``mypy_command`` and ``dmypy_command`` could be used by a malicious repo to execute arbitrary code by looking at its source with this plugin active. Still users want this feature. For security reasons this is disabled by default. If you really want it and accept the risks set the environment variable ``PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION`` in order to activate it. Using a ``pyproject.toml`` for configuration, which is in fact the preferred way, your configuration could look like this: :: [tool.pylsp-mypy] enabled = true live_mode = true strict = true exclude = ["tests/*"] A ``pyproject.toml`` does not conflict with the legacy config file (deprecated) given that it does not contain a ``pylsp-mypy`` section. The following explanation uses the syntax of the legacy config file (deprecated). 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: :: { "enabled": True, "live_mode": True, "strict": False, "exclude": ["tests/*"] } With ``dmypy`` enabled your config should look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "strict": False } With ``overrides`` specified (for example to tell mypy to use a different python than the currently active venv), your config could look like this: :: { "enabled": True, "overrides": ["--python-executable", "/home/me/bin/python", True] } With ``dmypy_status_file`` your config could look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "strict": False, "dmypy_status_file": ".custom_dmypy_status_file.json" } With ``config_sub_paths`` your config could look like this: :: { "enabled": True, "config_sub_paths": [".config"] } With ``report_progress`` your config could look like this: :: { "enabled": True, "report_progress": True } With ``mypy_command`` your config could look like this: :: { "enabled": True, "mypy_command": ["poetry", "run", "mypy"] } With ``dmypy_command`` your config could look like this: :: { "enabled": True, "live_mode": False, "dmypy": True, "dmypy_command": ["/path/to/venv/bin/dmypy"] } Developing ------------- Install development dependencies with (you might want to create a virtualenv first): :: pip install -r requirements.txt The project is formatted with `black`_. You can either configure your IDE to automatically format code with it, run it manually (``black .``) or rely on pre-commit (see below) to format files on git commit. The project is formatted with `isort`_. You can either configure your IDE to automatically sort imports with it, run it manually (``isort .``) or rely on pre-commit (see below) to sort files on git commit. The project uses two rst tests in order to assure uploadability to pypi: `rst-linter`_ as a pre-commit hook and `rstcheck`_ in a GitHub workflow. This does not catch all errors. This project uses `pre-commit`_ to enforce code-quality. After cloning the repository install the pre-commit hooks with: :: pre-commit install After that pre-commit will run `all defined hooks`_ on every ``git commit`` and keep you from committing if there are any errors. .. _black: https://github.com/psf/black .. _isort: https://github.com/PyCQA/isort .. _rst-linter: https://github.com/Lucas-C/pre-commit-hooks-markup .. _rstcheck: https://github.com/myint/rstcheck .. _pre-commit: https://pre-commit.com/ .. _all defined hooks: .pre-commit-config.yaml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048086.0 pylsp_mypy-0.7.1/pylsp_mypy.egg-info/SOURCES.txt0000644000175100017510000000054515140145126021220 0ustar00runnerrunnerLICENSE MANIFEST.in README.rst pyproject.toml setup.cfg setup.py pylsp_mypy/__init__.py pylsp_mypy/_version.py pylsp_mypy/plugin.py pylsp_mypy.egg-info/PKG-INFO pylsp_mypy.egg-info/SOURCES.txt pylsp_mypy.egg-info/dependency_links.txt pylsp_mypy.egg-info/entry_points.txt pylsp_mypy.egg-info/requires.txt pylsp_mypy.egg-info/top_level.txt test/test_plugin.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048086.0 pylsp_mypy-0.7.1/pylsp_mypy.egg-info/dependency_links.txt0000644000175100017510000000000115140145126023376 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048086.0 pylsp_mypy-0.7.1/pylsp_mypy.egg-info/entry_points.txt0000644000175100017510000000004715140145126022627 0ustar00runnerrunner[pylsp] pylsp_mypy = pylsp_mypy.plugin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048086.0 pylsp_mypy-0.7.1/pylsp_mypy.egg-info/requires.txt0000644000175100017510000000016615140145126021733 0ustar00runnerrunnerpython-lsp-server>=1.13.1 mypy>=0.981 [:python_version < "3.11"] tomli>=1.1.0 [test] tox pytest pytest-cov coverage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048086.0 pylsp_mypy-0.7.1/pylsp_mypy.egg-info/top_level.txt0000644000175100017510000000001315140145126022054 0ustar00runnerrunnerpylsp_mypy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/pyproject.toml0000644000175100017510000000072515140145113016325 0ustar00runnerrunner[tool.black] line-length = 100 include = '\.pyi?$' exclude = ''' /( \.git | \.mypy_cache | \.tox | \.venv | _build | build | dist )/ ''' [tool.isort] profile = "black" line_length = 100 [tool.pylsp-mypy] enabled = true live_mode = true strict = true [tool.mypy] python_version = "3.10" [[tool.mypy.overrides]] module = "pylsp.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "pylsp_mypy.plugin" disallow_untyped_decorators = false ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1770048086.6619315 pylsp_mypy-0.7.1/setup.cfg0000644000175100017510000000177015140145127015240 0ustar00runnerrunner[metadata] name = pylsp-mypy author = Richard Kellnberger, Tom van Ommeren description = Mypy linter for the Python LSP Server url = https://github.com/python-lsp/pylsp-mypy long_description = file: README.rst license = 'MIT' classifiers = Development Status :: 4 - Beta Intended Audience :: Developers Topic :: Software Development License :: OSI Approved :: MIT License Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 [options] python_requires = >= 3.10 packages = find: install_requires = python-lsp-server >= 1.13.1 mypy >= 0.981 tomli >= 1.1.0 ; python_version < "3.11" [flake8] max-complexity = 20 max-line-length = 100 [options.entry_points] pylsp = pylsp_mypy = pylsp_mypy.plugin [options.extras_require] test = tox pytest pytest-cov coverage [options.packages.find] exclude = contrib docs test [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/setup.py0000755000175100017510000000030415140145113015117 0ustar00runnerrunner#!/usr/bin/env python from setuptools import setup from pylsp_mypy import _version if __name__ == "__main__": setup(version=_version.__version__, long_description_content_type="text/x-rst") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1770048086.6599312 pylsp_mypy-0.7.1/test/0000755000175100017510000000000015140145127014371 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1770048075.0 pylsp_mypy-0.7.1/test/test_plugin.py0000644000175100017510000004101315140145113017272 0ustar00runnerrunnerimport collections import os import re import sys from pathlib import Path from unittest.mock import Mock, patch import pytest from mypy import api as mypy_api from pylsp import _utils, uris from pylsp.config.config import Config from pylsp.workspace import Document, Workspace from pylsp_mypy import plugin # TODO using these file as a document is a bad idea as tests can break by adding new tests DOC_URI = f"file:/{Path(__file__)}" DOC_TYPE_ERR = """{}.append(3) """ # 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 = ( 'test_plugin.py:124:1:129:77: note: Use "-> None" if function does not return a value' ) @pytest.fixture def last_diagnostics_monkeypatch(monkeypatch): # gets called before every test altering last_diagnostics in order to reset it monkeypatch.setattr(plugin, "last_diagnostics", collections.defaultdict(list)) return monkeypatch @pytest.fixture def workspace(tmpdir): """Return a workspace.""" ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) ws._config = Config(ws.root_uri, {}, 0, {}) return ws class FakeConfig(object): def __init__(self, path): self._root_path = path def plugin_settings(self, plugin, document_path=None): return {} def test_settings(tmpdir): config = Config(uris.from_fs_path(str(tmpdir)), {}, 0, {}) settings = plugin.pylsp_settings(config) assert settings == {"plugins": {"pylsp_mypy": {}}} def test_plugin(workspace, last_diagnostics_monkeypatch): doc = Document(DOC_URI, workspace, DOC_TYPE_ERR) plugin.pylsp_settings(workspace._config) diags = plugin.pylsp_lint(workspace._config, workspace, doc, is_saved=False) assert len(diags) == 1 diag = diags[0] 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): assert diag["range"]["end"] == {"line": 0, "character": 1} else: assert diag["range"]["end"] == {"line": 0, "character": 9} assert diag["severity"] == 1 assert diag["code"] == "attr-defined" @pytest.mark.parametrize("doc_source", ["a = 1\nb\n", "a = 1\r\nb\r\n", "a = 1\rb\r"]) def test_handling_of_line_endings(workspace, last_diagnostics_monkeypatch, doc_source): # setup doc = Document(DOC_URI, workspace, doc_source) plugin.pylsp_settings(workspace._config) # run diags = plugin.pylsp_lint(workspace._config, workspace, doc, is_saved=False) # assert undefined_name_diags = list(filter(lambda diag: diag["code"] == "name-defined", diags)) assert len(undefined_name_diags) == 1 diag = undefined_name_diags[0] assert diag["message"] == 'Name "b" is not defined' assert diag["range"]["start"] == {"line": 1, "character": 0} assert diag["range"]["end"] == {"line": 1, "character": 1} def test_parse_full_line(workspace): diag = plugin.parse_line(TEST_LINE) # TODO parse a document here assert diag["message"] == '"Request" has no attribute "id"' assert diag["range"]["start"] == {"line": 278, "character": 7} assert diag["range"]["end"] == {"line": 278, "character": 16} assert diag["severity"] == 1 assert diag["code"] == "attr-defined" def test_parse_note_line(workspace): diag = plugin.parse_line(TEST_LINE_NOTE) assert diag["message"] == 'Use "-> None" if function does not return a value' assert diag["range"]["start"] == {"line": 123, "character": 0} assert diag["range"]["end"] == {"line": 128, "character": 77} assert diag["severity"] == 3 assert "code" not in diag def test_multiple_workspaces(tmpdir, last_diagnostics_monkeypatch): DOC_SOURCE = """ def foo(): return unreachable = 1 """ DOC_ERR_MSG = "Statement is unreachable" # Initialize two workspace folders. folder1 = tmpdir.mkdir("folder1") folder2 = tmpdir.mkdir("folder2") # Create configuration file for workspace folder 1. mypy_config = folder1.join("mypy.ini") mypy_config.write("[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True") ws1 = Workspace(uris.from_fs_path(str(folder1)), Mock()) ws1._config = Config(ws1.root_uri, {}, 0, {}) ws2 = Workspace(uris.from_fs_path(str(folder2)), Mock()) ws2._config = Config(ws2.root_uri, {}, 0, {}) # Initialize settings for both folders. plugin.pylsp_settings(ws1._config) plugin.pylsp_settings(ws2._config) # Test document in workspace 1 (uses mypy.ini configuration). doc1 = Document(DOC_URI, ws1, DOC_SOURCE) diags = plugin.pylsp_lint(ws1._config, ws1, doc1, is_saved=False) assert len(diags) == 1 diag = diags[0] assert diag["message"] == DOC_ERR_MSG assert diag["code"] == "unreachable" # Test document in workspace 2 (without mypy.ini configuration) doc2 = Document(DOC_URI, ws2, DOC_SOURCE) diags = plugin.pylsp_lint(ws2._config, ws2, doc2, is_saved=False) assert len(diags) == 0 def test_apply_overrides(): assert plugin.apply_overrides(["1", "2"], []) == [] assert plugin.apply_overrides(["1", "2"], ["a"]) == ["a"] assert plugin.apply_overrides(["1", "2"], ["a", True]) == ["a", "1", "2"] assert plugin.apply_overrides(["1", "2"], [True, "a"]) == ["1", "2", "a"] assert plugin.apply_overrides(["1"], ["a", True, "b"]) == ["a", "1", "b"] @pytest.mark.skipif(os.name == "nt", reason="Not working on Windows due to test design.") def test_option_overrides(tmpdir, last_diagnostics_monkeypatch, workspace): import sys from stat import S_IRWXU from textwrap import dedent sentinel = tmpdir / "ran" source = dedent("""\ #!{} import os, sys, pathlib pathlib.Path({!r}).touch() os.execv({!r}, sys.argv) """).format(sys.executable, str(sentinel), sys.executable) wrapper = tmpdir / "bin/wrapper" wrapper.write(source, ensure=True) wrapper.chmod(S_IRWXU) overrides = ["--python-executable", wrapper.strpath, True] last_diagnostics_monkeypatch.setattr( FakeConfig, "plugin_settings", lambda _, p: {"overrides": overrides} if p == "pylsp_mypy" else {}, ) config = FakeConfig(uris.to_fs_path(workspace.root_uri)) plugin.pylsp_settings(config) assert not sentinel.exists() diags = plugin.pylsp_lint( config=config, workspace=workspace, document=Document(DOC_URI, workspace, DOC_TYPE_ERR), is_saved=False, ) assert len(diags) == 1 assert sentinel.exists() def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace): overrides = ["--python-executable", "/tmp/fake", True] last_diagnostics_monkeypatch.setattr( FakeConfig, "plugin_settings", lambda _, p: ( { "overrides": overrides, "dmypy": True, "live_mode": False, } if p == "pylsp_mypy" else {} ), ) m = Mock(wraps=lambda a, **_: Mock(returncode=0, **{"stdout": ""})) last_diagnostics_monkeypatch.setattr(plugin.subprocess, "run", m) document = Document(DOC_URI, workspace, DOC_TYPE_ERR) config = FakeConfig(uris.to_fs_path(workspace.root_uri)) plugin.pylsp_settings(config) plugin.pylsp_lint( config=config, workspace=workspace, document=document, is_saved=False, ) expected = [ "dmypy", "--status-file", ".dmypy.json", "run", "--", "--python-executable", "/tmp/fake", "--show-error-end", "--no-error-summary", "--no-pretty", document.path, ] m.assert_called_with(expected, capture_output=True, **plugin.windows_flag, encoding="utf-8") def test_dmypy_status_file(tmpdir, last_diagnostics_monkeypatch, workspace): statusFile = tmpdir / ".custom_dmypy_status_file.json" last_diagnostics_monkeypatch.setattr( FakeConfig, "plugin_settings", lambda _, p: ( { "dmypy": True, "live_mode": False, "dmypy_status_file": str(statusFile), } if p == "pylsp_mypy" else {} ), ) document = Document(DOC_URI, workspace, DOC_TYPE_ERR) config = FakeConfig(uris.to_fs_path(workspace.root_uri)) plugin.pylsp_settings(config) assert not statusFile.exists() try: plugin.pylsp_lint( config=config, workspace=workspace, document=document, is_saved=False, ) assert statusFile.exists() finally: mypy_api.run_dmypy(["--status-file", str(statusFile), "stop"]) def test_config_sub_paths(tmpdir, last_diagnostics_monkeypatch): DOC_SOURCE = """ def foo(): return unreachable = 1 """ DOC_ERR_MSG = "Statement is unreachable" config_sub_paths = [".config"] # Create configuration file for workspace. plugin_config = tmpdir.join("pyproject.toml") plugin_config.write(f"[tool.pylsp-mypy]\nenabled = true\nconfig_sub_paths = {config_sub_paths}") config_dir = tmpdir.mkdir(".config") mypy_config = config_dir.join("mypy.ini") mypy_config.write("[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True") # Initialize workspace. ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) ws._config = Config(ws.root_uri, {}, 0, {}) # Update settings for workspace. settings = plugin.pylsp_settings(ws._config) ws._config._plugin_settings = _utils.merge_dicts(ws._config._plugin_settings, settings) # Test document to make sure it uses .config/mypy.ini configuration. doc = Document(DOC_URI, ws, DOC_SOURCE) diags = plugin.pylsp_lint(ws._config, ws, doc, is_saved=False) assert len(diags) == 1 diag = diags[0] assert diag["message"] == DOC_ERR_MSG assert diag["code"] == "unreachable" def test_config_sub_paths_config_changed(tmpdir, last_diagnostics_monkeypatch): DOC_SOURCE = """ def foo(): return unreachable = 1 """ DOC_ERR_MSG = "Statement is unreachable" # Create configuration file for workspace. config_dir = tmpdir.mkdir(".config") mypy_config = config_dir.join("mypy.ini") mypy_config.write("[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True") config_sub_paths = [".config"] # Initialize workspace. ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) ws._config = Config(ws.root_uri, {}, 0, {}) # Update settings for workspace. plugin.pylsp_settings(ws._config) ws.update_config({"pylsp": {"plugins": {"pylsp_mypy": {"config_sub_paths": config_sub_paths}}}}) # Test document to make sure it uses .config/mypy.ini configuration. doc = Document(DOC_URI, ws, DOC_SOURCE) diags = plugin.pylsp_lint(ws._config, ws, doc, is_saved=False) assert len(diags) == 1 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 == [] @pytest.mark.parametrize( ("command", "settings", "cmd_on_path", "environmentVariableSet", "expected"), [ ("mypy", {}, ["/bin/mypy"], True, ["mypy"]), ("mypy", {}, None, True, []), ("mypy", {"mypy_command": ["/path/to/mypy"]}, "/bin/mypy", True, ["/path/to/mypy"]), ("mypy", {"mypy_command": ["/path/to/mypy"]}, None, True, ["/path/to/mypy"]), ("dmypy", {}, "/bin/dmypy", True, ["dmypy"]), ("dmypy", {}, None, True, []), ("dmypy", {"dmypy_command": ["/path/to/dmypy"]}, "/bin/dmypy", True, ["/path/to/dmypy"]), ("dmypy", {"dmypy_command": ["/path/to/dmypy"]}, None, True, ["/path/to/dmypy"]), ("mypy", {}, ["/bin/mypy"], False, ["mypy"]), ("mypy", {}, None, False, []), ("mypy", {"mypy_command": ["/path/to/mypy"]}, "/bin/mypy", False, ["mypy"]), ("mypy", {"mypy_command": ["/path/to/mypy"]}, None, False, []), ("dmypy", {}, "/bin/dmypy", False, ["dmypy"]), ("dmypy", {}, None, False, []), ("dmypy", {"dmypy_command": ["/path/to/dmypy"]}, "/bin/dmypy", False, ["dmypy"]), ("dmypy", {"dmypy_command": ["/path/to/dmypy"]}, None, False, []), ], ) def test_get_cmd(command, settings, cmd_on_path, environmentVariableSet: bool, expected): with patch("shutil.which", return_value=cmd_on_path): if environmentVariableSet: os.environ["PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION"] = "Does not matter at all" else: os.environ.pop("PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION", None) assert plugin.get_cmd(settings, command) == expected def test_config_overrides_mypy_command(last_diagnostics_monkeypatch, workspace): last_diagnostics_monkeypatch.setattr( FakeConfig, "plugin_settings", lambda _, p: ( { "mypy_command": ["/path/to/mypy"], } if p == "pylsp_mypy" else {} ), ) m = Mock(wraps=lambda a, **_: Mock(returncode=0, **{"stdout": ""})) last_diagnostics_monkeypatch.setattr(plugin.subprocess, "run", m) document = Document(DOC_URI, workspace, DOC_TYPE_ERR) config = FakeConfig(uris.to_fs_path(workspace.root_uri)) os.environ["PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION"] = "Does not matter at all" plugin.pylsp_settings(config) plugin.pylsp_lint( config=config, workspace=workspace, document=document, is_saved=False, ) called_argv = m.call_args.args[0] called_cmd = called_argv[0] assert called_cmd == "/path/to/mypy" def test_config_overrides_dmypy_command(last_diagnostics_monkeypatch, workspace): last_diagnostics_monkeypatch.setattr( FakeConfig, "plugin_settings", lambda _, p: ( { "dmypy": True, "live_mode": False, "dmypy_command": ["poetry", "run", "dmypy"], } if p == "pylsp_mypy" else {} ), ) m = Mock(wraps=lambda a, **_: Mock(returncode=0, **{"stdout": ""})) last_diagnostics_monkeypatch.setattr(plugin.subprocess, "run", m) document = Document(DOC_URI, workspace, DOC_TYPE_ERR) config = FakeConfig(uris.to_fs_path(workspace.root_uri)) os.environ["PYLSP_MYPY_ALLOW_DANGEROUS_CODE_EXECUTION"] = "Does not matter at all" plugin.pylsp_settings(config) plugin.pylsp_lint( config=config, workspace=workspace, document=document, is_saved=False, ) called_argv = m.call_args.args[0] called_cmd = called_argv[:3] assert called_cmd == ["poetry", "run", "dmypy"]