Skip to content

Commit f02235e

Browse files
goanpecaccordoba12
authored andcommitted
Add Jedi support for extra paths and different environment handling (#680)
1 parent 1c0c540 commit f02235e

File tree

7 files changed

+189
-25
lines changed

7 files changed

+189
-25
lines changed

.circleci/config.yml

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ jobs:
1717
- image: "python:3.5-stretch"
1818
steps:
1919
- checkout
20+
# To test Jedi environments
21+
- run: python3 -m venv /tmp/pyenv
22+
- run: /tmp/pyenv/bin/python -m pip install loghub
2023
- run: pip install -e .[all] .[test]
2124
- run: py.test -v test/
2225

pyls/python_ls.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,10 @@ def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializati
202202

203203
self.workspaces.pop(self.root_uri, None)
204204
self.root_uri = rootUri
205-
self.workspace = Workspace(rootUri, self._endpoint)
206-
self.workspaces[rootUri] = self.workspace
207205
self.config = config.Config(rootUri, initializationOptions or {},
208206
processId, _kwargs.get('capabilities', {}))
207+
self.workspace = Workspace(rootUri, self._endpoint, self.config)
208+
self.workspaces[rootUri] = self.workspace
209209
self._dispatchers = self._hook('pyls_dispatchers')
210210
self._hook('pyls_initialize')
211211

@@ -355,6 +355,7 @@ def m_workspace__did_change_configuration(self, settings=None):
355355
self.config.update((settings or {}).get('pyls', {}))
356356
for workspace_uri in self.workspaces:
357357
workspace = self.workspaces[workspace_uri]
358+
workspace.update_config(self.config)
358359
for doc_uri in workspace.documents:
359360
self.lint(doc_uri, is_saved=False)
360361

@@ -365,7 +366,7 @@ def m_workspace__did_change_workspace_folders(self, added=None, removed=None, **
365366

366367
for added_info in added:
367368
added_uri = added_info['uri']
368-
self.workspaces[added_uri] = Workspace(added_uri, self._endpoint)
369+
self.workspaces[added_uri] = Workspace(added_uri, self._endpoint, self.config)
369370

370371
# Migrate documents that are on the root workspace and have a better
371372
# match now

pyls/workspace.py

+55-9
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ class Workspace(object):
2121
M_APPLY_EDIT = 'workspace/applyEdit'
2222
M_SHOW_MESSAGE = 'window/showMessage'
2323

24-
def __init__(self, root_uri, endpoint):
24+
def __init__(self, root_uri, endpoint, config=None):
25+
self._config = config
2526
self._root_uri = root_uri
2627
self._endpoint = endpoint
2728
self._root_uri_scheme = uris.urlparse(self._root_uri)[0]
2829
self._root_path = uris.to_fs_path(self._root_uri)
2930
self._docs = {}
3031

32+
# Cache jedi environments
33+
self._environments = {}
34+
3135
# Whilst incubating, keep rope private
3236
self.__rope = None
3337
self.__rope_config = None
@@ -77,6 +81,11 @@ def update_document(self, doc_uri, change, version=None):
7781
self._docs[doc_uri].apply_change(change)
7882
self._docs[doc_uri].version = version
7983

84+
def update_config(self, config):
85+
self._config = config
86+
for doc_uri in self.documents:
87+
self.get_document(doc_uri).update_config(config)
88+
8089
def apply_edit(self, edit):
8190
return self._endpoint.request(self.M_APPLY_EDIT, {'edit': edit})
8291

@@ -97,17 +106,21 @@ def _create_document(self, doc_uri, source=None, version=None):
97106
doc_uri, source=source, version=version,
98107
extra_sys_path=self.source_roots(path),
99108
rope_project_builder=self._rope_project_builder,
109+
config=self._config, workspace=self,
100110
)
101111

102112

103113
class Document(object):
104114

105-
def __init__(self, uri, source=None, version=None, local=True, extra_sys_path=None, rope_project_builder=None):
115+
def __init__(self, uri, source=None, version=None, local=True, extra_sys_path=None, rope_project_builder=None,
116+
config=None, workspace=None):
106117
self.uri = uri
107118
self.version = version
108119
self.path = uris.to_fs_path(uri)
109120
self.filename = os.path.basename(self.path)
110121

122+
self._config = config
123+
self._workspace = workspace
111124
self._local = local
112125
self._source = source
113126
self._extra_sys_path = extra_sys_path or []
@@ -131,6 +144,9 @@ def source(self):
131144
return f.read()
132145
return self._source
133146

147+
def update_config(self, config):
148+
self._config = config
149+
134150
def apply_change(self, change):
135151
"""Apply a change to the document."""
136152
text = change['text']
@@ -197,28 +213,58 @@ def word_at_position(self, position):
197213
return m_start[0] + m_end[-1]
198214

199215
def jedi_names(self, all_scopes=False, definitions=True, references=False):
216+
environment_path = None
217+
if self._config:
218+
jedi_settings = self._config.plugin_settings('jedi', document_path=self.path)
219+
environment_path = jedi_settings.get('environment')
220+
environment = self.get_enviroment(environment_path) if environment_path else None
221+
200222
return jedi.api.names(
201223
source=self.source, path=self.path, all_scopes=all_scopes,
202-
definitions=definitions, references=references
224+
definitions=definitions, references=references, environment=environment,
203225
)
204226

205227
def jedi_script(self, position=None):
228+
extra_paths = []
229+
environment_path = None
230+
231+
if self._config:
232+
jedi_settings = self._config.plugin_settings('jedi', document_path=self.path)
233+
environment_path = jedi_settings.get('environment')
234+
extra_paths = jedi_settings.get('extra_paths') or []
235+
236+
sys_path = self.sys_path(environment_path) + extra_paths
237+
environment = self.get_enviroment(environment_path) if environment_path else None
238+
206239
kwargs = {
207240
'source': self.source,
208241
'path': self.path,
209-
'sys_path': self.sys_path()
242+
'sys_path': sys_path,
243+
'environment': environment,
210244
}
245+
211246
if position:
212247
kwargs['line'] = position['line'] + 1
213248
kwargs['column'] = _utils.clip_column(position['character'], self.lines, position['line'])
249+
214250
return jedi.Script(**kwargs)
215251

216-
def sys_path(self):
252+
def get_enviroment(self, environment_path=None):
253+
# TODO(gatesn): #339 - make better use of jedi environments, they seem pretty powerful
254+
if environment_path is None:
255+
environment = jedi.api.environment.get_cached_default_environment()
256+
else:
257+
if environment_path in self._workspace._environments:
258+
environment = self._workspace._environments[environment_path]
259+
else:
260+
environment = jedi.api.environment.create_environment(path=environment_path, safe=False)
261+
self._workspace._environments[environment_path] = environment
262+
263+
return environment
264+
265+
def sys_path(self, environment_path=None):
217266
# Copy our extra sys path
218267
path = list(self._extra_sys_path)
219-
220-
# TODO(gatesn): #339 - make better use of jedi environments, they seem pretty powerful
221-
environment = jedi.api.environment.get_cached_default_environment()
268+
environment = self.get_enviroment(environment_path=environment_path)
222269
path.extend(environment.get_sys_path())
223-
224270
return path

test/plugins/test_completion.py

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Copyright 2017 Palantir Technologies, Inc.
22
from distutils.version import LooseVersion
33
import os
4+
import sys
5+
6+
from test.test_utils import MockWorkspace
47
import jedi
58
import pytest
69

@@ -9,10 +12,13 @@
912
from pyls.plugins.jedi_completion import pyls_completions as pyls_jedi_completions
1013
from pyls.plugins.rope_completion import pyls_completions as pyls_rope_completions
1114

15+
16+
PY2 = sys.version[0] == '2'
17+
LINUX = sys.platform.startswith('linux')
18+
CI = os.environ.get('CI')
1219
LOCATION = os.path.realpath(
1320
os.path.join(os.getcwd(), os.path.dirname(__file__))
1421
)
15-
1622
DOC_URI = uris.from_fs_path(__file__)
1723
DOC = """import os
1824
print os.path.isabs("/tmp")
@@ -200,3 +206,62 @@ def test_multistatement_snippet(config):
200206
position = {'line': 0, 'character': len(document)}
201207
completions = pyls_jedi_completions(config, doc, position)
202208
assert completions[0]['insertText'] == 'date(${1:year}, ${2:month}, ${3:day})$0'
209+
210+
211+
def test_jedi_completion_extra_paths(config, tmpdir):
212+
# Create a tempfile with some content and pass to extra_paths
213+
temp_doc_content = '''
214+
def spam():
215+
pass
216+
'''
217+
p = tmpdir.mkdir("extra_path")
218+
extra_paths = [str(p)]
219+
p = p.join("foo.py")
220+
p.write(temp_doc_content)
221+
222+
# Content of doc to test completion
223+
doc_content = """import foo
224+
foo.s"""
225+
doc = Document(DOC_URI, doc_content)
226+
227+
# After 'foo.s' without extra paths
228+
com_position = {'line': 1, 'character': 5}
229+
completions = pyls_jedi_completions(config, doc, com_position)
230+
assert completions is None
231+
232+
# Update config extra paths
233+
config.update({'plugins': {'jedi': {'extra_paths': extra_paths}}})
234+
doc.update_config(config)
235+
236+
# After 'foo.s' with extra paths
237+
com_position = {'line': 1, 'character': 5}
238+
completions = pyls_jedi_completions(config, doc, com_position)
239+
assert completions[0]['label'] == 'spam()'
240+
241+
242+
@pytest.mark.skipif(PY2 or not LINUX or not CI, reason="tested on linux and python 3 only")
243+
def test_jedi_completion_environment(config):
244+
# Content of doc to test completion
245+
doc_content = '''import logh
246+
'''
247+
doc = Document(DOC_URI, doc_content, workspace=MockWorkspace())
248+
249+
# After 'import logh' with default environment
250+
com_position = {'line': 0, 'character': 11}
251+
252+
assert os.path.isdir('/tmp/pyenv/')
253+
254+
config.update({'plugins': {'jedi': {'environment': None}}})
255+
doc.update_config(config)
256+
completions = pyls_jedi_completions(config, doc, com_position)
257+
assert completions is None
258+
259+
# Update config extra environment
260+
env_path = '/tmp/pyenv/bin/python'
261+
config.update({'plugins': {'jedi': {'environment': env_path}}})
262+
doc.update_config(config)
263+
264+
# After 'import logh' with new environment
265+
completions = pyls_jedi_completions(config, doc, com_position)
266+
assert completions[0]['label'] == 'loghub'
267+
assert 'changelog generator' in completions[0]['documentation'].lower()

test/plugins/test_symbols.py

+37-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
# Copyright 2017 Palantir Technologies, Inc.
2+
import os
3+
import sys
4+
5+
from test.test_utils import MockWorkspace
6+
import pytest
7+
28
from pyls import uris
39
from pyls.plugins.symbols import pyls_document_symbols
410
from pyls.lsp import SymbolKind
511
from pyls.workspace import Document
612

13+
14+
PY2 = sys.version[0] == '2'
15+
LINUX = sys.platform.startswith('linux')
16+
CI = os.environ.get('CI')
717
DOC_URI = uris.from_fs_path(__file__)
818
DOC = """import sys
919
@@ -21,6 +31,23 @@ def main(x):
2131
"""
2232

2333

34+
def helper_check_symbols_all_scope(symbols):
35+
# All eight symbols (import sys, a, B, __init__, x, y, main, y)
36+
assert len(symbols) == 8
37+
38+
def sym(name):
39+
return [s for s in symbols if s['name'] == name][0]
40+
41+
# Check we have some sane mappings to VSCode constants
42+
assert sym('a')['kind'] == SymbolKind.Variable
43+
assert sym('B')['kind'] == SymbolKind.Class
44+
assert sym('__init__')['kind'] == SymbolKind.Function
45+
assert sym('main')['kind'] == SymbolKind.Function
46+
47+
# Not going to get too in-depth here else we're just testing Jedi
48+
assert sym('a')['location']['range']['start'] == {'line': 2, 'character': 0}
49+
50+
2451
def test_symbols(config):
2552
doc = Document(DOC_URI, DOC)
2653
config.update({'plugins': {'jedi_symbols': {'all_scopes': False}}})
@@ -49,18 +76,16 @@ def sym(name):
4976
def test_symbols_all_scopes(config):
5077
doc = Document(DOC_URI, DOC)
5178
symbols = pyls_document_symbols(config, doc)
79+
helper_check_symbols_all_scope(symbols)
5280

53-
# All eight symbols (import sys, a, B, __init__, x, y, main, y)
54-
assert len(symbols) == 8
55-
56-
def sym(name):
57-
return [s for s in symbols if s['name'] == name][0]
5881

59-
# Check we have some sane mappings to VSCode constants
60-
assert sym('a')['kind'] == SymbolKind.Variable
61-
assert sym('B')['kind'] == SymbolKind.Class
62-
assert sym('__init__')['kind'] == SymbolKind.Function
63-
assert sym('main')['kind'] == SymbolKind.Function
82+
@pytest.mark.skipif(PY2 or not LINUX or not CI, reason="tested on linux and python 3 only")
83+
def test_symbols_all_scopes_with_jedi_environment(config):
84+
doc = Document(DOC_URI, DOC, workspace=MockWorkspace())
6485

65-
# Not going to get too in-depth here else we're just testing Jedi
66-
assert sym('a')['location']['range']['start'] == {'line': 2, 'character': 0}
86+
# Update config extra environment
87+
env_path = '/tmp/pyenv/bin/python'
88+
config.update({'plugins': {'jedi': {'environment': env_path}}})
89+
doc.update_config(config)
90+
symbols = pyls_document_symbols(config, doc)
91+
helper_check_symbols_all_scope(symbols)

test/test_utils.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
# Copyright 2017 Palantir Technologies, Inc.
22
import time
3+
import sys
4+
35
import mock
6+
47
from pyls import _utils
58

69

10+
class MockWorkspace(object):
11+
"""Mock workspace used by tests that use jedi environment."""
12+
13+
def __init__(self):
14+
"""Mock workspace used by tests that use jedi environment."""
15+
self._environments = {}
16+
17+
# This is to avoid pyling tests of the variable not being used
18+
sys.stdout.write(str(self._environments))
19+
20+
721
def test_debounce():
822
interval = 0.1
923
obj = mock.Mock()

vscode-client/package.json

+10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@
3535
},
3636
"uniqueItems": true
3737
},
38+
"pyls.plugins.jedi.extra_paths": {
39+
"type": "array",
40+
"default": [],
41+
"description": "Define extra paths for jedi.Script."
42+
},
43+
"pyls.plugins.jedi.environment": {
44+
"type": "string",
45+
"default": null,
46+
"description": "Define environment for jedi.Script and Jedi.names."
47+
},
3848
"pyls.plugins.jedi_completion.enabled": {
3949
"type": "boolean",
4050
"default": true,

0 commit comments

Comments
 (0)