Skip to content

Commit 62a1e6f

Browse files
authored
Merge branch 'python-lsp:develop' into document-flake8-plugin
2 parents 000c4bc + e3c5dfe commit 62a1e6f

File tree

4 files changed

+155
-13
lines changed

4 files changed

+155
-13
lines changed

Diff for: CONFIGURATION.md

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ This server can be configured using `workspace/didChangeConfiguration` method. E
2222
| `pylsp.plugins.jedi_completion.include_class_objects` | `boolean` | Adds class objects as a separate completion item. | `true` |
2323
| `pylsp.plugins.jedi_completion.fuzzy` | `boolean` | Enable fuzzy when requesting autocomplete. | `false` |
2424
| `pylsp.plugins.jedi_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` |
25+
| `pylsp.plugins.jedi_completion.resolve_at_most_labels` | `number` | How many labels (at most) should be resolved? | `25` |
26+
| `pylsp.plugins.jedi_completion.cache_labels_for` | `array` of `string` items | Modules for which the labels should be cached. | `["pandas", "numpy", "tensorflow", "matplotlib"]` |
2527
| `pylsp.plugins.jedi_definition.enabled` | `boolean` | Enable or disable the plugin. | `true` |
2628
| `pylsp.plugins.jedi_definition.follow_imports` | `boolean` | The goto call will follow imports. | `true` |
2729
| `pylsp.plugins.jedi_definition.follow_builtin_imports` | `boolean` | If follow_imports is True will decide if it follow builtin imports. | `true` |
@@ -54,6 +56,7 @@ This server can be configured using `workspace/didChangeConfiguration` method. E
5456
| `pylsp.plugins.pylint.args` | `array` of non-unique `string` items | Arguments to pass to pylint. | `null` |
5557
| `pylsp.plugins.pylint.executable` | `string` | Executable to run pylint with. Enabling this will run pylint on unsaved files via stdin. Can slow down workflow. Only works with python3. | `null` |
5658
| `pylsp.plugins.rope_completion.enabled` | `boolean` | Enable or disable the plugin. | `true` |
59+
| `pylsp.plugins.rope_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` |
5760
| `pylsp.plugins.yapf.enabled` | `boolean` | Enable or disable the plugin. | `true` |
5861
| `pylsp.rope.extensionModules` | `string` | Builtin and c-extension modules that are allowed to be imported and inspected by rope. | `null` |
5962
| `pylsp.rope.ropeFolder` | `array` of unique `string` items | The name of the folder in which rope stores project configurations and data. Pass `null` for not using such a folder at all. | `null` |

Diff for: pylsp/config/schema.json

