Skip to content

Depend on python-lsp-server #2

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 18, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ Mypy plugin for PYLS
.. image:: https://github.com/Richardk2n/pyls-mypy/workflows/Python%20package/badge.svg?branch=master
:target: https://github.com/Richardk2n/pyls-mypy/

This is a plugin for the Palantir's Python Language Server (https://github.com/palantir/python-language-server)
This is a plugin for the [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 pyls itself.
Install into the same virtualenv as python-lsp-server itself.

``pip install mypy-ls``

Expand All @@ -31,7 +31,7 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg
::

{
"enabled": True,
"live_mode": True,
"strict": False
"enabled": True,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my editor replacing the tabs with spaces, sorry about that.

"live_mode": True,
"strict": False
}
77 changes: 40 additions & 37 deletions mypy_ls/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
File that contains the pyls plugin mypy-ls.
File that contains the pylsp plugin mypy-ls.

Created on Fri Jul 10 09:53:57 2020

Expand All @@ -12,9 +12,9 @@
import os.path
import logging
from mypy import api as mypy_api
from pyls import hookimpl
from pyls.workspace import Document, Workspace
from pyls.config.config import Config
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

Expand All @@ -27,7 +27,9 @@
tmpFile: Optional[IO[str]] = None


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

Expand All @@ -54,51 +56,53 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
if file_path != "<string>": # 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)
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':
if severity == "error":
errno = 1
diag: Dict[str, Any] = {
'source': 'mypy',
'range': {
'start': {'line': lineno, 'character': offset},
"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}
"end": {"line": lineno, "character": offset + 1},
},
'message': msg,
'severity': errno
"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'])
word = document.word_at_position(diag["range"]["start"])
if word:
diag['range']['end']['character'] = (
diag['range']['start']['character'] + len(word))
diag["range"]["end"]["character"] = diag["range"]["start"][
"character"
] + len(word)

return diag
return None


@hookimpl
def pyls_lint(config: Config, workspace: Workspace, document: Document,
is_saved: bool) -> List[Dict[str, Any]]:
def pylsp_lint(
config: Config, workspace: Workspace, document: Document, is_saved: bool
) -> List[Dict[str, Any]]:
"""
Lints.

Parameters
----------
config : Config
The pyls config.
The pylsp config.
workspace : Workspace
The pyls workspace.
The pylsp workspace.
document : Document
The document to be linted.
is_saved : bool
Expand All @@ -110,27 +114,25 @@ def pyls_lint(config: Config, workspace: Workspace, document: Document,
List of the linting data.

"""
settings = config.plugin_settings('mypy-ls')
live_mode = settings.get('live_mode', True)
args = ['--incremental',
'--show-column-numbers',
'--follow-imports', 'silent']
settings = config.plugin_settings("mypy-ls")
live_mode = settings.get("live_mode", True)
args = ["--incremental", "--show-column-numbers", "--follow-imports", "silent"]

global tmpFile
if live_mode and not is_saved and tmpFile:
tmpFile = open(tmpFile.name, "w")
tmpFile.write(document.source)
tmpFile.close()
args.extend(['--shadow-file', document.path, tmpFile.name])
args.extend(["--shadow-file", document.path, tmpFile.name])
elif not is_saved:
return []

if mypyConfigFile:
args.append('--config-file')
args.append("--config-file")
args.append(mypyConfigFile)
args.append(document.path)
if settings.get('strict', False):
args.append('--strict')
if settings.get("strict", False):
args.append("--strict")

report, errors, _ = mypy_api.run(args)

Expand All @@ -144,14 +146,14 @@ def pyls_lint(config: Config, workspace: Workspace, document: Document,


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

Parameters
----------
config : Config
The pyls config.
The pylsp config.

Returns
-------
Expand Down Expand Up @@ -187,10 +189,11 @@ def init(workspace: str) -> Dict[str, str]:
configuration = eval(file.read())
global mypyConfigFile
mypyConfigFile = findConfigFile(workspace, "mypy.ini")
if (("enabled" not in configuration or configuration["enabled"])
and ("live_mode" not in configuration or configuration["live_mode"])):
if ("enabled" not in configuration or configuration["enabled"]) and (
"live_mode" not in configuration or configuration["live_mode"]
):
global tmpFile
tmpFile = tempfile.NamedTemporaryFile('w', delete=False)
tmpFile = tempfile.NamedTemporaryFile("w", delete=False)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize that my editor automatically blackens all Python files, I didn't mean to push these changes. I will revert them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some uniform formatting would probably be good and I did a very poor job in that regard. Therefore, this is fine with me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh well I already pushed a revert. If you are OK with formatting with black I will open an issue and do it properly, with a pre-commit hook probably.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be nice

tmpFile.close()
return configuration

Expand Down
7 changes: 5 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
python-language-server>=0.34.0
mypy
future;python_version < '3'
flake8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do the additional requirements come from?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I think they came from the old pyls-mypy branch I based my changes on (see tomv564#52). I will fix it.

configparser
python-lsp-server
mypy;python_version >= '3.2'
8 changes: 4 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[metadata]
name = mypy-ls
author = Tom van Ommeren, Richard Kellnberger
description = Mypy linter for the Python Language Server
description = Mypy linter for the Python LSP Server
url = https://github.com/Richardk2n/pyls-mypy
long_description = file: README.rst
license='MIT'
Expand All @@ -17,13 +17,13 @@ classifiers =
[options]
python_requires = >= 3.6
packages = find:
install_requires =
python-language-server>=0.34.0
install_requires =
python-lsp-server
mypy


[options.entry_points]
pyls = mypy_ls = mypy_ls.plugin
pylsp = mypy_ls = mypy_ls.plugin

[options.extras_require]
test =
Expand Down
53 changes: 25 additions & 28 deletions test/test_plugin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import pytest

from pyls.workspace import Workspace, Document
from pyls.config.config import Config
from pyls import uris
from pylsp.workspace import Workspace, Document
from pylsp.config.config import Config
from pylsp import uris
from mock import Mock
from mypy_ls import plugin

Expand All @@ -12,10 +12,8 @@
TYPE_ERR_MSG = '"Dict[<nothing>, <nothing>]" 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"')
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
Expand All @@ -27,7 +25,6 @@ def workspace(tmpdir):


class FakeConfig(object):

def __init__(self):
self._root_path = "C:"

Expand All @@ -37,53 +34,53 @@ def plugin_settings(self, plugin, document_path=None):

def test_settings():
config = FakeConfig()
settings = plugin.pyls_settings(config)
settings = plugin.pylsp_settings(config)
assert settings == {"plugins": {"mypy-ls": {}}}


def test_plugin(workspace):
config = FakeConfig()
doc = Document(DOC_URI, workspace, DOC_TYPE_ERR)
workspace = None
plugin.pyls_settings(config)
diags = plugin.pyls_lint(config, workspace, doc, is_saved=False)
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}
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}
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}
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}
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))])
@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)
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]}
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]}