From 5a3c3281116003ff19b6fbcbd6bc591fe1287c31 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Thu, 10 Aug 2017 10:30:57 -0400 Subject: [PATCH 1/7] Clean up some diagnostics code --- pyls/lsp.py | 7 +++++++ pyls/plugins/pycodestyle_lint.py | 24 +++++++++--------------- pyls/plugins/pyflakes_lint.py | 5 +++-- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/pyls/lsp.py b/pyls/lsp.py index fc2d39c1..728b1044 100644 --- a/pyls/lsp.py +++ b/pyls/lsp.py @@ -26,6 +26,13 @@ class CompletionItemKind(object): Reference = 18 +class DiagnosticSeverity(object): + Error = 1 + Warning = 2 + Information = 3 + Hint = 4 + + class MessageType(object): Error = 1 Warning = 2 diff --git a/pyls/plugins/pycodestyle_lint.py b/pyls/plugins/pycodestyle_lint.py index 9f9b751b..efe7d417 100644 --- a/pyls/plugins/pycodestyle_lint.py +++ b/pyls/plugins/pycodestyle_lint.py @@ -1,7 +1,7 @@ # Copyright 2017 Palantir Technologies, Inc. import logging import pycodestyle -from pyls import config as pyls_config, hookimpl +from pyls import config as pyls_config, hookimpl, lsp log = logging.getLogger(__name__) @@ -10,7 +10,7 @@ @hookimpl -def pyls_lint(config, workspace, document): +def pyls_lint(config, document): # Read config from all over the place config_files = config.find_parents(document.path, CONFIG_FILES) pycodestyle_conf = pyls_config.build_config('pycodestyle', config_files) @@ -56,24 +56,18 @@ def error(self, lineno, offset, text, check): }, } code, _message = text.split(" ", 1) - severity = self._get_severity(code) self.diagnostics.append({ 'source': 'pycodestyle', 'range': range, 'message': text, 'code': code, - 'severity': severity + 'severity': _get_severity(code) }) - def _get_severity(self, code): - """ VSCode Severity Mapping - ERROR: 1, - WARNING: 2, - INFO: 3, - HINT: 4 - """ - if code[0] == 'E': - return 1 - elif code[0] == 'W': - return 2 + +def _get_severity(code): + if code[0] == 'E': + return lsp.DiagnosticSeverity.Error + elif code[0] == 'W': + return lsp.DiagnosticSeverity.Warning diff --git a/pyls/plugins/pyflakes_lint.py b/pyls/plugins/pyflakes_lint.py index 2d16244d..d521ecb7 100644 --- a/pyls/plugins/pyflakes_lint.py +++ b/pyls/plugins/pyflakes_lint.py @@ -1,6 +1,6 @@ # Copyright 2017 Palantir Technologies, Inc. from pyflakes import api as pyflakes_api -from pyls import hookimpl +from pyls import hookimpl, lsp @hookimpl @@ -39,5 +39,6 @@ def flake(self, message): self.diagnostics.append({ 'source': 'pyflakes', 'range': range, - 'message': message.message % message.message_args + 'message': message.message % message.message_args, + 'severity': lsp.DiagnosticSeverity.Warning }) From 02587f360c843c9a59556fc6a1966af71d18f556 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Thu, 10 Aug 2017 11:34:25 -0400 Subject: [PATCH 2/7] import magic --- pyls/plugins/__init__.py | 7 ++- pyls/plugins/importmagic_lint.py | 96 ++++++++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 pyls/plugins/importmagic_lint.py diff --git a/pyls/plugins/__init__.py b/pyls/plugins/__init__.py index b30e651b..c33241db 100644 --- a/pyls/plugins/__init__.py +++ b/pyls/plugins/__init__.py @@ -1,12 +1,13 @@ # Copyright 2017 Palantir Technologies, Inc. from . import ( - completion, definition, format, - hover, pyflakes_lint, pycodestyle_lint, + completion, definition, format, hover, + importmagic_lint, pyflakes_lint, pycodestyle_lint, references, signature, symbols ) CORE_PLUGINS = [ - completion, definition, format, hover, pyflakes_lint, pycodestyle_lint, + completion, definition, format, hover, + importmagic_lint, pyflakes_lint, pycodestyle_lint, references, signature, symbols ] diff --git a/pyls/plugins/importmagic_lint.py b/pyls/plugins/importmagic_lint.py new file mode 100644 index 00000000..0583231c --- /dev/null +++ b/pyls/plugins/importmagic_lint.py @@ -0,0 +1,96 @@ +# Copyright 2017 Palantir Technologies, Inc. +import logging +import re +import importmagic +from pyls import hookimpl, lsp + +log = logging.getLogger(__name__) + +SOURCE = 'importmagic' +ADD_IMPORT_COMMAND = 'importmagic/addimport' +MAX_COMMANDS = 4 +UNRES_RE = re.compile("Unresolved import '(?P[\w.]+)'") + +_index_cache = {} + + +def _get_index(sys_path): + key = tuple(sys_path) + if key not in _index_cache: + index = importmagic.SymbolIndex() + index.build_index(paths=sys_path) + _index_cache[key] = index + return _index_cache[key] + + +@hookimpl +def pyls_commands(): + return [ADD_IMPORT_COMMAND] + + +@hookimpl +def pyls_lint(document): + scope = importmagic.Scope.from_source(document.source) + unresolved, _unreferenced = scope.find_unresolved_and_unreferenced_symbols() + + diagnostics = [] + + # Annoyingly, we only get the text of an unresolved import, so we'll look for it ourselves + for unres in unresolved: + if unres not in document.source: + continue + + for line_no, line in enumerate(document.lines): + pos = line.find(unres) + if pos < 0: + continue + + diagnostics.append({ + 'source': SOURCE, + 'range': { + 'start': {'line': line_no, 'character': pos}, + 'end': {'line': line_no, 'character': pos + len(unres)} + }, + 'message': "Unresolved import '%s'" % unres, + 'severity': lsp.DiagnosticSeverity.Error, + }) + + return diagnostics + + +@hookimpl +def pyls_code_actions(workspace, document, context): + actions = [] + diagnostics = context.get('diagnostics', []) + for diagnostic in diagnostics: + if diagnostic.get('source') != SOURCE: + continue + m = UNRES_RE.match(diagnostic['message']) + if not m: + continue + + unres = m.group('unresolved') + index = _get_index(workspace.syspath_for_path(document.path)) + + for score, module, variable in sorted(index.symbol_scores(unres)[:MAX_COMMANDS], reverse=True): + if score < 1: + # These tend to be terrible + continue + + actions.append({ + 'title': _command_title(variable, module), + 'command': ADD_IMPORT_COMMAND, + 'arguments': { + 'unresolved': unres, + 'module': module, + 'variable': variable, + 'score': score + } + }) + return actions + + +def _command_title(variable, module): + if not variable: + return 'Import %s' % module + return 'Import %s from %s' % (variable, module) diff --git a/setup.py b/setup.py index 28e2f9d3..5d432d87 100755 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ install_requires=[ 'configparser', 'future', + 'importmagic', 'jedi>=0.10', 'json-rpc', 'pycodestyle', From 529feb3f0e53d2f4432dfb4bfa56d4113dd38147 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Thu, 10 Aug 2017 11:35:41 -0400 Subject: [PATCH 3/7] Fix tests --- test/plugins/test_lint.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/plugins/test_lint.py b/test/plugins/test_lint.py index 2a5880f5..b0cb1017 100644 --- a/test/plugins/test_lint.py +++ b/test/plugins/test_lint.py @@ -20,9 +20,9 @@ def hello(): """ -def test_pycodestyle(config, workspace): +def test_pycodestyle(config): doc = Document(DOC_URI, DOC) - diags = pycodestyle_lint.pyls_lint(config, workspace, doc) + diags = pycodestyle_lint.pyls_lint(config, doc) assert all([d['source'] == 'pycodestyle' for d in diags]) @@ -58,7 +58,7 @@ def test_pycodestyle_config(): config = Config(workspace.root, {}) # Make sure we get a warning for 'indentation contains tabs' - diags = pycodestyle_lint.pyls_lint(config, workspace, doc) + diags = pycodestyle_lint.pyls_lint(config, doc) assert [d for d in diags if d['code'] == 'W191'] content = { @@ -73,7 +73,7 @@ def test_pycodestyle_config(): f.write(content) # And make sure we don't get any warnings - diags = pycodestyle_lint.pyls_lint(config, workspace, doc) + diags = pycodestyle_lint.pyls_lint(config, doc) assert len([d for d in diags if d['code'] == 'W191']) == 0 if working else 1 os.unlink(os.path.join(tmp, conf_file)) From e1b6298b341bd1d789a0aad8b42f80b99996ce22 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Thu, 10 Aug 2017 17:46:09 -0400 Subject: [PATCH 4/7] Initial cut at auto-import --- pyls/plugins/importmagic_lint.py | 48 ++++++++++++++++++++++++++------ pyls/python_ls.py | 2 +- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/pyls/plugins/importmagic_lint.py b/pyls/plugins/importmagic_lint.py index 0583231c..fc193006 100644 --- a/pyls/plugins/importmagic_lint.py +++ b/pyls/plugins/importmagic_lint.py @@ -7,9 +7,9 @@ log = logging.getLogger(__name__) SOURCE = 'importmagic' -ADD_IMPORT_COMMAND = 'importmagic/addimport' +ADD_IMPORT_COMMAND = 'importmagic.addimport' MAX_COMMANDS = 4 -UNRES_RE = re.compile("Unresolved import '(?P[\w.]+)'") +UNRES_RE = re.compile(r"Unresolved import '(?P[\w.]+)'") _index_cache = {} @@ -77,19 +77,51 @@ def pyls_code_actions(workspace, document, context): # These tend to be terrible continue + # Generate the patch we would need to apply + imports = importmagic.Imports(index, document.source) + if variable: + imports.add_import_from(module, variable) + else: + imports.add_import(module) + start_line, end_line, text = imports.get_update() + actions.append({ 'title': _command_title(variable, module), 'command': ADD_IMPORT_COMMAND, - 'arguments': { - 'unresolved': unres, - 'module': module, - 'variable': variable, - 'score': score - } + 'arguments': [{ + 'uri': document.uri, + 'version': document.version, + 'startLine': start_line, + 'endLine': end_line, + 'newText': text + }] }) return actions +@hookimpl +def pyls_execute_command(workspace, command, arguments): + if command != ADD_IMPORT_COMMAND: + return + + args = arguments[0] + + edit = {'documentChanges': [{ + 'textDocument': { + 'uri': args['uri'], + 'version': args['version'] + }, + 'edits': [{ + 'range': { + 'start': {'line': args['startLine'], 'character': 0}, + 'end': {'line': args['endLine'], 'character': 0}, + }, + 'newText': args['newText'] + }] + }]} + workspace.apply_edit(edit) + + def _command_title(variable, module): if not variable: return 'Import %s' % module diff --git a/pyls/python_ls.py b/pyls/python_ls.py index 2f27bc3d..65e55365 100644 --- a/pyls/python_ls.py +++ b/pyls/python_ls.py @@ -76,7 +76,7 @@ def document_symbols(self, doc_uri): return flatten(self._hook(self._hooks.pyls_document_symbols, doc_uri)) def execute_command(self, command, arguments): - return self._hook(self._hooks.pyls_execute_command, command=command, arguments=arguments) + return self._hook(self._hooks.pyls_execute_command, command=command, arguments=arguments) or {} def format_document(self, doc_uri): return self._hook(self._hooks.pyls_format_document, doc_uri) From e1257bfbe50eca88c5fc34578f9dee407598be85 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 3 Oct 2017 22:47:43 +0100 Subject: [PATCH 5/7] Merge develop --- pyls/python_ls.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyls/python_ls.py b/pyls/python_ls.py index 980e5e1f..3d43531d 100644 --- a/pyls/python_ls.py +++ b/pyls/python_ls.py @@ -69,11 +69,7 @@ def document_symbols(self, doc_uri): return flatten(self._hook('pyls_document_symbols', doc_uri)) def execute_command(self, command, arguments): -<<<<<<< HEAD - return self._hook(self._hooks.pyls_execute_command, command=command, arguments=arguments) or {} -======= return self._hook('pyls_execute_command', command=command, arguments=arguments) ->>>>>>> develop def format_document(self, doc_uri): return self._hook('pyls_format_document', doc_uri) From d9108c805cd3d46eda91c9196002db49da88d4b9 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 3 Oct 2017 23:06:03 +0100 Subject: [PATCH 6/7] Stuff --- pyls/plugins/importmagic_lint.py | 12 +++++++++--- setup.py | 1 + vscode-client/package.json | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pyls/plugins/importmagic_lint.py b/pyls/plugins/importmagic_lint.py index fc193006..f63dd42e 100644 --- a/pyls/plugins/importmagic_lint.py +++ b/pyls/plugins/importmagic_lint.py @@ -1,9 +1,12 @@ # Copyright 2017 Palantir Technologies, Inc. import logging import re + import importmagic + from pyls import hookimpl, lsp + log = logging.getLogger(__name__) SOURCE = 'importmagic' @@ -59,7 +62,10 @@ def pyls_lint(document): @hookimpl -def pyls_code_actions(workspace, document, context): +def pyls_code_actions(config, workspace, document, context): + # Update the style configuration + importmagic.Imports.set_style(config.plugin_settings('importmagic_lint')) + actions = [] diagnostics = context.get('diagnostics', []) for diagnostic in diagnostics: @@ -124,5 +130,5 @@ def pyls_execute_command(workspace, command, arguments): def _command_title(variable, module): if not variable: - return 'Import %s' % module - return 'Import %s from %s' % (variable, module) + return 'Import "%s"' % module + return 'Import "%s" from "%s"' % (variable, module) diff --git a/setup.py b/setup.py index 34aff2a4..715e6761 100755 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ 'pyls = pyls.__main__:main', ], 'pyls': [ + 'importmagic = pyls.plugins.importmagic_lint', 'jedi_completion = pyls.plugins.completion', 'jedi_definition = pyls.plugins.definition', 'jedi_hover = pyls.plugins.hover', diff --git a/vscode-client/package.json b/vscode-client/package.json index 5c8b2855..fdfdec65 100644 --- a/vscode-client/package.json +++ b/vscode-client/package.json @@ -20,6 +20,22 @@ "title": "Python Language Server Configuration", "type": "object", "properties": { + "pyls.plugins.importmagic_lint.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." + }, + "pyls.plugins.importmagic_lint.maxColumns": { + "type": "number", + "default": 79, + "description": "The maximum number of columns for generated import statements" + }, + "pyls.plugins.importmagic_lint.multiline": { + "type": "string", + "default": "parentheses", + "enum": ["parentheses", "backslash"], + "description": "The style to use for multiline import statements" + }, "pyls.plugins.jedi_completion.enabled": { "type": "boolean", "default": true, From e4547292bcecea1e68ef871d10ca798bc252b518 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 3 Oct 2017 23:14:04 +0100 Subject: [PATCH 7/7] Fixes --- pyls/plugins/importmagic_lint.py | 5 +++-- pyls/plugins/pycodestyle_lint.py | 5 ++++- pyls/plugins/signature.py | 2 ++ vscode-client/package.json | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pyls/plugins/importmagic_lint.py b/pyls/plugins/importmagic_lint.py index f63dd42e..d03a366f 100644 --- a/pyls/plugins/importmagic_lint.py +++ b/pyls/plugins/importmagic_lint.py @@ -4,7 +4,7 @@ import importmagic -from pyls import hookimpl, lsp +from pyls import hookimpl, lsp, _utils log = logging.getLogger(__name__) @@ -64,7 +64,8 @@ def pyls_lint(document): @hookimpl def pyls_code_actions(config, workspace, document, context): # Update the style configuration - importmagic.Imports.set_style(config.plugin_settings('importmagic_lint')) + conf = config.plugin_settings('importmagic_lint') + importmagic.Imports.set_style(**{_utils.camel_to_underscore(k): v for k, v in conf.items()}) actions = [] diagnostics = context.get('diagnostics', []) diff --git a/pyls/plugins/pycodestyle_lint.py b/pyls/plugins/pycodestyle_lint.py index 74d54a02..fe50e685 100644 --- a/pyls/plugins/pycodestyle_lint.py +++ b/pyls/plugins/pycodestyle_lint.py @@ -1,7 +1,10 @@ # Copyright 2017 Palantir Technologies, Inc. import logging + import pycodestyle -from pyls import config as pyls_config, hookimpl, lsp, _utils + +from pyls import _utils, config as pyls_config, hookimpl, lsp + log = logging.getLogger(__name__) diff --git a/pyls/plugins/signature.py b/pyls/plugins/signature.py index 92efbb3e..13250215 100644 --- a/pyls/plugins/signature.py +++ b/pyls/plugins/signature.py @@ -1,7 +1,9 @@ # Copyright 2017 Palantir Technologies, Inc. import logging + from pyls import hookimpl + log = logging.getLogger(__name__) diff --git a/vscode-client/package.json b/vscode-client/package.json index fdfdec65..cd309964 100644 --- a/vscode-client/package.json +++ b/vscode-client/package.json @@ -27,7 +27,7 @@ }, "pyls.plugins.importmagic_lint.maxColumns": { "type": "number", - "default": 79, + "default": null, "description": "The maximum number of columns for generated import statements" }, "pyls.plugins.importmagic_lint.multiline": {