From e8a4fe6142007401d0f7c9889db111ab0a88f11f Mon Sep 17 00:00:00 2001 From: Sven Heidemann Date: Mon, 27 Jun 2022 10:52:26 +0200 Subject: [PATCH] Added venv support to install command --- src/cpl_cli/command/install_service.py | 31 +++++++--- src/cpl_cli/configuration/project_settings.py | 18 +++--- .../configuration/venv_helper_service.py | 15 +++++ src/cpl_cli/cpl-cli.json | 4 +- src/cpl_cli/main.py | 2 + src/cpl_cli/startup.py | 36 +++++++++++ src/cpl_cli/startup_argument_extension.py | 61 +------------------ src/cpl_cli/startup_workspace_extension.py | 55 +++++++++++++++++ src/cpl_core/utils/pip.py | 41 ++++++------- .../custom/general/src/general/general.json | 2 +- 10 files changed, 163 insertions(+), 102 deletions(-) create mode 100644 src/cpl_cli/configuration/venv_helper_service.py create mode 100644 src/cpl_cli/startup_workspace_extension.py diff --git a/src/cpl_cli/command/install_service.py b/src/cpl_cli/command/install_service.py index d4342988..ec27a067 100644 --- a/src/cpl_cli/command/install_service.py +++ b/src/cpl_cli/command/install_service.py @@ -1,5 +1,6 @@ import json import os +import shutil import subprocess import textwrap import time @@ -9,12 +10,12 @@ from cpl_cli.command_abc import CommandABC from cpl_cli.configuration.build_settings import BuildSettings from cpl_cli.configuration.project_settings import ProjectSettings from cpl_cli.configuration.settings_helper import SettingsHelper +from cpl_cli.configuration.venv_helper_service import VenvHelper from cpl_cli.error import Error from cpl_core.configuration.configuration_abc import ConfigurationABC from cpl_core.console.console import Console from cpl_core.console.foreground_color_enum import ForegroundColorEnum -from cpl_core.environment.application_environment_abc import \ - ApplicationEnvironmentABC +from cpl_core.environment.application_environment_abc import ApplicationEnvironmentABC from cpl_core.utils.pip import Pip from packaging import version @@ -57,12 +58,27 @@ class InstallService(CommandABC): def _wait(self, t: int, *args, source: str = None, stdout=None, stderr=None): time.sleep(t) + def _init_venv(self): + if self._is_virtual: + return + venv_path = os.path.abspath(os.path.join(self._env.working_directory, self._project_settings.python_executable)) + + if not os.path.exists(venv_path): + Console.spinner( + f'Creating venv {venv_path}', + VenvHelper.create_venv, + venv_path, + text_foreground_color=ForegroundColorEnum.green, + spinner_foreground_color=ForegroundColorEnum.cyan + ) + + Pip.set_executable(venv_path) + def _install_project(self): """ Installs dependencies of CPl project :return: """ - if self._project_settings is None or self._build_settings is None: Error.error('The command requires to be run in an CPL project, but a project could not be found.') return @@ -71,8 +87,6 @@ class InstallService(CommandABC): Error.error(f'Found invalid dependencies in {self._project_file}.') return - if not self._is_virtual: - Pip.set_executable(self._project_settings.python_executable) for dependency in self._project_settings.dependencies: Console.spinner( f'Installing: {dependency}', @@ -94,9 +108,6 @@ class InstallService(CommandABC): :return: """ is_already_in_project = False - if not self._is_virtual: - Pip.set_executable(self._project_settings.python_executable) - if self._project_settings is None or self._build_settings is None: Error.error('The command requires to be run in an CPL project, but a project could not be found.') return @@ -200,7 +211,11 @@ class InstallService(CommandABC): args.remove('simulate') Console.write_line('Running in simulation mode:') + self._init_venv() + if len(args) == 0: self._install_project() else: self._install_package(args[0]) + + # shutil.rmtree(os.path.abspath(os.path.join(self._env.working_directory, self._project_settings.python_executable))) diff --git a/src/cpl_cli/configuration/project_settings.py b/src/cpl_cli/configuration/project_settings.py index c73bde40..94d490c6 100644 --- a/src/cpl_cli/configuration/project_settings.py +++ b/src/cpl_cli/configuration/project_settings.py @@ -3,12 +3,12 @@ import sys import traceback from typing import Optional +from cpl_cli.configuration.project_settings_name_enum import ProjectSettingsNameEnum +from cpl_cli.configuration.version_settings import VersionSettings +from cpl_cli.error import Error from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC from cpl_core.console.console import Console from cpl_core.console.foreground_color_enum import ForegroundColorEnum -from cpl_cli.configuration.version_settings import VersionSettings -from cpl_cli.configuration.project_settings_name_enum import ProjectSettingsNameEnum -from cpl_cli.error import Error class ProjectSettings(ConfigurationModelABC): @@ -114,13 +114,10 @@ class ProjectSettings(ConfigurationModelABC): self._python_version = settings[ProjectSettingsNameEnum.python_version.value] self._python_path = settings[ProjectSettingsNameEnum.python_path.value] - if ProjectSettingsNameEnum.python_path.value in settings and \ - sys.platform in settings[ProjectSettingsNameEnum.python_path.value]: + if ProjectSettingsNameEnum.python_path.value in settings and sys.platform in settings[ProjectSettingsNameEnum.python_path.value]: path = settings[ProjectSettingsNameEnum.python_path.value][sys.platform] - if not os.path.isfile(path) and not os.path.islink(path): - if path != '' and path is not None: - Error.warn(f'{ProjectSettingsNameEnum.python_path.value} not found') - + if path == '' or path is None: + Error.warn(f'{ProjectSettingsNameEnum.python_path.value} not found') path = sys.executable else: path = sys.executable @@ -134,7 +131,6 @@ class ProjectSettings(ConfigurationModelABC): except Exception as e: Console.set_foreground_color(ForegroundColorEnum.red) - Console.write_line( - f'[ ERROR ] [ {__name__} ]: Reading error in {ProjectSettings.__name__} settings') + Console.write_line(f'[ ERROR ] [ {__name__} ]: Reading error in {ProjectSettings.__name__} settings') Console.write_line(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') Console.set_foreground_color(ForegroundColorEnum.default) diff --git a/src/cpl_cli/configuration/venv_helper_service.py b/src/cpl_cli/configuration/venv_helper_service.py new file mode 100644 index 00000000..fcc6b9b9 --- /dev/null +++ b/src/cpl_cli/configuration/venv_helper_service.py @@ -0,0 +1,15 @@ +import os +import subprocess +import sys + + +class VenvHelper: + + @staticmethod + def create_venv(path): + subprocess.run( + [sys.executable, '-m', 'venv', os.path.abspath(os.path.join(path, '../../'))], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL + ) diff --git a/src/cpl_cli/cpl-cli.json b/src/cpl_cli/cpl-cli.json index 1f2c75d9..f0b0ebb9 100644 --- a/src/cpl_cli/cpl-cli.json +++ b/src/cpl_cli/cpl-cli.json @@ -19,7 +19,9 @@ "cpl-core>=2022.6.17.dev8" ], "PythonVersion": ">=3.10", - "PythonPath": {}, + "PythonPath": { + "linux": "../../venv" + }, "Classifiers": [] }, "BuildSettings": { diff --git a/src/cpl_cli/main.py b/src/cpl_cli/main.py index 4b6959d9..4ee21688 100644 --- a/src/cpl_cli/main.py +++ b/src/cpl_cli/main.py @@ -5,6 +5,7 @@ import pkg_resources from cpl_cli.cli import CLI from cpl_cli.startup import Startup from cpl_cli.startup_argument_extension import StartupArgumentExtension +from cpl_cli.startup_workspace_extension import StartupWorkspaceExtension from cpl_core.application.application_builder import ApplicationBuilder from cpl_core.application.startup_extension_abc import StartupExtensionABC @@ -31,6 +32,7 @@ def get_startup_extensions() -> list[Type[StartupExtensionABC]]: def main(): app_builder = ApplicationBuilder(CLI) app_builder.use_startup(Startup) + app_builder.use_extension(StartupWorkspaceExtension) app_builder.use_extension(StartupArgumentExtension) for extension in get_startup_extensions(): app_builder.use_extension(extension) diff --git a/src/cpl_cli/startup.py b/src/cpl_cli/startup.py index 48d2f0bd..847f8e3c 100644 --- a/src/cpl_cli/startup.py +++ b/src/cpl_cli/startup.py @@ -1,5 +1,23 @@ import os +from cpl_cli.command.add_service import AddService +from cpl_cli.command.build_service import BuildService +from cpl_cli.command.custom_script_service import CustomScriptService +from cpl_cli.command.generate_service import GenerateService +from cpl_cli.command.help_service import HelpService +from cpl_cli.command.install_service import InstallService +from cpl_cli.command.new_service import NewService +from cpl_cli.command.publish_service import PublishService +from cpl_cli.command.remove_service import RemoveService +from cpl_cli.command.run_service import RunService +from cpl_cli.command.start_service import StartService +from cpl_cli.command.uninstall_service import UninstallService +from cpl_cli.command.update_service import UpdateService +from cpl_cli.command.version_service import VersionService +from cpl_cli.validators.project_validator import ProjectValidator + +from cpl_cli.validators.workspace_validator import WorkspaceValidator + from cpl_core.console import Console from cpl_cli.error import Error @@ -37,4 +55,22 @@ class Startup(StartupABC): services.add_transient(PublisherABC, PublisherService) services.add_transient(LiveServerService) + services.add_transient(WorkspaceValidator) + services.add_transient(ProjectValidator) + + services.add_transient(AddService) + services.add_transient(BuildService) + services.add_transient(CustomScriptService) + services.add_transient(GenerateService) + services.add_transient(HelpService) + services.add_transient(InstallService) + services.add_transient(NewService) + services.add_transient(PublishService) + services.add_transient(RemoveService) + services.add_transient(RunService) + services.add_transient(StartService) + services.add_transient(UninstallService) + services.add_transient(UpdateService) + services.add_transient(VersionService) + return services.build_service_provider() diff --git a/src/cpl_cli/startup_argument_extension.py b/src/cpl_cli/startup_argument_extension.py index 01f89e2c..8e795305 100644 --- a/src/cpl_cli/startup_argument_extension.py +++ b/src/cpl_cli/startup_argument_extension.py @@ -1,11 +1,5 @@ -import os -from typing import Optional - -from cpl_core.console import Console - from cpl_cli.command.add_service import AddService from cpl_cli.command.build_service import BuildService -from cpl_cli.command.custom_script_service import CustomScriptService from cpl_cli.command.generate_service import GenerateService from cpl_cli.command.help_service import HelpService from cpl_cli.command.install_service import InstallService @@ -17,7 +11,6 @@ from cpl_cli.command.start_service import StartService from cpl_cli.command.uninstall_service import UninstallService from cpl_cli.command.update_service import UpdateService from cpl_cli.command.version_service import VersionService -from cpl_cli.configuration.workspace_settings import WorkspaceSettings from cpl_cli.validators.project_validator import ProjectValidator from cpl_cli.validators.workspace_validator import WorkspaceValidator from cpl_core.application.startup_extension_abc import StartupExtensionABC @@ -25,7 +18,6 @@ from cpl_core.configuration.argument_type_enum import ArgumentTypeEnum from cpl_core.configuration.configuration_abc import ConfigurationABC from cpl_core.dependency_injection.service_collection_abc import ServiceCollectionABC from cpl_core.environment.application_environment_abc import ApplicationEnvironmentABC -from cpl_core.utils.string import String class StartupArgumentExtension(StartupExtensionABC): @@ -33,40 +25,7 @@ class StartupArgumentExtension(StartupExtensionABC): def __init__(self): pass - @staticmethod - def _search_project_json(working_directory: str) -> Optional[str]: - project_name = None - name = os.path.basename(working_directory) - for r, d, f in os.walk(working_directory): - for file in f: - if file.endswith('.json'): - f_name = file.split('.json')[0] - if f_name == name or String.convert_to_camel_case(f_name).lower() == String.convert_to_camel_case(name).lower(): - project_name = f_name - break - - return project_name - - def _read_cpl_environment(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): - workspace: Optional[WorkspaceSettings] = config.get_configuration(WorkspaceSettings) - config.add_configuration('PATH_WORKSPACE', env.working_directory) - if workspace is not None: - for script in workspace.scripts: - config.create_console_argument(ArgumentTypeEnum.Executable, '', script, [], CustomScriptService) - return - - project = self._search_project_json(env.working_directory) - if project is not None: - project = f'{project}.json' - - if project is None: - return - - config.add_json_file(project, optional=True, output=False) - def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): - config.add_json_file('cpl-workspace.json', path=env.working_directory, optional=True, output=False) - config.create_console_argument(ArgumentTypeEnum.Executable, '', 'add', ['a', 'A'], AddService, True, validators=[WorkspaceValidator]) \ .add_console_argument(ArgumentTypeEnum.Flag, '--', 'simulate', ['s', 'S']) config.create_console_argument(ArgumentTypeEnum.Executable, '', 'build', ['b', 'B'], BuildService, True, validators=[ProjectValidator]) @@ -107,23 +66,5 @@ class StartupArgumentExtension(StartupExtensionABC): config.for_each_argument(lambda a: a.add_console_argument(ArgumentTypeEnum.Flag, '--', 'help', ['h', 'H'])) config.create_console_argument(ArgumentTypeEnum.Executable, '', 'help', ['h', 'H'], HelpService) - self._read_cpl_environment(config, env) - def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): - services.add_transient(WorkspaceValidator) - services.add_transient(ProjectValidator) - - services.add_transient(AddService) - services.add_transient(BuildService) - services.add_transient(CustomScriptService) - services.add_transient(GenerateService) - services.add_transient(HelpService) - services.add_transient(InstallService) - services.add_transient(NewService) - services.add_transient(PublishService) - services.add_transient(RemoveService) - services.add_transient(RunService) - services.add_transient(StartService) - services.add_transient(UninstallService) - services.add_transient(UpdateService) - services.add_transient(VersionService) + pass diff --git a/src/cpl_cli/startup_workspace_extension.py b/src/cpl_cli/startup_workspace_extension.py new file mode 100644 index 00000000..26fe4d0d --- /dev/null +++ b/src/cpl_cli/startup_workspace_extension.py @@ -0,0 +1,55 @@ +import os +from typing import Optional + +from cpl_cli.command.custom_script_service import CustomScriptService +from cpl_cli.configuration.workspace_settings import WorkspaceSettings +from cpl_core.application.startup_extension_abc import StartupExtensionABC +from cpl_core.configuration.argument_type_enum import ArgumentTypeEnum +from cpl_core.configuration.configuration_abc import ConfigurationABC +from cpl_core.dependency_injection.service_collection_abc import ServiceCollectionABC +from cpl_core.environment.application_environment_abc import ApplicationEnvironmentABC +from cpl_core.utils.string import String + + +class StartupWorkspaceExtension(StartupExtensionABC): + + def __init__(self): + pass + + @staticmethod + def _search_project_json(working_directory: str) -> Optional[str]: + project_name = None + name = os.path.basename(working_directory) + for r, d, f in os.walk(working_directory): + for file in f: + if file.endswith('.json'): + f_name = file.split('.json')[0] + if f_name == name or String.convert_to_camel_case(f_name).lower() == String.convert_to_camel_case(name).lower(): + project_name = f_name + break + + return project_name + + def _read_cpl_environment(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): + workspace: Optional[WorkspaceSettings] = config.get_configuration(WorkspaceSettings) + config.add_configuration('PATH_WORKSPACE', env.working_directory) + if workspace is not None: + for script in workspace.scripts: + config.create_console_argument(ArgumentTypeEnum.Executable, '', script, [], CustomScriptService) + return + + project = self._search_project_json(env.working_directory) + if project is not None: + project = f'{project}.json' + + if project is None: + return + + config.add_json_file(project, optional=True, output=False) + + def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): + config.add_json_file('cpl-workspace.json', path=env.working_directory, optional=True, output=False) + self._read_cpl_environment(config, env) + + def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + pass diff --git a/src/cpl_core/utils/pip.py b/src/cpl_core/utils/pip.py index bea3da12..c0ebf062 100644 --- a/src/cpl_core/utils/pip.py +++ b/src/cpl_core/utils/pip.py @@ -4,19 +4,22 @@ import sys from contextlib import suppress from typing import Optional +from cpl_core.console import Console + class Pip: r"""Executes pip commands""" _executable = sys.executable _env = os.environ - _is_venv = False """Getter""" + @classmethod def get_executable(cls) -> str: return cls._executable """Setter""" + @classmethod def set_executable(cls, executable: str): r"""Sets the executable @@ -26,25 +29,28 @@ class Pip: executable: :class:`str` The python command """ - if executable is not None and executable != sys.executable: - cls._executable = executable - if os.path.islink(cls._executable): - cls._is_venv = True - path = os.path.dirname(os.path.dirname(cls._executable)) - cls._env = os.environ - if sys.platform == 'win32': - cls._env['PATH'] = f'{path}\\bin' + os.pathsep + os.environ.get('PATH', '') - else: - cls._env['PATH'] = f'{path}/bin' + os.pathsep + os.environ.get('PATH', '') - cls._env['VIRTUAL_ENV'] = path + if executable is None or executable == sys.executable: + return + + cls._executable = executable + if not os.path.islink(cls._executable): + return + + path = os.path.dirname(os.path.dirname(cls._executable)) + cls._env = os.environ + if sys.platform == 'win32': + cls._env['PATH'] = f'{path}\\bin' + os.pathsep + os.environ.get('PATH', '') + else: + cls._env['PATH'] = f'{path}/bin' + os.pathsep + os.environ.get('PATH', '') + cls._env['VIRTUAL_ENV'] = path @classmethod def reset_executable(cls): r"""Resets the executable to system standard""" cls._executable = sys.executable - cls._is_venv = False """Public utils functions""" + @classmethod def get_package(cls, package: str) -> Optional[str]: r"""Gets given package py local pip list @@ -60,8 +66,6 @@ class Pip: result = None with suppress(Exception): args = [cls._executable, "-m", "pip", "show", package] - if cls._is_venv: - args = ["pip", "show", package] result = subprocess.check_output( args, @@ -92,8 +96,6 @@ class Pip: Bytes string of the command result """ args = [cls._executable, "-m", "pip", "list", "--outdated"] - if cls._is_venv: - args = ["pip", "list", "--outdated"] return subprocess.check_output(args, env=cls._env) @@ -115,8 +117,6 @@ class Pip: Stderr of subprocess.run """ pip_args = [cls._executable, "-m", "pip", "install"] - if cls._is_venv: - pip_args = ["pip", "install"] for arg in args: pip_args.append(arg) @@ -126,6 +126,7 @@ class Pip: pip_args.append(source) pip_args.append(package) + print(pip_args) subprocess.run(pip_args, stdout=stdout, stderr=stderr, env=cls._env) @classmethod @@ -142,8 +143,6 @@ class Pip: Stderr of subprocess.run """ args = [cls._executable, "-m", "pip", "uninstall", "--yes", package] - if cls._is_venv: - args = ["pip", "uninstall", "--yes", package] subprocess.run( args, diff --git a/src/tests/custom/general/src/general/general.json b/src/tests/custom/general/src/general/general.json index 448345cd..fc68548e 100644 --- a/src/tests/custom/general/src/general/general.json +++ b/src/tests/custom/general/src/general/general.json @@ -20,7 +20,7 @@ ], "PythonVersion": ">=3.10", "PythonPath": { - "linux": "../../../../../../cpl-env/bin/python3.9", + "linux": "../../venv/bin/python", "win32": "" }, "Classifiers": []