Skip to content
This repository was archived by the owner on Mar 27, 2019. It is now read-only.

Commit b17dae2

Browse files
committed
wip - support multiple projects in workspace
1 parent ebbd7dc commit b17dae2

File tree

3 files changed

+101
-32
lines changed

3 files changed

+101
-32
lines changed

langserver/jedi.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ def _new_script_impl(self, parent_span, *args, **kwargs):
3333

3434
if self.workspace is not None:
3535
path = self.workspace.project_to_cache_path(path)
36+
project = self.workspace.find_project_for_path(path)
3637

3738
environment = None
38-
for env in jedi.find_virtualenvs([self.workspace.VENV_PATH], safe=False):
39-
if env._base_path == self.workspace.VENV_PATH:
39+
for env in jedi.find_virtualenvs([project.VENV_PATH], safe=False):
40+
if env._base_path == project.VENV_PATH:
4041
environment = env
4142
break
4243

langserver/langserver.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,15 @@ def serve_x_definition(self, request):
354354
if not defs:
355355
return results
356356

357+
project = self.workspace.find_project_for_path(
358+
self.workspace.project_to_cache_path(path))
359+
357360
for d in defs:
358361
# TODO: handle case where a def doesn't have a module_path
359362
if not d.module_path:
360363
continue
361364

