Skip to content

Commit 6b58a1e

Browse files
committed
Introduce pyls.sourceRoots configuration
Before this commit, in order to specify source root(s) other than the workspace root, 'setup.py' or 'pyproject.toml' should be created, even if such file is not actually needed. In order to explicitly specify source root(s) in the workspace, this commit introduces pyls.sourceRoots configuration, and makes Workspace.source_roots() return its value if configured. Path in pyls.sourceRoots is ignored, if it refers outside of the workspace. This configuration is also useful in the case that the workspace consists of multiple (independent) services and a library shared by them, for example. In such case, N + 1 source roots are listed in pyls.sourceRoots configuration.
1 parent 45b067c commit 6b58a1e

File tree

4 files changed

+90
-0
lines changed

4 files changed

+90
-0
lines changed

pyls/config/pyls_conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
PROJECT_CONFIGS = ['setup.cfg', 'tox.ini']
99

1010
OPTIONS = [
11+
('source-roots', 'sourceRoots', list),
1112
]
1213

1314

pyls/workspace.py

+30
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,38 @@ def publish_diagnostics(self, doc_uri, diagnostics):
9595
def show_message(self, message, msg_type=lsp.MessageType.Info):
9696
self._endpoint.notify(self.M_SHOW_MESSAGE, params={'type': msg_type, 'message': message})
9797

98+
def normalize_config_paths(self, config_key, inside_only=True):
99+
"""Return normalized paths specified in pyls configuration"""
100+
raw_paths = self._config and self._config.settings().get(config_key)
101+
if not raw_paths:
102+
return []
103+
104+
inside_prefix = self._root_path + os.path.sep
105+
paths = []
106+
107+
for raw_path in raw_paths:
108+
full_path = os.path.normpath(os.path.join(self._root_path, raw_path))
109+
if (not inside_only or
110+
# in "inside_only=True" case, path must (1) not be
111+
# absolute (= be relative to root of workspace),
112+
# (2) not have drive letter (on Windows), and (3)
113+
# be inside workspace
114+
(not os.path.isabs(raw_path) and
115+
not os.path.splitdrive(raw_path)[0] and
116+
full_path.startswith(inside_prefix))):
117+
paths.append(full_path)
118+
else:
119+
log.warning("Ignoring path '%s' in pyls.%s", raw_path, config_key)
120+
121+
return paths
122+
98123
def source_roots(self, document_path):
99124
"""Return the source roots for the given document."""
125+
source_roots = self.normalize_config_paths('sourceRoots')
126+
if source_roots:
127+
# Use specified sourceRoots without traversing upper directories
128+
return source_roots
129+
100130
files = _utils.find_parents(self._root_path, document_path, ['setup.py', 'pyproject.toml']) or []
101131
return list(set((os.path.dirname(project_file) for project_file in files))) or [self._root_path]
102132

test/test_workspace.py

+50
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
import pytest
66

77
from pyls import uris
8+
from pyls.python_ls import PythonLanguageServer
89

910
PY2 = sys.version_info.major == 2
1011

1112
if PY2:
1213
import pathlib2 as pathlib
14+
from StringIO import StringIO
1315
else:
1416
import pathlib
17+
from io import StringIO
1518

1619

1720
DOC_URI = uris.from_fs_path(__file__)
@@ -119,3 +122,50 @@ def test_multiple_workspaces(tmpdir, pyls):
119122
pyls.m_workspace__did_change_workspace_folders(
120123
added=[], removed=[added_workspaces[0]])
121124
assert workspace1_uri not in pyls.workspaces
125+
126+
127+
@pytest.mark.parametrize('metafile', ['setup.cfg', 'tox.ini', None])
128+
def test_source_roots_config(tmpdir, metafile):
129+
root_path = str(tmpdir)
130+
131+
invalid_roots = ['/invalid/root', '../baz']
132+
source_roots = ['service/foo', 'service/bar'] + invalid_roots
133+
134+
if metafile:
135+
# configured by metafile at pyls startup
136+
with open(os.path.join(root_path, metafile), 'w+') as f:
137+
f.write('[pyls]\nsource-roots=\n %s\n' %
138+
',\n '.join(source_roots))
139+
140+
pyls = PythonLanguageServer(StringIO, StringIO)
141+
pyls.m_initialize(
142+
processId=1,
143+
rootUri=uris.from_fs_path(root_path),
144+
initializationOptions={}
145+
)
146+
147+
if not metafile:
148+
# configured by client via LSP after pyls startup
149+
pyls.m_workspace__did_change_configuration({
150+
'pyls': {'sourceRoots': source_roots},
151+
})
152+
153+
# put new document under ROOT/service/foo
154+
prefix = os.path.join(root_path, source_roots[0])
155+
test_uri = uris.from_fs_path(os.path.join(prefix, 'hello/test.py'))
156+
pyls.workspace.put_document(test_uri, 'assert True')
157+
test_doc = pyls.workspace.get_document(test_uri)
158+
159+
# apply os.path.normcase() on paths below, because case-sensitive
160+
# comparison on Windows causes unintentional failure for case
161+
# instability around drive letter
162+
163+
sys_path = [os.path.normcase(p) for p in test_doc.sys_path()]
164+
for raw_path in source_roots:
165+
full_path = os.path.normcase(os.path.join(root_path, raw_path))
166+
norm_path = os.path.normpath(full_path)
167+
if raw_path in invalid_roots:
168+
assert norm_path not in sys_path
169+
assert full_path not in sys_path # check for safety
170+
else:
171+
assert norm_path in sys_path

vscode-client/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@
3535
},
3636
"uniqueItems": true
3737
},
38+
"pyls.sourceRoots": {
39+
"type": "array",
40+
"default": [],
41+
"description": "List of source roots in the working space.",
42+
"items": {
43+
"type": "string"
44+
},
45+
"uniqueItems": true
46+
},
3847
"pyls.plugins.jedi.extra_paths": {
3948
"type": "array",
4049
"default": [],

0 commit comments

Comments
 (0)