Skip to content

Commit e3c5dfe

Browse files
authored
Implement cached label resolution and label resolution limit (#26)
* Implement cached label resolution and label resolution limit * Add settings to schema * Update test for new option * Allow to customize the modules for which the labels should be cached * Fix the default
1 parent db9b055 commit e3c5dfe

File tree

4 files changed

+155
-13
lines changed

4 files changed

+155
-13
lines changed

CONFIGURATION.md

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ This server can be configured using `workspace/didChangeConfiguration` method. E
1212
| `pylsp.plugins.jedi_completion.include_class_objects` | `boolean` | Adds class objects as a separate completion item. | `true` |
1313
| `pylsp.plugins.jedi_completion.fuzzy` | `boolean` | Enable fuzzy when requesting autocomplete. | `false` |
1414
| `pylsp.plugins.jedi_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` |
15+
| `pylsp.plugins.jedi_completion.resolve_at_most_labels` | `number` | How many labels (at most) should be resolved? | `25` |
16+
| `pylsp.plugins.jedi_completion.cache_labels_for` | `array` of `string` items | Modules for which the labels should be cached. | `["pandas", "numpy", "tensorflow", "matplotlib"]` |
1517
| `pylsp.plugins.jedi_definition.enabled` | `boolean` | Enable or disable the plugin. | `true` |
1618
| `pylsp.plugins.jedi_definition.follow_imports` | `boolean` | The goto call will follow imports. | `true` |
1719
| `pylsp.plugins.jedi_definition.follow_builtin_imports` | `boolean` | If follow_imports is True will decide if it follow builtin imports. | `true` |
@@ -44,6 +46,7 @@ This server can be configured using `workspace/didChangeConfiguration` method. E
4446
| `pylsp.plugins.pylint.args` | `array` of non-unique `string` items | Arguments to pass to pylint. | `null` |
4547
| `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` |
4648
| `pylsp.plugins.rope_completion.enabled` | `boolean` | Enable or disable the plugin. | `true` |
49+
| `pylsp.plugins.rope_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` |
4750
| `pylsp.plugins.yapf.enabled` | `boolean` | Enable or disable the plugin. | `true` |
4851
| `pylsp.rope.extensionModules` | `string` | Builtin and c-extension modules that are allowed to be imported and inspected by rope. | `null` |
4952
| `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` |

pylsp/config/schema.json

+18
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@
5454
"default": false,
5555
"description": "Resolve documentation and detail eagerly."
5656
},
57+
"pylsp.plugins.jedi_completion.resolve_at_most_labels": {
58+
"type": "number",
59+
"default": 25,
60+
"description": "How many labels (at most) should be resolved?"
61+
},
62+
"pylsp.plugins.jedi_completion.cache_labels_for": {
63+
"type": "array",
64+
"items": {
65+
"type": "string"
66+
},
67+
"default": ["pandas", "numpy", "tensorflow", "matplotlib"],
68+
"description": "Modules for which the labels should be cached."
69+
},
5770
"pylsp.plugins.jedi_definition.enabled": {
5871
"type": "boolean",
5972
"default": true,
@@ -258,6 +271,11 @@
258271
"default": true,
259272
"description": "Enable or disable the plugin."
260273
},
274+
"pylsp.plugins.rope_completion.eager": {
275+
"type": "boolean",
276+
"default": false,
277+
"description": "Resolve documentation and detail eagerly."
278+
},
261279
"pylsp.plugins.yapf.enabled": {
262280
"type": "boolean",
263281
"default": true,

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)

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)