From e8a4fe6142007401d0f7c9889db111ab0a88f11f Mon Sep 17 00:00:00 2001 From: Sven Heidemann Date: Mon, 27 Jun 2022 10:52:26 +0200 Subject: [PATCH 1/2] 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": [] From 323e363b4286270082ae5f3b27efa4bea6636a41 Mon Sep 17 00:00:00 2001 From: Sven Heidemann Date: Mon, 27 Jun 2022 11:50:22 +0200 Subject: [PATCH 2/2] Improved venv support to all related commands --- src/cpl_cli/command/install_service.py | 21 ++----------- src/cpl_cli/command/new_service.py | 31 +++++++++++++++++-- src/cpl_cli/command/uninstall_service.py | 4 +-- src/cpl_cli/command/update_service.py | 4 ++- .../configuration/venv_helper_service.py | 28 +++++++++++++++++ src/cpl_cli/startup_argument_extension.py | 3 +- unittests/unittests_cli/new_test_case.py | 18 ++++++++--- 7 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/cpl_cli/command/install_service.py b/src/cpl_cli/command/install_service.py index ec27a067..43db92b9 100644 --- a/src/cpl_cli/command/install_service.py +++ b/src/cpl_cli/command/install_service.py @@ -58,22 +58,6 @@ 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 @@ -211,11 +195,12 @@ class InstallService(CommandABC): args.remove('simulate') Console.write_line('Running in simulation mode:') - self._init_venv() + VenvHelper.init_venv(self._is_virtual, self._env, self._project_settings) 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))) + if not self._is_virtual: + Pip.reset_executable() diff --git a/src/cpl_cli/command/new_service.py b/src/cpl_cli/command/new_service.py index 73f50ced..e01bbf92 100644 --- a/src/cpl_cli/command/new_service.py +++ b/src/cpl_cli/command/new_service.py @@ -6,6 +6,7 @@ from typing import Optional from packaging import version import cpl_core +from cpl_cli.configuration.venv_helper_service import VenvHelper from cpl_cli.source_creator.unittest_builder import UnittestBuilder from cpl_core.configuration.configuration_abc import ConfigurationABC @@ -36,7 +37,7 @@ class NewService(CommandABC): self._config = configuration self._env = self._config.environment - self._workspace = self._config.get_configuration(WorkspaceSettings) + self._workspace: WorkspaceSettings = self._config.get_configuration(WorkspaceSettings) self._project: ProjectSettings = ProjectSettings() self._project_dict = {} self._build: BuildSettings = BuildSettings() @@ -51,6 +52,7 @@ class NewService(CommandABC): self._use_startup: bool = False self._use_service_providing: bool = False self._use_async: bool = False + self._use_venv: bool = False @property def help_message(self) -> str: @@ -107,7 +109,7 @@ class NewService(CommandABC): ], ProjectSettingsNameEnum.python_version.value: f'>={sys.version.split(" ")[0]}', ProjectSettingsNameEnum.python_path.value: { - sys.platform: '' + sys.platform: '../../venv/bin/python' if self._use_venv else '' }, ProjectSettingsNameEnum.classifiers.value: [] } @@ -275,6 +277,22 @@ class NewService(CommandABC): except Exception as e: Console.error('Could not create project', str(e)) + def _create_venv(self): + + project = self._project.name + if self._workspace is not None: + project = self._workspace.default_project + + if self._env.working_directory.endswith(project): + project = '' + + VenvHelper.init_venv( + False, + self._env, + self._project, + explicit_path=os.path.join(self._env.working_directory, project, self._project.python_executable.replace('../', '')) + ) + def execute(self, args: list[str]): """ Entry point of command @@ -308,6 +326,9 @@ class NewService(CommandABC): if 'service-providing' in args: self._use_service_providing = True args.remove('service-providing') + if 'venv' in args: + self._use_venv = True + args.remove('venv') console = self._config.get_configuration(ProjectTypeEnum.console.value) library = self._config.get_configuration(ProjectTypeEnum.library.value) @@ -316,16 +337,22 @@ class NewService(CommandABC): self._name = console self._schematic = ProjectTypeEnum.console.value self._console(args) + if self._use_venv: + self._create_venv() elif console is None and library is not None and unittest is None: self._name = library self._schematic = ProjectTypeEnum.library.value self._library(args) + if self._use_venv: + self._create_venv() elif console is None and library is None and unittest is not None: self._name = unittest self._schematic = ProjectTypeEnum.unittest.value self._unittest(args) + if self._use_venv: + self._create_venv() else: self._help('Usage: cpl new [options]') diff --git a/src/cpl_cli/command/uninstall_service.py b/src/cpl_cli/command/uninstall_service.py index f2b88d5f..ab83dcbd 100644 --- a/src/cpl_cli/command/uninstall_service.py +++ b/src/cpl_cli/command/uninstall_service.py @@ -4,6 +4,7 @@ import subprocess import textwrap import time +from cpl_cli.configuration.venv_helper_service import VenvHelper from cpl_core.configuration.configuration_abc import ConfigurationABC from cpl_core.console.console import Console from cpl_core.console.foreground_color_enum import ForegroundColorEnum @@ -71,8 +72,7 @@ class UninstallService(CommandABC): args.remove('--simulate') Console.write_line('Running in simulation mode:') - if not self._is_virtual: - Pip.set_executable(self._project_settings.python_executable) + VenvHelper.init_venv(self._is_virtual, self._env, self._project_settings) package = args[0] is_in_dependencies = False diff --git a/src/cpl_cli/command/update_service.py b/src/cpl_cli/command/update_service.py index 3b1895f5..5ce2df95 100644 --- a/src/cpl_cli/command/update_service.py +++ b/src/cpl_cli/command/update_service.py @@ -3,6 +3,7 @@ import os import subprocess import textwrap +from cpl_cli.configuration.venv_helper_service import VenvHelper from cpl_core.configuration.configuration_abc import ConfigurationABC from cpl_core.console.console import Console from cpl_core.console.foreground_color_enum import ForegroundColorEnum @@ -168,7 +169,8 @@ class UpdateService(CommandABC): Console.write_line('Running in simulation mode:') self._is_simulation = True - Pip.set_executable(self._project_settings.python_executable) + VenvHelper.init_venv(False, self._env, self._project_settings) + self._check_project_dependencies() self._check_outdated() Pip.reset_executable() diff --git a/src/cpl_cli/configuration/venv_helper_service.py b/src/cpl_cli/configuration/venv_helper_service.py index fcc6b9b9..28a59171 100644 --- a/src/cpl_cli/configuration/venv_helper_service.py +++ b/src/cpl_cli/configuration/venv_helper_service.py @@ -2,9 +2,37 @@ import os import subprocess import sys +from cpl_cli.configuration import ProjectSettings +from cpl_core.environment import ApplicationEnvironmentABC + +from cpl_core.utils import Pip + +from cpl_core.console import Console, ForegroundColorEnum + class VenvHelper: + @staticmethod + def init_venv(is_virtual: bool, env: ApplicationEnvironmentABC, project_settings: ProjectSettings, explicit_path=None): + if is_virtual: + return + + venv_path = os.path.abspath(os.path.join(env.working_directory, project_settings.python_executable)) + + if explicit_path is not None: + venv_path = os.path.abspath(explicit_path) + + 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) + @staticmethod def create_venv(path): subprocess.run( diff --git a/src/cpl_cli/startup_argument_extension.py b/src/cpl_cli/startup_argument_extension.py index 8e795305..e3f1fef4 100644 --- a/src/cpl_cli/startup_argument_extension.py +++ b/src/cpl_cli/startup_argument_extension.py @@ -50,7 +50,8 @@ class StartupArgumentExtension(StartupExtensionABC): .add_console_argument(ArgumentTypeEnum.Flag, '--', 'application-base', ['ab', 'AB']) \ .add_console_argument(ArgumentTypeEnum.Flag, '--', 'startup', ['s', 'S']) \ .add_console_argument(ArgumentTypeEnum.Flag, '--', 'service-providing', ['sp', 'SP']) \ - .add_console_argument(ArgumentTypeEnum.Flag, '--', 'nothing', ['n', 'N']) + .add_console_argument(ArgumentTypeEnum.Flag, '--', 'nothing', ['n', 'N']) \ + .add_console_argument(ArgumentTypeEnum.Flag, '--', 'venv', ['v', 'V']) config.create_console_argument(ArgumentTypeEnum.Executable, '', 'publish', ['p', 'P'], PublishService, True, validators=[ProjectValidator]) config.create_console_argument(ArgumentTypeEnum.Executable, '', 'remove', ['r', 'R'], RemoveService, True, validators=[WorkspaceValidator]) \ .add_console_argument(ArgumentTypeEnum.Flag, '--', 'simulate', ['s', 'S']) diff --git a/unittests/unittests_cli/new_test_case.py b/unittests/unittests_cli/new_test_case.py index 4125993d..b2b24177 100644 --- a/unittests/unittests_cli/new_test_case.py +++ b/unittests/unittests_cli/new_test_case.py @@ -12,10 +12,15 @@ class NewTestCase(unittest.TestCase): def setUp(self): os.chdir(os.path.abspath(PLAYGROUND_PATH)) - def _test_project(self, project_type: str, name: str, *args): + def _test_project(self, project_type: str, name: str, *args, test_venv=False): CLICommands.new(project_type, name, *args) workspace_path = os.path.abspath(os.path.join(PLAYGROUND_PATH, name)) self.assertTrue(os.path.exists(workspace_path)) + if test_venv: + self.assertTrue(os.path.exists(os.path.join(workspace_path, 'venv'))) + self.assertTrue(os.path.exists(os.path.join(workspace_path, 'venv/bin'))) + self.assertTrue(os.path.exists(os.path.join(workspace_path, 'venv/bin/python'))) + self.assertTrue(os.path.islink(os.path.join(workspace_path, 'venv/bin/python'))) project_path = os.path.abspath(os.path.join(PLAYGROUND_PATH, name, 'src', String.convert_to_snake_case(name))) self.assertTrue(os.path.exists(project_path)) @@ -38,11 +43,16 @@ class NewTestCase(unittest.TestCase): else: self.assertFalse(os.path.isfile(os.path.join(project_path, f'test_case.py'))) - def _test_sub_project(self, project_type: str, name: str, workspace_name: str, *args): + def _test_sub_project(self, project_type: str, name: str, workspace_name: str, *args, test_venv=False): os.chdir(os.path.abspath(os.path.join(os.getcwd(), workspace_name))) CLICommands.new(project_type, name, *args) workspace_path = os.path.abspath(os.path.join(PLAYGROUND_PATH, workspace_name)) self.assertTrue(os.path.exists(workspace_path)) + if test_venv: + self.assertTrue(os.path.exists(os.path.join(workspace_path, 'venv'))) + self.assertTrue(os.path.exists(os.path.join(workspace_path, 'venv/bin'))) + self.assertTrue(os.path.exists(os.path.join(workspace_path, 'venv/bin/python'))) + self.assertTrue(os.path.islink(os.path.join(workspace_path, 'venv/bin/python'))) project_path = os.path.abspath(os.path.join(PLAYGROUND_PATH, workspace_name, 'src', String.convert_to_snake_case(name))) self.assertTrue(os.path.exists(project_path)) @@ -76,7 +86,7 @@ class NewTestCase(unittest.TestCase): os.chdir(os.path.abspath(os.path.join(os.getcwd(), '../'))) def test_console(self): - self._test_project('console', 'test-console', '--ab', '--s') + self._test_project('console', 'test-console', '--ab', '--s', '--venv', test_venv=True) def test_console_without_s(self): self._test_project('console', 'test-console-without-s', '--ab') @@ -88,7 +98,7 @@ class NewTestCase(unittest.TestCase): self._test_project('console', 'test-console-without-anything', '--n') def test_sub_console(self): - self._test_sub_project('console', 'test-sub-console', 'test-console', '--ab', '--s', '--sp') + self._test_sub_project('console', 'test-sub-console', 'test-console', '--ab', '--s', '--sp', '--venv', test_venv=True) def test_library(self): self._test_project('library', 'test-library', '--ab', '--s', '--sp')