Repository: python-lsp/python-lsp-server
Pull request #18 (2021-04-25 18:15:40)

Fix Jedi type map (use types offered by modern Jedi)
The type map used by Jedi completion is severely outdated (includes types that are never returned by Jedi, and is missing types which are). This PR updates the types list (including future-proof namespace which is going to be introduced in next release) and adds a test to verify that these types actually are returned by Jedi for objects of such types (and properly mapped to LSP types).

andfoy: Thanks @krassowski!

Patch:
diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py
index 1e3c80e..5b07f56 100644
--- a/pylsp/plugins/jedi_completion.py
+++ b/pylsp/plugins/jedi_completion.py
@@ -10,39 +10,21 @@ from pylsp import _utils, hookimpl, lsp
 
 log = logging.getLogger(__name__)
 
-# Map to the VSCode type
+# Map to the LSP type
+# > Valid values for type are ``module``, `` class ``, ``instance``, ``function``,
+# > ``param``, ``path``, ``keyword``, ``property`` and ``statement``.
+# see: https://jedi.readthedocs.io/en/latest/docs/api-classes.html#jedi.api.classes.BaseName.type
 _TYPE_MAP = {
-    'none': lsp.CompletionItemKind.Value,
-    'type': lsp.CompletionItemKind.Class,
-    'tuple': lsp.CompletionItemKind.Class,
-    'dict': lsp.CompletionItemKind.Class,
-    'dictionary': lsp.CompletionItemKind.Class,
-    'function': lsp.CompletionItemKind.Function,
-    'lambda': lsp.CompletionItemKind.Function,
-    'generator': lsp.CompletionItemKind.Function,
+    'module': lsp.CompletionItemKind.Module,
+    'namespace': lsp.CompletionItemKind.Module,    # to be added in Jedi 0.18+
     'class': lsp.CompletionItemKind.Class,
     'instance': lsp.CompletionItemKind.Reference,
-    'method': lsp.CompletionItemKind.Method,
-    'builtin': lsp.CompletionItemKind.Class,
-    'builtinfunction': lsp.CompletionItemKind.Function,
-    'module': lsp.CompletionItemKind.Module,
-    'file': lsp.CompletionItemKind.File,
-    'path': lsp.CompletionItemKind.Text,
-    'xrange': lsp.CompletionItemKind.Class,
-    'slice': lsp.CompletionItemKind.Class,
-    'traceback': lsp.CompletionItemKind.Class,
-    'frame': lsp.CompletionItemKind.Class,
-    'buffer': lsp.CompletionItemKind.Class,
-    'dictproxy': lsp.CompletionItemKind.Class,
-    'funcdef': lsp.CompletionItemKind.Function,
-    'property': lsp.CompletionItemKind.Property,
-    'import': lsp.CompletionItemKind.Module,
-    'keyword': lsp.CompletionItemKind.Keyword,
-    'constant': lsp.CompletionItemKind.Variable,
-    'variable': lsp.CompletionItemKind.Variable,
-    'value': lsp.CompletionItemKind.Value,
+    'function': lsp.CompletionItemKind.Function,
     'param': lsp.CompletionItemKind.Variable,
-    'statement': lsp.CompletionItemKind.Keyword,
+    'path': lsp.CompletionItemKind.File,
+    'keyword': lsp.CompletionItemKind.Keyword,
+    'property': lsp.CompletionItemKind.Property,    # added in Jedi 0.18
+    'statement': lsp.CompletionItemKind.Variable
 }
 
 # Types of parso nodes for which snippet is not included in the completion


Test patch:
diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py
index 258b228..df19724 100644
--- a/test/plugins/test_completion.py
+++ b/test/plugins/test_completion.py
@@ -4,12 +4,16 @@
 import os
 import sys
 
+from pathlib import Path
+from typing import NamedTuple, Dict
+
 import pytest
 
 from pylsp import uris, lsp
 from pylsp.workspace import Document
 from pylsp.plugins.jedi_completion import pylsp_completions as pylsp_jedi_completions
 from pylsp.plugins.rope_completion import pylsp_completions as pylsp_rope_completions
+from pylsp._utils import JEDI_VERSION
 
 
 PY2 = sys.version[0] == '2'
@@ -50,6 +54,77 @@ def test_rope_import_completion(config, workspace):
     assert items is None
 
 
+class TypeCase(NamedTuple):
+    document: str
+    position: dict
+    label: str
+    expected: lsp.CompletionItemKind
+
+
+TYPE_CASES: Dict[str, TypeCase] = {
+    'variable': TypeCase(
+        document='test = 1\ntes',
+        position={'line': 1, 'character': 3},
+        label='test',
+        expected=lsp.CompletionItemKind.Variable
+    ),
+    'function': TypeCase(
+        document='def test():\n    pass\ntes',
+        position={'line': 2, 'character': 3},
+        label='test()',
+        expected=lsp.CompletionItemKind.Function
+    ),
+    'keyword': TypeCase(
+        document='fro',
+        position={'line': 0, 'character': 3},
+        label='from',
+        expected=lsp.CompletionItemKind.Keyword
+    ),
+    'file': TypeCase(
+        document='"' + __file__[:-2].replace('"', '\\"') + '"',
+        position={'line': 0, 'character': len(__file__) - 2},
+        label=Path(__file__).name + '"',
+        expected=lsp.CompletionItemKind.File
+    ),
+    'module': TypeCase(
+        document='import statis',
+        position={'line': 0, 'character': 13},
+        label='statistics',
+        expected=lsp.CompletionItemKind.Module
+    ),
+    'class': TypeCase(
+        document='KeyErr',
+        position={'line': 0, 'character': 6},
+        label='KeyError',
+        expected=lsp.CompletionItemKind.Class
+    ),
+    'property': TypeCase(
+        document=(
+            'class A:\n'
+            '    @property\n'
+            '    def test(self):\n'
+            '        pass\n'
+            'A().tes'
+        ),
+        position={'line': 4, 'character': 5},
+        label='test',
+        expected=lsp.CompletionItemKind.Property
+    )
+}
+
+
+@pytest.mark.parametrize('case', list(TYPE_CASES.values()), ids=list(TYPE_CASES.keys()))
+def test_jedi_completion_type(case, config, workspace):
+    # property support was introduced in 0.18
+    if case.expected == lsp.CompletionItemKind.Property and JEDI_VERSION.startswith('0.17'):
+        return
+
+    doc = Document(DOC_URI, workspace, case.document)
+    items = pylsp_jedi_completions(config, doc, case.position)
+    items = {i['label']: i for i in items}
+    assert items[case.label]['kind'] == case.expected
+
+
 def test_jedi_completion(config, workspace):
     # Over 'i' in os.path.isabs(...)
     com_position = {'line': 1, 'character': 15}