Skip to content

Commit 6dd31cc

Browse files
committed
Implement cached label resolution and label resolution limit
1 parent b65f4a8 commit 6dd31cc

File tree

2 files changed

+121
-12
lines changed

2 files changed

+121
-12
lines changed

pylsp/plugins/jedi_completion.py

+101-12
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
@@ -55,6 +58,7 @@
5558
@hookimpl
5659
def pylsp_completions(config, document, position):
5760
"""Get formatted completions for current code position"""
61+
# pylint: disable=too-many-locals
5862
settings = config.plugin_settings('jedi_completion', document_path=document.path)
5963
code_position = _utils.position_to_jedi_linecolumn(document, position)
6064

@@ -70,18 +74,28 @@ def pylsp_completions(config, document, position):
7074
should_include_params = settings.get('include_params')
7175
should_include_class_objects = settings.get('include_class_objects', True)
7276

77+
max_labels_resolve = settings.get('resolve_at_most_labels', 25)
78+
7379
include_params = snippet_support and should_include_params and use_snippets(document, position)
7480
include_class_objects = snippet_support and should_include_class_objects and use_snippets(document, position)
7581

7682
ready_completions = [
77-
_format_completion(c, include_params)
78-
for c in completions
83+
_format_completion(
84+
c,
85+
include_params,
86+
resolve_label=(i < max_labels_resolve)
87+
)
88+
for i, c in enumerate(completions)
7989
]
8090

8191
if include_class_objects:
82-
for c in completions:
92+
for i, c in enumerate(completions):
8393
if c.type == 'class':
84-
completion_dict = _format_completion(c, False)
94+
completion_dict = _format_completion(
95+
c,
96+
False,
97+
resolve_label=(i < max_labels_resolve)
98+
)
8599
completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter
86100
completion_dict['label'] += ' object'
87101
ready_completions.append(completion_dict)
@@ -139,9 +153,9 @@ def use_snippets(document, position):
139153
not (expr_type in _ERRORS and 'import' in code))
140154

141155

142-
def _format_completion(d, include_params=True):
156+
def _format_completion(d, include_params=True, resolve_label=False):
143157
completion = {
144-
'label': _label(d),
158+
'label': _label(d, resolve_label),
145159
'kind': _TYPE_MAP.get(d.type),
146160
'detail': _detail(d),
147161
'documentation': _utils.format_docstring(d.docstring()),
@@ -180,12 +194,12 @@ def _format_completion(d, include_params=True):
180194
return completion
181195

182196

183-
def _label(definition):
184-
sig = definition.get_signatures()
185-
if definition.type in ('function', 'method') and sig:
186-
params = ', '.join(param.name for param in sig[0].params)
187-
return '{}({})'.format(definition.name, params)
188-
197+
def _label(definition, resolve=False):
198+
if not resolve:
199+
return definition.name
200+
sig = LABEL_RESOLVER.get_or_create(definition)
201+
if sig:
202+
return sig
189203
return definition.name
190204

191205

@@ -204,3 +218,78 @@ def _sort_text(definition):
204218
# If its 'hidden', put it next last
205219
prefix = 'z{}' if definition.name.startswith('_') else 'a{}'
206220
return prefix.format(definition.name)
221+
222+
223+
class LabelResolver:
224+
225+
def __init__(self, format_label_callback, time_to_live=60 * 30):
226+
self.format_label = format_label_callback
227+
self._cache = {}
228+
self._time_to_live = time_to_live
229+
self._cache_ttl = defaultdict(set)
230+
self._clear_every = 2
231+
# see https://github.com/davidhalter/jedi/blob/master/jedi/inference/helpers.py#L194-L202
232+
self._cached_modules = {'pandas', 'numpy', 'tensorflow', 'matplotlib'}
233+
234+
def clear_outdated(self):
235+
now = self.time_key()
236+
to_clear = [
237+
timestamp
238+
for timestamp in self._cache_ttl
239+
if timestamp < now
240+
]
241+
for time_key in to_clear:
242+
for key in self._cache_ttl[time_key]:
243+
del self._cache[key]
244+
del self._cache_ttl[time_key]
245+
246+
def time_key(self):
247+
return int(time() / self._time_to_live)
248+
249+
def get_or_create(self, completion: Completion):
250+
if not completion.full_name:
251+
use_cache = False
252+
else:
253+
module_parts = completion.full_name.split('.')
254+
use_cache = module_parts and module_parts[0] in self._cached_modules
255+
256+
if use_cache:
257+
key = self._create_completion_id(completion)
258+
if key not in self._cache:
259+
if self.time_key() % self._clear_every == 0:
260+
self.clear_outdated()
261+
262+
self._cache[key] = self.resolve_label(completion)
263+
self._cache_ttl[self.time_key()].add(key)
264+
return self._cache[key]
265+
266+
return self.resolve_label(completion)
267+
268+
def _create_completion_id(self, completion: Completion):
269+
return (
270+
completion.full_name, completion.module_path,
271+
completion.line, completion.column,
272+
self.time_key()
273+
)
274+
275+
def resolve_label(self, completion):
276+
try:
277+
sig = completion.get_signatures()
278+
return self.format_label(completion, sig)
279+
except Exception as e: # pylint: disable=broad-except
280+
log.warning(
281+
'Something went wrong when resolving label for {completion}: {e}',
282+
completion=completion, e=e
283+
)
284+
return ''
285+
286+
287+
def format_label(completion, sig):
288+
if sig and completion.type in ('function', 'method'):
289+
params = ', '.join(param.name for param in sig[0].params)
290+
label = '{}({})'.format(completion.name, params)
291+
return label
292+
return completion.name
293+
294+
295+
LABEL_RESOLVER = LabelResolver(format_label)

test/plugins/test_completion.py

+20
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

@@ -79,6 +80,24 @@ def test_jedi_completion_with_fuzzy_enabled(config, workspace):
7980
pylsp_jedi_completions(config, doc, {'line': 1, 'character': 1000})
8081

8182

83+
def test_jedi_completion_resolve_at_most(config, workspace):
84+
# Over 'i' in os.path.isabs(...)
85+
com_position = {'line': 1, 'character': 15}
86+
doc = Document(DOC_URI, workspace, DOC)
87+
88+
# Do not resolve any labels
89+
config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': 0}}})
90+
items = pylsp_jedi_completions(config, doc, com_position)
91+
labels = {i['label'] for i in items}
92+
assert 'isabs' in labels
93+
94+
# Resolve all items
95+
config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': math.inf}}})
96+
items = pylsp_jedi_completions(config, doc, com_position)
97+
labels = {i['label'] for i in items}
98+
assert 'isabs(path)' in labels
99+
100+
82101
def test_rope_completion(config, workspace):
83102
# Over 'i' in os.path.isabs(...)
84103
com_position = {'line': 1, 'character': 15}
@@ -94,6 +113,7 @@ def test_jedi_completion_ordering(config, workspace):
94113
# Over the blank line
95114
com_position = {'line': 8, 'character': 0}
96115
doc = Document(DOC_URI, workspace, DOC)
116+
config.update({'plugins': {'jedi_completion': {'resolve_at_most_labels': math.inf}}})
97117
completions = pylsp_jedi_completions(config, doc, com_position)
98118

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

0 commit comments

Comments
 (0)