Skip to content

Commit d15a76e

Browse files
committed
Improve error handling. Add base_config. Add prepare_runners and install_deps_in_env actions and handlers.
1 parent 790c37a commit d15a76e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1496
-266
lines changed

extensions/fine_python_pip/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[project]
22
name = "fine-python-pip"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = ""
55
authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }]
66
readme = "README.md"
77
requires-python = ">=3.11, < 3.14"
8-
dependencies = ["finecode_extension_api==0.1.0"]
8+
dependencies = ["finecode_extension_api==0.1.3"]
99

1010
[dependency-groups]
11-
dev_workspace = ["finecode==0.2.*"]
11+
dev_workspace = ["finecode==0.2.6.dev14+g790c37afd.d20250821"]
1212
dev_no_runtime = ["finecode_dev_common_preset==0.1.*"]
1313

1414
[tool.finecode]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from setuptools import setup
2+
from setuptools.command.build import build
3+
from setuptools.command.build_ext import build_ext
4+
from setuptools.command.build_py import build_py
5+
from setuptools.command.egg_info import egg_info
6+
import tempfile
7+
import atexit
8+
import shutil
9+
import sys
10+
11+
# Create a single temp directory for all build operations
12+
_TEMP_BUILD_DIR = None
13+
14+
def get_temp_build_dir(pkg_name):
15+
global _TEMP_BUILD_DIR
16+
if _TEMP_BUILD_DIR is None:
17+
_TEMP_BUILD_DIR = tempfile.mkdtemp(prefix=f'{pkg_name}_build_')
18+
atexit.register(lambda: shutil.rmtree(_TEMP_BUILD_DIR, ignore_errors=True))
19+
return _TEMP_BUILD_DIR
20+
21+
class TempDirBuildMixin:
22+
def initialize_options(self):
23+
super().initialize_options()
24+
temp_dir = get_temp_build_dir(self.distribution.get_name())
25+
self.build_base = temp_dir
26+
27+
class TempDirEggInfoMixin:
28+
def initialize_options(self):
29+
super().initialize_options()
30+
temp_dir = get_temp_build_dir(self.distribution.get_name())
31+
self.egg_base = temp_dir
32+
33+
class CustomBuild(TempDirBuildMixin, build):
34+
pass
35+
36+
class CustomBuildPy(TempDirBuildMixin, build_py):
37+
pass
38+
39+
class CustomBuildExt(TempDirBuildMixin, build_ext):
40+
pass
41+
42+
class CustomEggInfo(TempDirEggInfoMixin, egg_info):
43+
def initialize_options(self):
44+
# Don't use temp dir for editable installs
45+
if '--editable' in sys.argv or '-e' in sys.argv:
46+
egg_info.initialize_options(self)
47+
else:
48+
super().initialize_options()
49+
50+
setup(
51+
name="finecode_extension_runner",
52+
cmdclass={
53+
'build': CustomBuild,
54+
'build_py': CustomBuildPy,
55+
'build_ext': CustomBuildExt,
56+
'egg_info': CustomEggInfo,
57+
}
58+
)
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
# TODO: is not used anymore
12
from .prepare_env_handler import PipPrepareEnvHandler
3+
from .install_deps_in_env_handler import PipInstallDepsInEnvHandler
24

35
__all__ = [
4-
'PipPrepareEnvHandler'
6+
'PipPrepareEnvHandler',
7+
'PipInstallDepsInEnvHandler'
58
]
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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:\nstdout: {process.get_output()}\nstderr: {process.get_error_output()}'
91+
92+
return None

extensions/fine_python_virtualenv/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
[project]
22
name = "fine-python-virtualenv"
3-
version = "0.1.0"
3+
version = "0.1.2"
44
description = ""
55
authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }]
66
readme = "README.md"
77
requires-python = ">=3.11, < 3.14"
88
dependencies = [
9-
"finecode_extension_api==0.1.0",
9+
"finecode_extension_api==0.1.3",
1010
"virtualenv (>=20.0.0,<21.0.0)",
1111
]
1212

1313
[dependency-groups]
14-
dev_workspace = ["finecode==0.2.*"]
14+
dev_workspace = ["finecode==0.2.6.dev14+g790c37afd.d20250821"]
1515
dev_no_runtime = ["finecode_dev_common_preset==0.1.*"]
1616

