1+ import asyncio
2+ import dataclasses
3+ import pathlib
4+
5+ from finecode_extension_api import code_action
6+ from finecode_extension_api .actions import install_deps_in_env as install_deps_in_env_action
7+ from finecode_extension_api .interfaces import (icommandrunner , ilogger )
8+
9+
10+ @dataclasses .dataclass
11+ class PipInstallDepsInEnvHandlerConfig (code_action .ActionHandlerConfig ):
12+ find_links : list [str ] | None = None
13+
14+
15+ class PipInstallDepsInEnvHandler (
16+ code_action .ActionHandler [install_deps_in_env_action .InstallDepsInEnvAction , PipInstallDepsInEnvHandlerConfig ]
17+ ):
18+ def __init__ (self , config : PipInstallDepsInEnvHandlerConfig , command_runner : icommandrunner .ICommandRunner , logger : ilogger .ILogger ) -> None :
19+ self .config = config
20+ self .command_runner = command_runner
21+ self .logger = logger
22+
23+ async def run (
24+ self ,
25+ payload : install_deps_in_env_action .InstallDepsInEnvRunPayload ,
26+ run_context : install_deps_in_env_action .InstallDepsInEnvRunContext ,
27+ ) -> install_deps_in_env_action .InstallDepsInEnvRunResult :
28+ env_name = payload .env_name
29+ dependencies = payload .dependencies
30+ venv_dir_path = payload .venv_dir_path
31+ project_dir_path = payload .project_dir_path
32+ python_executable = venv_dir_path / 'bin' / 'python'
33+
34+ # split dependencies in editable and not editable because pip supports
35+ # installation of editable only with CLI flag '-e'
36+ editable_dependencies : list [install_deps_in_env_action .Dependency ] = []
37+ non_editable_dependencies : list [install_deps_in_env_action .Dependency ] = []
38+ for dependency in dependencies :
39+ if dependency .editable :
40+ editable_dependencies .append (dependency )
41+ else :
42+ non_editable_dependencies .append (dependency )
43+
44+ errors : list [str ] = []
45+ # run pip processes sequentially because they are executed in the same venv,
46+ # avoid potential concurrency problem in this way
47+ if len (non_editable_dependencies ) > 0 :
48+ cmd = self ._construct_pip_install_cmd (python_executable = python_executable , dependencies = non_editable_dependencies , editable = False )
49+ error = await self ._run_pip_cmd (cmd = cmd , env_name = env_name , project_dir_path = project_dir_path )
50+ if error is not None :
51+ errors .append (error )
52+
53+ # install editable after non-editable, because non-editable can overwrite editable if there is the same dependency
54+ if len (editable_dependencies ) > 0 :
55+ cmd = self ._construct_pip_install_cmd (python_executable = python_executable , dependencies = editable_dependencies , editable = True )
56+ error = await self ._run_pip_cmd (cmd = cmd , env_name = env_name , project_dir_path = project_dir_path )
57+ if error is not None :
58+ errors .append (error )
59+
60+ return install_deps_in_env_action .InstallDepsInEnvRunResult (errors = errors )
61+
62+ def _construct_pip_install_cmd (self , python_executable : pathlib .Path , dependencies : list [install_deps_in_env_action .Dependency ], editable : bool ) -> str :
63+ install_params : str = ''
64+ if editable :
65+ install_params += '-e '
66+
67+ if self .config .find_links is not None :
68+ for link in self .config .find_links :
69+ install_params += f' --find-links="{ link } "'
70+
71+ for dependency in dependencies :
72+ if '@ file://' in dependency .version_or_source :
73+ # dependency is specified as '<name> @ file://' but pip CLI supports
74+ # only 'file://'
75+ start_idx_of_file_uri = dependency .version_or_source .index ('file://' )
76+ # put in single quoutes to avoid problems in case of spaces in path
77+ # because in CLI commands single dependencies are splitted by space
78+ install_params += f"'{ dependency .version_or_source [start_idx_of_file_uri :]} ' "
79+ else :
80+ # put in single quoutes to avoid problems in case of spaces in version,
81+ # because in CLI commands single dependencies are splitted by space
82+ install_params += f"'{ dependency .name } { dependency .version_or_source } ' "
83+ cmd = f'{ python_executable } -m pip --disable-pip-version-check install { install_params } '
84+ return cmd
85+
86+ async def _run_pip_cmd (self , cmd : str , env_name : str , project_dir_path : pathlib .Path ) -> str | None :
87+ process = await self .command_runner .run (cmd , cwd = project_dir_path )
88+ await process .wait_for_end ()
89+ if process .get_exit_code () != 0 :
90+ return f'Installation of dependencies "{ cmd } " in env { env_name } from { project_dir_path } failed:\n stdout: { process .get_output ()} \n stderr: { process .get_error_output ()} '
91+
92+ return None
0 commit comments