362-
module_kind, rel_module_path = self.workspace.get_module_info(
365+
module_kind, rel_module_path = project.get_module_info(
363366
d.module_path)
364367

365368
if module_kind != ModuleKind.PROJECT:

langserver/workspace.py

Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,9 @@ def __init__(self, fs: FileSystem, project_root: str,
4242
log.debug("Setting Cloned Project path to %s",
4343
self.CLONED_PROJECT_PATH)
4444

45-
self.PROJECT_HAS_PIPFILE = False
46-
4745
# Clone the project from the provided filesystem into the local
4846
# cache
4947
for file_path in self.fs.walk(str(self.PROJECT_ROOT)):
50-
if file_path == "/Pipfile":
51-
self.PROJECT_HAS_PIPFILE = True
5248

5349
cache_file_path = self.project_to_cache_path(file_path)
5450
try:
@@ -63,8 +59,83 @@ def __init__(self, fs: FileSystem, project_root: str,
6359
else:
6460
raise e
6561

62+
self.project = Project(self.CLONED_PROJECT_PATH,
63+
self.CLONED_PROJECT_PATH)
64+
65+
def find_project_for_path(self, path):
66+
return self.project.find_project_for_path(path)
67+
68+
def project_to_cache_path(self, project_path):
69+
"""
70+
Translates a path from the root of the project to the equivalent path in
71+
the local cache.
72+
73+
e.x.: '/a/b.py' -> '/python-cloned-projects-cache/project_name/a/b.py'
74+
"""
75+
76+
# strip the leading '/' so that we can join it properly
77+
return self.CLONED_PROJECT_PATH / project_path.lstrip("/")
78+
79+
def cleanup(self):
80+
self.project.cleanup()
81+
82+
83+
class Project:
84+
def __init__(self, workspace_root_dir, project_root_dir):
85+
self.WORKSPACE_ROOT_DIR = workspace_root_dir
86+
self.PROJECT_ROOT_DIR = project_root_dir
87+
self.sub_projects = self._find_subprojects(project_root_dir)
6688
self._install_external_dependencies()
6789

90+
def _find_subprojects(self, current_dir):
91+
'''
92+
Returns a map containing the top-level subprojects contained inside
93+
this project, keyed by the absolute path to the subproject.
94+
'''
95+
sub_projects = {}
96+
97+
top_level_folders = (
98+
child for child in current_dir.iterdir() if child.is_dir())
99+
100+
for folder in top_level_folders:
101+
102+
def gen_len(generator):
103+
return sum(1 for _ in generator)
104+
105+
# do any subfolders contain installation files?
106+
if any(gen_len(folder.glob(pattern)) for pattern in INSTALLATION_FILE_PATTERNS):
107+
sub_projects[folder] = Project(self.WORKSPACE_ROOT_DIR, folder)
108+
else:
109+
sub_projects.update(self._find_subprojects(folder))
110+
111+
return sub_projects
112+
113+
def find_project_for_path(self, path):
114+
'''
115+
Returns the deepest project instance that contains this path.
116+
117+
'''
118+
if path.is_file():
119+
folder = path.parent
120+
else:
121+
folder = path
122+
123+
if folder == self.PROJECT_ROOT_DIR:
124+
return self
125+
126+
# If the project_dir isn't an ancestor of the folder,
127+
# there is no way it or any of its subprojects
128+
# could contain this path
129+
if self.PROJECT_ROOT_DIR not in folder.parents:
130+
return None
131+
132+
for sub_project in self.sub_projects.values():
133+
deepest_project = sub_project.find_project_for_path(path)
134+
if deepest_project is not None:
135+
return deepest_project
136+
137+
return self
138+
68139
def get_module_info(self, raw_jedi_module_path):
69140
"""
70141
Given an absolute module path provided from jedi,
@@ -80,8 +151,8 @@ def get_module_info(self, raw_jedi_module_path):
80151

81152
module_path = Path(raw_jedi_module_path)
82153

83-
if self.CLONED_PROJECT_PATH in module_path.parents:
84-
return (ModuleKind.PROJECT, module_path.relative_to(self.CLONED_PROJECT_PATH))
154+
if self.PROJECT_ROOT_DIR in module_path.parents:
155+
return (ModuleKind.PROJECT, module_path.relative_to(self.WORKSPACE_ROOT_DIR))
85156

86157
if GlobalConfig.PYTHON_PATH in module_path.parents:
87158
return (ModuleKind.STANDARD_LIBRARY, module_path.relative_to(GlobalConfig.PYTHON_PATH))
@@ -103,17 +174,6 @@ def get_module_info(self, raw_jedi_module_path):
103174

104175
return (ModuleKind.UNKNOWN, module_path)
105176

106-
def project_to_cache_path(self, project_path):
107-
"""
108-
Translates a path from the root of the project to the equivalent path in
109-
the local cache.
110-
111-
e.x.: '/a/b.py' -> '/python-cloned-projects-cache/project_name/a/b.py'
112-
"""
113-
114-
# strip the leading '/' so that we can join it properly
115-
return self.CLONED_PROJECT_PATH / project_path.lstrip("/")
116-
117177
def _install_external_dependencies(self):
118178
"""
119179
Installs the external dependencies for the project.
@@ -128,21 +188,18 @@ def _install_external_dependencies(self):
128188
self._install_pipenv()
129189

130190
def _install_pipenv(self):
131-
# pipenv creates the Pipfile automatically whenever it does anything -
132-
# only install if the project had one to begin with
133-
if self.PROJECT_HAS_PIPFILE:
134-
if (self.CLONED_PROJECT_PATH / "Pipfile.lock").exists():
135-
self.run_command("pipenv install --dev --ignore-pipfile")
136-
elif (self.CLONED_PROJECT_PATH / "Pipfile").exists():
137-
self.run_command("pipenv install --dev")
191+
if (self.PROJECT_ROOT_DIR / "Pipfile.lock").exists():
192+
self.run_command("pipenv install --dev --ignore-pipfile")
193+
elif (self.PROJECT_ROOT_DIR / "Pipfile").exists():
194+
self.run_command("pipenv install --dev")
138195

139196
def _install_pip(self):
140-
for requirements_file in self.CLONED_PROJECT_PATH.glob("*requirements.txt"):
197+
for requirements_file in self.PROJECT_ROOT_DIR.glob(REQUIREMENTS_GLOB_PATTERN):
141198
self.run_command(
142199
"pip install -r {}".format(str(requirements_file.absolute())))
143200

144201
def _install_setup_py(self):
145-
if (self.CLONED_PROJECT_PATH / "setup.py").exists():
202+
if (self.PROJECT_ROOT_DIR / "setup.py").exists():
146203
self.run_command("python setup.py install")
147204

148205
@property
@@ -156,11 +213,14 @@ def VENV_PATH(self):
156213
return Path(venv_path)
157214

158215
def cleanup(self):
216+
for sub_project in self.sub_projects.values():
217+
sub_project.cleanup()
218+
159219
log.info("Removing project's virtual environment %s", self.VENV_PATH)
160220
self.remove_venv()
161221

162-
log.info("Removing cloned project cache %s", self.CLONED_PROJECT_PATH)
163-
rmtree(self.CLONED_PROJECT_PATH, ignore_errors=True)
222+
log.info("Removing cloned project cache %s", self.PROJECT_ROOT_DIR)
223+
rmtree(self.PROJECT_ROOT_DIR, ignore_errors=True)
164224

165225
def ensure_venv_created(self):
166226
'''
@@ -182,7 +242,7 @@ def run_command(self, command, no_prefix=False, **kwargs):
182242
- the projects virtual environment is loaded into
183243
the command's environment
184244
'''
185-
kwargs["cwd"] = self.CLONED_PROJECT_PATH
245+
kwargs["cwd"] = self.PROJECT_ROOT_DIR
186246

187247
if not no_prefix:
188248

@@ -203,6 +263,11 @@ def run_command(self, command, no_prefix=False, **kwargs):
203263
return delegator.run(command, **kwargs)
204264

205265

266+
REQUIREMENTS_GLOB_PATTERN = "*requirements.txt"
267+
268+
INSTALLATION_FILE_PATTERNS = ["Pipfile", REQUIREMENTS_GLOB_PATTERN, "setup.py"]
269+
270+
206271
class ModuleKind(Enum):
207272
PROJECT = 1
208273
STANDARD_LIBRARY = 2

0 commit comments

Comments
 (0)