1717
[tool.finecode]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from setuptools import setup
2+
from setuptools.command.build import build
3+
from setuptools.command.build_ext import build_ext
4+
from setuptools.command.build_py import build_py
5+
from setuptools.command.egg_info import egg_info
6+
import tempfile
7+
import atexit
8+
import shutil
9+
import sys
10+
11+
# Create a single temp directory for all build operations
12+
_TEMP_BUILD_DIR = None
13+
14+
def get_temp_build_dir(pkg_name):
15+
global _TEMP_BUILD_DIR
16+
if _TEMP_BUILD_DIR is None:
17+
_TEMP_BUILD_DIR = tempfile.mkdtemp(prefix=f'{pkg_name}_build_')
18+
atexit.register(lambda: shutil.rmtree(_TEMP_BUILD_DIR, ignore_errors=True))
19+
return _TEMP_BUILD_DIR
20+
21+
class TempDirBuildMixin:
22+
def initialize_options(self):
23+
super().initialize_options()
24+
temp_dir = get_temp_build_dir(self.distribution.get_name())
25+
self.build_base = temp_dir
26+
27+
class TempDirEggInfoMixin:
28+
def initialize_options(self):
29+
super().initialize_options()
30+
temp_dir = get_temp_build_dir(self.distribution.get_name())
31+
self.egg_base = temp_dir
32+
33+
class CustomBuild(TempDirBuildMixin, build):
34+
pass
35+
36+
class CustomBuildPy(TempDirBuildMixin, build_py):
37+
pass
38+
39+
class CustomBuildExt(TempDirBuildMixin, build_ext):
40+
pass
41+
42+
class CustomEggInfo(TempDirEggInfoMixin, egg_info):
43+
def initialize_options(self):
44+
# Don't use temp dir for editable installs
45+
if '--editable' in sys.argv or '-e' in sys.argv:
46+
egg_info.initialize_options(self)
47+
else:
48+
super().initialize_options()
49+
50+
setup(
51+
name="finecode_extension_runner",
52+
cmdclass={
53+
'build': CustomBuild,
54+
'build_py': CustomBuildPy,
55+
'build_ext': CustomBuildExt,
56+
'egg_info': CustomEggInfo,
57+
}
58+
)
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
from .handler import VirtualenvPrepareEnvHandler
1+
from .prepare_envs_handler import VirtualenvPrepareEnvHandler
2+
from .prepare_runners_handler import VirtualenvPrepareRunnersHandler
23

34

45
__all__ = [
5-
'VirtualenvPrepareEnvHandler'
6+
'VirtualenvPrepareEnvHandler',
7+
'VirtualenvPrepareRunnersHandler'
68
]

extensions/fine_python_virtualenv/src/fine_python_virtualenv/handler.py renamed to extensions/fine_python_virtualenv/src/fine_python_virtualenv/prepare_envs_handler.py

File renamed without changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from finecode_extension_api.interfaces import ilogger, ifilemanager
2+
import virtualenv
3+
4+
from finecode_extension_api import code_action
5+
from finecode_extension_api.actions import prepare_runners as prepare_runners_action
6+
7+
8+
class VirtualenvPrepareRunnersHandlerConfig(code_action.ActionHandlerConfig):
9+
...
10+
11+
12+
class VirtualenvPrepareRunnersHandler(
13+
code_action.ActionHandler[prepare_runners_action.PrepareRunnersAction, VirtualenvPrepareRunnersHandlerConfig]
14+
):
15+
def __init__(
16+
self,
17+
config: VirtualenvPrepareRunnersHandlerConfig,
18+
logger: ilogger.ILogger,
19+
file_manager: ifilemanager.IFileManager
20+
) -> None:
21+
self.config = config
22+
self.logger = logger
23+
self.file_manager = file_manager
24+
25+
async def run(
26+
self,
27+
payload: prepare_runners_action.PrepareRunnersRunPayload,
28+
run_context: prepare_runners_action.PrepareRunnersRunContext,
29+
) -> prepare_runners_action.PrepareRunnersRunResult:
30+
# create virtual envs
31+
32+
# would it be faster parallel?
33+
for env_info in payload.envs:
34+
if payload.recreate and env_info.venv_dir_path.exists():
35+
self.logger.debug(f"Remove virtualenv dir {env_info.venv_dir_path}")
36+
await self.file_manager.remove_dir(env_info.venv_dir_path)
37+
38+
self.logger.info(f"Creating virtualenv {env_info.venv_dir_path}")
39+
if not env_info.venv_dir_path.exists():
40+
# TODO: '-p <identifier>'
41+
virtualenv.cli_run([env_info.venv_dir_path.as_posix()], options=None, setup_logging=False, env=None)
42+
else:
43+
self.logger.info(f"Virtualenv in {env_info} exists already")
44+
45+
return prepare_runners_action.PrepareRunnersRunResult(errors=[])

finecode_extension_api/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[project]
22
name = "finecode-extension-api"
3-
version = "0.1.0"
3+
version = "0.1.3"
44
description = ""
55
authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }]
66
readme = "README.md"
77
requires-python = ">=3.11, < 3.14"
88
dependencies = ["typing-extensions (>=4.12.2,<5.0.0)"]
99

1010
[dependency-groups]
11-
dev_workspace = ["finecode==0.2.*"]
11+
dev_workspace = ["finecode==0.2.6.dev14+g790c37afd.d20250821"]
1212
dev_no_runtime = ["finecode_dev_common_preset==0.1.*"]
1313

1414
[tool.finecode]

0 commit comments

Comments
 (0)