+18
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@
104104
"default": false,
105105
"description": "Resolve documentation and detail eagerly."
106106
},
107+
"pylsp.plugins.jedi_completion.resolve_at_most_labels": {
108+
"type": "number",
109+
"default": 25,
110+
"description": "How many labels (at most) should be resolved?"
111+
},
112+
"pylsp.plugins.jedi_completion.cache_labels_for": {
113+
"type": "array",
114+
"items": {
115+
"type": "string"
116+
},
117+
"default": ["pandas", "numpy", "tensorflow", "matplotlib"],
118+
"description": "Modules for which the labels should be cached."
119+
},
107120
"pylsp.plugins.jedi_definition.enabled": {
108121
"type": "boolean",
109122
"default": true,
@@ -308,6 +321,11 @@
308321
"default": true,
309322
"description": "Enable or disable the plugin."
310323
},
324+
"pylsp.plugins.rope_completion.eager": {
325+
"type": "boolean",
326+
"default": false,
327+
"description": "Resolve documentation and detail eagerly."
328+
},
311329
"pylsp.plugins.yapf.enabled": {
312330
"type": "boolean",
313331
"default": true,

Diff for: pylsp/plugins/jedi_completion.py

+113-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
import logging
55
import os.path as osp
6+
from collections import defaultdict
7+
from time import time
68

9+
from jedi.api.classes import Completion
710
import parso
811

912
from pylsp import _utils, hookimpl, lsp
@@ -38,7 +41,6 @@
3841
def pylsp_completions(config, document, position):
3942
"""Get formatted completions for current code position"""
4043
# pylint: disable=too-many-locals
41-
4244
settings = config.plugin_settings('jedi_completion', document_path=document.path)
4345
resolve_eagerly = settings.get('eager', False)
4446
code_position = _utils.position_to_jedi_linecolumn(document, position)
@@ -55,19 +57,34 @@ def pylsp_completions(config, document, position):
5557
should_include_params = settings.get('include_params')
5658
should_include_class_objects = settings.get('include_class_objects', True)
5759

60+
max_labels_resolve = settings.get('resolve_at_most_labels', 25)
61+
modules_to_cache_labels_for = settings.get('cache_labels_for', None)
62+
if modules_to_cache_labels_for is not None:
63+
LABEL_RESOLVER.cached_modules = modules_to_cache_labels_for
64+
5865
include_params = snippet_support and should_include_params and use_snippets(document, position)
5966
include_class_objects = snippet_support and should_include_class_objects and use_snippets(document, position)
6067

6168
ready_completions = [
62-
_format_completion(c, include_params)
63-
for c in completions
69+
_format_completion(
70+
c,
71+
include_params,
72+
resolve=resolve_eagerly,
73+
resolve_label=(i < max_labels_resolve)
74+
)
75+
for i, c in enumerate(completions)
6476
]
6577

6678
# TODO split up once other improvements are merged
6779
if include_class_objects:
68-
for c in completions:
80+
for i, c in enumerate(completions):
6981
if c.type == 'class':
70-
completion_dict = _format_completion(c, False, resolve=resolve_eagerly)
82+
completion_dict = _format_completion(
83+
c,
84+
False,
85+
resolve=resolve_eagerly,
86+
resolve_label=(i < max_labels_resolve)
87+
)
7188
completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter
7289
completion_dict['label'] += ' object'
7390
ready_completions.append(completion_dict)
@@ -150,9 +167,9 @@ def _resolve_completion(completion, d):
150167
return completion
151168

152169

153-
def _format_completion(d, include_params=True, resolve=False):
170+
def _format_completion(d, include_params=True, resolve=False, resolve_label=False):
154171
completion = {
155-
'label': _label(d),
172+
'label': _label(d, resolve_label),
156173
'kind': _TYPE_MAP.get(d.type),
157174
'sortText': _sort_text(d),
158175
'insertText': d.name
@@ -192,12 +209,12 @@ def _format_completion(d, include_params=True, resolve=False):
192209
return completion
193210

194211

195-
def _label(definition):
196-
sig = definition.get_signatures()
197-
if definition.type in ('function', 'method') and sig:
198-
params = ', '.join(param.name for param in sig[0].params)
199-
return '{}({})'.format(definition.name, params)
200-
212+
def _label(definition, resolve=False):
213+
if not resolve:
214+
return definition.name
215+
sig = LABEL_RESOLVER.get_or_create(definition)
216+
if sig:
217+
return sig
201218
return definition.name
202219

203220

@@ -216,3 +233,86 @@ def _sort_text(definition):
216233
# If its 'hidden', put it next last
217234
prefix = 'z{}' if definition.name.startswith('_') else 'a{}'
218235
return prefix.format(definition.name)
236+
237+
238+
class LabelResolver:
239+
240+
def __init__(self, format_label_callback, time_to_live=60 * 30):
241+
self.format_label = format_label_callback
242+
self._cache = {}
243+
self._time_to_live = time_to_live
244+
self._cache_ttl = defaultdict(set)
245+
self._clear_every = 2
246+
# see https://github.com/davidhalter/jedi/blob/master/jedi/inference/helpers.py#L194-L202
247+
self._cached_modules = {'pandas', 'numpy', 'tensorflow', 'matplotlib'}
248+
249+
@property
250+
def cached_modules(self):
251+
return self._cached_modules
252+
253+
@cached_modules.setter
254+
def cached_modules(self, new_value):
255+
self._cached_modules = set(new_value)
256+
257+
def clear_outdated(self):
258+
now = self.time_key()
259+
to_clear = [
260+
timestamp
261+
for timestamp in self._cache_ttl
262+
if timestamp < now
263+
]
264+
for time_key in to_clear:
265+
for key in self._cache_ttl[time_key]:
266+
del self._cache[key]
267+
del self._cache_ttl[time_key]
268+
269+
def time_key(self):
270+
return int(time() / self._time_to_live)
271+
272+
def get_or_create(self, completion: Completion):
273+
if not completion.full_name:
274+
use_cache = False
275+
else:
276+
module_parts = completion.full_name.split('.')
277+
use_cache = module_parts and module_parts[0] in self._cached_modules
278+
279+
if use_cache:
280+
key = self._create_completion_id(completion)
281+
if key not in self._cache:
282+
if self.time_key() % self._clear_every == 0:
283+
self.clear_outdated()
284+
285+
self._cache[key] = self.resolve_label(completion)
286+
self._cache_ttl[self.time_key()].add(key)
287+
return self._cache[key]
288+
289+
return self.resolve_label(completion)
290+
291+
def _create_completion_id(self, completion: Completion):
292+
return (
293+
completion.full_name, completion.module_path,
294+
completion.line, completion.column,
295+
self.time_key()
296+
)
297+
298+
def resolve_label(self, completion):
299+
try:
300+
sig = completion.get_signatures()
301+
return self.format_label(completion, sig)
302+
except Exception as e: # pylint: disable=broad-except
303+
log.warning(
304+
'Something went wrong when resolving label for {completion}: {e}',
305+
completion=completion, e=e
306+
)
307+
return ''
308+
309+
310+
def format_label(completion, sig):
311+
if sig and completion.type in ('function', 'method'):
312+
params = ', '.join(param.name for param in sig[0].params)
313+
label = '{}({})'.format(completion.name, params)
314+
return label
315+
return completion.name
316+
317+
318+
LABEL_RESOLVER = LabelResolver(format_label)

Diff for: test/plugins/test_completion.py

+21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright 2017-2020 Palantir Technologies, Inc.
22
# Copyright 2021- Python Language Server Contributors.
33

4+
import math
45
import os
56
import sys
67

@@ -148,6 +149,7 @@ def test_jedi_completion_item_resolve(config, workspace):
148149
# Over the blank line
149150
com_position = {'line': 8, 'character': 0}
150151
doc = Document(DOC_URI, workspace, DOC)
152+
config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': math.inf}}})
151153
completions = pylsp_jedi_completions(config, doc, com_position)
152154

153155
items = {c['label']: c for c in completions}
@@ -179,6 +181,24 @@ def test_jedi_completion_with_fuzzy_enabled(config, workspace):
179181
pylsp_jedi_completions(config, doc, {'line': 1, 'character': 1000})
180182

181183

184+
def test_jedi_completion_resolve_at_most(config, workspace):
185+
# Over 'i' in os.path.isabs(...)
186+
com_position = {'line': 1, 'character': 15}
187+
doc = Document(DOC_URI, workspace, DOC)
188+
189+
# Do not resolve any labels
190+
config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': 0}}})
191+
items = pylsp_jedi_completions(config, doc, com_position)
192+
labels = {i['label'] for i in items}
193+
assert 'isabs' in labels
194+
195+
# Resolve all items
196+
config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': math.inf}}})
197+
items = pylsp_jedi_completions(config, doc, com_position)
198+
labels = {i['label'] for i in items}
199+
assert 'isabs(path)' in labels
200+
201+
182202
def test_rope_completion(config, workspace):
183203
# Over 'i' in os.path.isabs(...)
184204
com_position = {'line': 1, 'character': 15}
@@ -194,6 +214,7 @@ def test_jedi_completion_ordering(config, workspace):
194214
# Over the blank line
195215
com_position = {'line': 8, 'character': 0}
196216
doc = Document(DOC_URI, workspace, DOC)
217+
config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': math.inf}}})
197218
completions = pylsp_jedi_completions(config, doc, com_position)
198219

199220
items = {c['label']: c['sortText'] for c in completions}

0 commit comments

Comments
 (0)