pax_global_header00006660000000000000000000000064141607160510014513gustar00rootroot0000000000000052 comment=0c61489c896bf5eae708f77e7ca5b48cc6f2da44 pylsp-mypy-0.5.7/000077500000000000000000000000001416071605100136675ustar00rootroot00000000000000pylsp-mypy-0.5.7/.github/000077500000000000000000000000001416071605100152275ustar00rootroot00000000000000pylsp-mypy-0.5.7/.github/workflows/000077500000000000000000000000001416071605100172645ustar00rootroot00000000000000pylsp-mypy-0.5.7/.github/workflows/python-package.yml000066400000000000000000000037321416071605100227260ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: branches: [ master ] pull_request: branches: [ master ] jobs: testCode: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Check black formatting run: | # stop the build if black detect any changes black --check . - name: Test with pytest run: | pytest testUploadability: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Test with rstcheck run: | rstcheck README.rst - name: Build and check run: | python setup.py sdist bdist_wheel twine check dist/* pylsp-mypy-0.5.7/.github/workflows/python-publish.yml000066400000000000000000000015161416071605100227770ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* pylsp-mypy-0.5.7/.gitignore000066400000000000000000000022051416071605100156560ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ pylsp-mypy-0.5.7/.pre-commit-config.yaml000066400000000000000000000004621416071605100201520ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black rev: 21.6b0 hooks: - id: black - repo: https://github.com/Lucas-C/pre-commit-hooks-markup rev: v1.0.1 hooks: - id: rst-linterpylsp-mypy-0.5.7/LICENSE000066400000000000000000000021271416071605100146760ustar00rootroot00000000000000MIT License Copyright (c) 2017 Tom van Ommeren Copyright (c) 2020 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. pylsp-mypy-0.5.7/MANIFEST.in000066400000000000000000000000231416071605100154200ustar00rootroot00000000000000include README.rst pylsp-mypy-0.5.7/README.rst000066400000000000000000000070151416071605100153610ustar00rootroot00000000000000Mypy plugin for PYLSP ====================== .. image:: https://badge.fury.io/py/pylsp-mypy.svg :target: https://badge.fury.io/py/pylsp-mypy .. image:: https://github.com/Richardk2n/pylsp-mypy/workflows/Python%20package/badge.svg?branch=master :target: https://github.com/Richardk2n/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.6 or newer. Installation ------------ Install into the same virtualenv as python-lsp-server itself. ``pip install pylsp-mypy`` Configuration ------------- ``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. ``dmypy`` (default is False) 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. ``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``). 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 } 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] } 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 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 .. _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 pylsp-mypy-0.5.7/mypy.ini000066400000000000000000000002101416071605100153570ustar00rootroot00000000000000[mypy] python_version = 3.6 [mypy-pylsp.*] ignore_missing_imports = True [mypy-pylsp_mypy.plugin] disallow_untyped_decorators = Falsepylsp-mypy-0.5.7/pylsp_mypy/000077500000000000000000000000001416071605100161145ustar00rootroot00000000000000pylsp-mypy-0.5.7/pylsp_mypy/__init__.py000066400000000000000000000000011416071605100202140ustar00rootroot00000000000000 pylsp-mypy-0.5.7/pylsp_mypy/_version.py000066400000000000000000000000261416071605100203100ustar00rootroot00000000000000__version__ = "0.5.7" pylsp-mypy-0.5.7/pylsp_mypy/plugin.py000066400000000000000000000304151416071605100177670ustar00rootroot00000000000000# -*- 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 re import tempfile import os import os.path import subprocess from pathlib import Path import logging from mypy import api as mypy_api from pylsp import hookimpl from pylsp.workspace import Document, Workspace from pylsp.config.config import Config from typing import Optional, Dict, Any, IO, List import atexit import collections import warnings import shutil import ast line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" log = logging.getLogger(__name__) # A mapping from workspace path to config file path mypyConfigFileMap: Dict[str, Optional[str]] = {} tmpFile: Optional[IO[str]] = 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) 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 = re.match(line_pattern, line) if result: file_path, linenoStr, offsetStr, severity, msg = result.groups() 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(linenoStr or 1) - 1 # 0-based line number offset = int(offsetStr or 1) - 1 # 0-based offset errno = 2 if severity == "error": errno = 1 diag: Dict[str, Any] = { "source": "mypy", "range": { "start": {"line": lineno, "character": offset}, # There may be a better solution, but mypy does not provide end "end": {"line": lineno, "character": offset + 1}, }, "message": msg, "severity": errno, } if document: # although mypy does not provide the end of the affected range, we # can make a good guess by highlighting the word that Mypy flagged word = document.word_at_position(diag["range"]["start"]) if word: diag["range"]["end"]["character"] = diag["range"]["start"]["character"] + len(word) return diag return None 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 @hookimpl def pylsp_lint( config: Config, workspace: Workspace, document: Document, is_saved: bool ) -> List[Dict[str, Any]]: """ Lints. 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") if oldSettings1 != {}: warnings.warn( DeprecationWarning( "Your configuration uses the namespace mypy-ls, this should be changed to pylsp_mypy" ) ) oldSettings2 = config.plugin_settings("mypy_ls") if oldSettings2 != {}: warnings.warn( DeprecationWarning( "Your configuration uses the namespace mypy_ls, this should be changed to pylsp_mypy" ) ) if settings == {}: settings = oldSettings1 if settings == {}: settings = oldSettings2 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 args = ["--show-column-numbers"] global tmpFile if live_mode and not is_saved: if tmpFile: tmpFile = open(tmpFile.name, "w") else: tmpFile = tempfile.NamedTemporaryFile("w", delete=False) log.info("live_mode tmpFile = %s", tmpFile.name) tmpFile.write(document.source) 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]) if not dmypy: args.extend(["--incremental", "--follow-imports", "silent"]) args = apply_overrides(args, overrides) if shutil.which("mypy"): # mypy exists on path # -> use mypy on path log.info("executing mypy args = %s on path", args) completed_process = subprocess.run( ["mypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) report = completed_process.stdout.decode() errors = completed_process.stderr.decode() else: # mypy does not exist on path, 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, _ = 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 if shutil.which("dmypy"): # dmypy exists on path # -> use mypy on path completed_process = subprocess.run( ["dmypy", *apply_overrides(args, overrides)], stderr=subprocess.PIPE ) _err = completed_process.stderr.decode() _status = completed_process.returncode if _status != 0: log.info( "restarting dmypy from status: %s message: %s via path", _status, _err.strip() ) subprocess.run(["dmypy", "kill"]) else: # dmypy does not exist on path, but must exist in the env pylsp-mypy is installed in # -> use dmypy via api _, _err, _status = mypy_api.run_dmypy(["status"]) if _status != 0: log.info( "restarting dmypy from status: %s message: %s via api", _status, _err.strip() ) mypy_api.run_dmypy(["kill"]) # run to use existing daemon or restart if required args = ["run", "--"] + apply_overrides(args, overrides) if shutil.which("dmypy"): # dmypy exists on path # -> use mypy on path log.info("dmypy run args = %s via path", args) completed_process = subprocess.run( ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) report = completed_process.stdout.decode() errors = completed_process.stderr.decode() else: # dmypy does not exist on path, 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, _ = mypy_api.run_dmypy(args) log.debug("report:\n%s", report) log.debug("errors:\n%s", errors) diagnostics = [] 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"]) if path: with open(path) as file: configuration = ast.literal_eval(file.read()) mypyConfigFile = findConfigFile(workspace, ["mypy.ini", ".mypy.ini"]) mypyConfigFileMap[workspace] = mypyConfigFile log.info("mypyConfigFile = %s configuration = %s", mypyConfigFile, configuration) return configuration def findConfigFile(path: str, names: List[str]) -> 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. names : List[str] The file to be found (or alternative names). 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: file = parent.joinpath(name) if file.is_file(): if file.name in ["mypy-ls.cfg", "mypy_ls.cfg"]: warnings.warn( DeprecationWarning( f"{str(file)}: {file.name} is no longer supported, you should rename your " "config file to pylsp-mypy.cfg" ) ) return str(file) return None @atexit.register def close() -> None: """ Deltes the tempFile should it exist. Returns ------- None. """ if tmpFile and tmpFile.name: os.unlink(tmpFile.name) pylsp-mypy-0.5.7/pyproject.toml000066400000000000000000000002311416071605100165770ustar00rootroot00000000000000[tool.black] line-length = 100 include = '\.pyi?$' exclude = ''' /( \.git | \.mypy_cache | \.tox | \.venv | _build | build | dist )/ ''' pylsp-mypy-0.5.7/requirements.txt000066400000000000000000000000611416071605100171500ustar00rootroot00000000000000python-lsp-server mypy black pre-commit rstcheck pylsp-mypy-0.5.7/setup.cfg000066400000000000000000000016151416071605100155130ustar00rootroot00000000000000[metadata] name = pylsp-mypy author = Tom van Ommeren, Richard Kellnberger description = Mypy linter for the Python LSP Server url = https://github.com/Richardk2n/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.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] python_requires = >= 3.6 packages = find: install_requires = python-lsp-server mypy [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 pylsp-mypy-0.5.7/setup.py000077500000000000000000000003031416071605100154000ustar00rootroot00000000000000#!/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") pylsp-mypy-0.5.7/test/000077500000000000000000000000001416071605100146465ustar00rootroot00000000000000pylsp-mypy-0.5.7/test/__init__.py000066400000000000000000000000001416071605100167450ustar00rootroot00000000000000pylsp-mypy-0.5.7/test/test_plugin.py000066400000000000000000000155121416071605100175610ustar00rootroot00000000000000import pytest from pylsp.workspace import Workspace, Document from pylsp.config.config import Config from pylsp import uris from unittest.mock import Mock from pylsp_mypy import plugin import collections DOC_URI = __file__ DOC_TYPE_ERR = """{}.append(3) """ TYPE_ERR_MSG = '"Dict[, ]" has no attribute "append"' TEST_LINE = 'test_plugin.py:279:8: error: "Request" has no attribute "id"' TEST_LINE_WITHOUT_COL = "test_plugin.py:279: " 'error: "Request" has no attribute "id"' TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"' @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): self._root_path = "C:" def plugin_settings(self, plugin, document_path=None): return {} def test_settings(): config = FakeConfig() settings = plugin.pylsp_settings(config) assert settings == {"plugins": {"pylsp_mypy": {}}} def test_plugin(workspace, last_diagnostics_monkeypatch): config = FakeConfig() doc = Document(DOC_URI, workspace, DOC_TYPE_ERR) plugin.pylsp_settings(config) diags = plugin.pylsp_lint(config, workspace, doc, is_saved=False) assert len(diags) == 1 diag = diags[0] assert diag["message"] == TYPE_ERR_MSG assert diag["range"]["start"] == {"line": 0, "character": 0} assert diag["range"]["end"] == {"line": 0, "character": 1} def test_parse_full_line(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE, doc) assert diag["message"] == '"Request" has no attribute "id"' assert diag["range"]["start"] == {"line": 278, "character": 7} assert diag["range"]["end"] == {"line": 278, "character": 8} def test_parse_line_without_col(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_COL, doc) assert diag["message"] == '"Request" has no attribute "id"' assert diag["range"]["start"] == {"line": 278, "character": 0} assert diag["range"]["end"] == {"line": 278, "character": 1} def test_parse_line_without_line(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE, doc) assert diag["message"] == '"Request" has no attribute "id"' assert diag["range"]["start"] == {"line": 0, "character": 0} assert diag["range"]["end"] == {"line": 0, "character": 6} @pytest.mark.parametrize("word,bounds", [("", (7, 8)), ("my_var", (7, 13))]) def test_parse_line_with_context(monkeypatch, word, bounds, workspace): doc = Document(DOC_URI, workspace) monkeypatch.setattr(Document, "word_at_position", lambda *args: word) diag = plugin.parse_line(TEST_LINE, doc) assert diag["message"] == '"Request" has no attribute "id"' assert diag["range"]["start"] == {"line": 278, "character": bounds[0]} assert diag["range"]["end"] == {"line": 278, "character": bounds[1]} 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") ws1 = Workspace(uris.from_fs_path(str(folder1)), Mock()) ws1._config = Config(ws1.root_uri, {}, 0, {}) folder2 = tmpdir.mkdir("folder2") ws2 = Workspace(uris.from_fs_path(str(folder2)), Mock()) ws2._config = Config(ws2.root_uri, {}, 0, {}) # Create configuration file for workspace folder 1. mypy_config = folder1.join("mypy.ini") mypy_config.write("[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True") # 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 # 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"] def test_option_overrides(tmpdir, last_diagnostics_monkeypatch, workspace): import sys from textwrap import dedent from stat import S_IRWXU 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 {}, ) assert not sentinel.exists() diags = plugin.pylsp_lint( config=FakeConfig(), 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.decode": lambda: ""})) last_diagnostics_monkeypatch.setattr(plugin.subprocess, "run", m) plugin.pylsp_lint( config=FakeConfig(), workspace=workspace, document=Document(DOC_URI, workspace, DOC_TYPE_ERR), is_saved=False, ) expected = [ "dmypy", "run", "--", "--python-executable", "/tmp/fake", "--show-column-numbers", __file__, ] m.assert_called_with(expected, stderr=-1, stdout=-1)