Fixed install & added update

This commit is contained in:
2025-10-09 19:58:49 +02:00
parent 0a6a17acf6
commit c4334f32ed
10 changed files with 199 additions and 141 deletions

View File

@@ -11,7 +11,7 @@
"click": "~8.3.0" "click": "~8.3.0"
}, },
"devDependencies": { "devDependencies": {
"black": "25.1.0" "black": "~25.9"
}, },
"references": [ "references": [
"../cpl/cpl.project.json" "../cpl/cpl.project.json"

View File

@@ -10,6 +10,7 @@ from cpl.core.console import Console
@cli.command("install", aliases=["i"]) @cli.command("install", aliases=["i"])
@click.argument("package", type=click.STRING, required=False)
@click.option( @click.option(
"--path", "--path",
"project_path", "project_path",
@@ -20,8 +21,26 @@ from cpl.core.console import Console
@click.option("--name", "project_name", help="Project name (lookup via workspace)") @click.option("--name", "project_name", help="Project name (lookup via workspace)")
@click.option("--dev", is_flag=True, help="Include dev dependencies") @click.option("--dev", is_flag=True, help="Include dev dependencies")
@click.option("--verbose", is_flag=True, help="Enable verbose output") @click.option("--verbose", is_flag=True, help="Enable verbose output")
def install(project_path: str, project_name: str, dev: bool, verbose: bool): def install(package: str, project_path: str, project_name: str, dev: bool, verbose: bool):
project = resolve_project(Path(project_path), project_name) project = resolve_project(Path(project_path), project_name)
if package is not None:
Console.write_line(f"Installing {package} to '{project.name}':")
try:
Pip.command("install", package, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to install {package}: exit code {e.returncode}")
package_name = Pip.get_package_without_version(package)
installed_version = Pip.get_package_version(package_name, path=project_path)
if installed_version is None:
Console.error(f"Package '{package_name}' not found after installation.")
return
project.dependencies[package_name] = Pip.apply_prefix(installed_version, Pip.get_package_full_version(package))
project.save()
Console.write_line(f"Added {package_name}~{installed_version} to project dependencies.")
return
deps: dict = project.dependencies deps: dict = project.dependencies
if dev: if dev:
deps.update(project.dev_dependencies) deps.update(project.dev_dependencies)
@@ -36,6 +55,6 @@ def install(project_path: str, project_name: str, dev: bool, verbose: bool):
dep = Pip.normalize_dep(name, version) dep = Pip.normalize_dep(name, version)
Console.write_line(f" -> {dep}") Console.write_line(f" -> {dep}")
try: try:
Pip.command(project_path, "install", dep, verbose=verbose) Pip.command("install", dep, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
Console.error(f"Failed to install {dep}: exit code {e.returncode}") Console.error(f"Failed to install {dep}: exit code {e.returncode}")

View File

@@ -29,9 +29,10 @@ def uninstall(package: str, project_path: str, project_name: str, dev: bool, ver
deps = project.dependencies if not dev else project.dev_dependencies deps = project.dependencies if not dev else project.dev_dependencies
try: try:
Pip.command(project_path, "install", package, verbose=verbose) Pip.command("install", package, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
Console.error(f"Failed to uninstall {package}: exit code {e.returncode}") Console.error(f"Failed to uninstall {package}: exit code {e.returncode}")
return
if package in deps: if package in deps:
del deps[package] del deps[package]

View File

@@ -0,0 +1,81 @@
import subprocess
from pathlib import Path
import click
from cpl.cli.cli import cli
from cpl.cli.utils.pip import Pip
from cpl.cli.utils.structure import resolve_project
from cpl.core.console import Console
@cli.command("update", aliases=["u"])
@click.argument("package", type=click.STRING, required=False)
@click.option(
"--path",
"project_path",
type=click.Path(file_okay=False, exists=False),
default=".",
help="Path to project directory",
)
@click.option("--name", "project_name", help="Project name (lookup via workspace)")
@click.option("--dev", is_flag=True, help="Include dev dependencies")
@click.option("--verbose", is_flag=True, help="Enable verbose output")
def update(package: str, project_path: str, project_name: str, dev: bool, verbose: bool):
project = resolve_project(Path(project_path), project_name)
deps: dict = project.dependencies
if dev:
deps = project.dev_dependencies
if package is not None:
if package not in deps:
Console.error(f"Package '{package}' not installed.")
return
old_spec = deps[package]
Console.write_line(f"Updating {package} to '{project.name}':")
try:
Pip.command(
"install --upgrade",
f"{Pip.normalize_dep(package, old_spec)}",
verbose=verbose,
path=project_path,
)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to install {package}: exit code {e.returncode}")
return
installed_version = Pip.get_package_version(package, path=project_path)
if installed_version is None:
Console.error(f"Package '{package}' not found after update.")
return
deps[package] = Pip.apply_prefix(installed_version, old_spec)
project.save()
Console.write_line(f"Updated {package} to {deps[package]}")
return
if not deps:
Console.error("No dependencies to install.")
return
Console.write_line(f"Updating dependencies for '{project.name}':")
for name, version in list(deps.items()):
dep = Pip.normalize_dep(name, version)
Console.write_line(f" -> {dep}")
try:
Pip.command("install --upgrade", dep, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to update {dep}: exit code {e.returncode}")
return
installed_version = Pip.get_package_version(name, path=project_path)
if installed_version is None:
Console.error(f"Package '{name}' not found after update.")
continue
deps[name] = Pip.apply_prefix(installed_version, version)
project.save()

View File

@@ -5,6 +5,7 @@ from cpl.cli.cli import cli
from cpl.cli.command.init import init from cpl.cli.command.init import init
from cpl.cli.command.install import install from cpl.cli.command.install import install
from cpl.cli.command.uninstall import uninstall from cpl.cli.command.uninstall import uninstall
from cpl.cli.command.update import update
from cpl.cli.model.workspace import Workspace from cpl.cli.model.workspace import Workspace
from cpl.cli.utils.custom_command import script_command from cpl.cli.utils.custom_command import script_command
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
@@ -44,6 +45,7 @@ def configure():
cli.add_command(init) cli.add_command(init)
cli.add_command(install) cli.add_command(install)
cli.add_command(uninstall) cli.add_command(uninstall)
cli.add_command(update)
def main(): def main():

View File

@@ -1,3 +1,4 @@
import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -6,6 +7,7 @@ from cpl.core.console import Console
class Pip: class Pip:
_ANY_PREFIX_RE = re.compile(r"(===|~=|==|!=|>=|<=|>>|<<|>|<|!|~|=|\^)")
@staticmethod @staticmethod
def normalize_dep(name: str, raw: str) -> str: def normalize_dep(name: str, raw: str) -> str:
@@ -27,14 +29,75 @@ class Pip:
for prefix, pip_op in table.items(): for prefix, pip_op in table.items():
if raw.startswith(prefix): if raw.startswith(prefix):
op = pip_op op = pip_op
raw = raw[len(prefix) :] raw = raw[len(prefix):]
break break
return f"{name}{op}{raw}" return f"{name}{op}{raw}"
@classmethod
def apply_prefix(cls, installed: str, spec: str = None) -> str:
if spec is None or not spec.strip():
return f"~{installed}"
s = spec.strip()
if "," in s:
return s
m = cls._ANY_PREFIX_RE.search(s)
if not m:
return f"~{installed}"
op = m.group(1)
rest = s[m.end():].strip()
if "," in rest:
rest = rest.split(",", 1)[0].strip()
if " " in rest:
rest = rest.split()[0]
orig_version = rest
installed_parts = [p for p in installed.split(".") if p != ""]
if orig_version:
orig_parts = [p for p in orig_version.split(".") if p != ""]
trimmed_installed = ".".join(installed_parts[:len(orig_parts)]) or installed
else:
trimmed_installed = installed
pip_to_cpl = {
"==": "",
"!=": "!",
">=": ">",
">": ">>",
"<=": "<",
"<": "<<",
"~=": "~",
"===": "=",
"^": "~",
}
if op in pip_to_cpl:
cpl_op = pip_to_cpl[op]
else:
cpl_op = op
return f"{cpl_op}{trimmed_installed}"
@classmethod
def get_package_without_version(cls, spec: str) -> str:
for sep in ["==", ">=", "<=", ">", "<", "~=", "!="]:
if sep in spec:
return spec.split(sep, 1)[0].strip() or None
return spec.strip() or None
@classmethod
def get_package_full_version(cls, spec: str) -> str | None:
package_name = cls.get_package_without_version(spec)
return spec.replace(package_name, "").strip() or None
@staticmethod @staticmethod
def command(project_path: str, command: str, *args,verbose:bool=False): def command(command: str, *args,verbose:bool=False, path: str=None):
venv_path = ensure_venv(Path(project_path)) venv_path = ensure_venv(Path(path or './'))
pip = get_venv_pip(venv_path) pip = get_venv_pip(venv_path)
if verbose: if verbose:
@@ -48,3 +111,25 @@ class Pip:
stdout=subprocess.DEVNULL if not verbose else None, stdout=subprocess.DEVNULL if not verbose else None,
stderr=subprocess.STDOUT if not verbose else None, stderr=subprocess.STDOUT if not verbose else None,
) )
@staticmethod
def get_package_version(package: str, path: str=None) -> str | None:
venv_path = ensure_venv(Path(path or './'))
pip = get_venv_pip(venv_path)
try:
result = subprocess.run(
f"{pip} show {package}",
shell=True,
check=True,
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
)
for line in result.stdout.splitlines():
if line.startswith("Version:"):
return line.split(":", 1)[1].strip()
except subprocess.CalledProcessError:
return None
return None

View File

@@ -1 +1,2 @@
cpl-core cpl-core
click==8.3.0

View File

@@ -16,7 +16,7 @@
"croniter": "~6.0.0" "croniter": "~6.0.0"
}, },
"devDependencies": { "devDependencies": {
"black": "25.1.0" "black": "~25.9"
}, },
"references": [], "references": [],
"main": null, "main": null,

View File

@@ -1,6 +1,5 @@
from .base64 import Base64 from .base64 import Base64
from .credential_manager import CredentialManager from .credential_manager import CredentialManager
from .json_processor import JSONProcessor from .json_processor import JSONProcessor
from .pip import Pip
from .string import String from .string import String
from .get_value import get_value from .get_value import get_value

View File

@@ -1,130 +0,0 @@
import os
import subprocess
import sys
from contextlib import suppress
from typing import Optional
class Pip:
r"""Executes pip commands"""
_executable = sys.executable
_env = os.environ
"""Getter"""
@classmethod
def get_executable(cls) -> str:
return cls._executable
"""Setter"""
@classmethod
def set_executable(cls, executable: str):
r"""Sets the executable
Parameter:
executable: :class:`str`
The python command
"""
if executable is None or executable == sys.executable:
return
cls._executable = executable
if not os.path.islink(cls._executable) or not os.path.isfile(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
"""Public utils functions"""
@classmethod
def get_package(cls, package: str) -> Optional[str]:
r"""Gets given package py local pip list
Parameter:
package: :class:`str`
Returns:
The package name as string
"""
result = None
with suppress(Exception):
args = [cls._executable, "-m", "pip", "freeze", "--all"]
result = subprocess.check_output(args, stderr=subprocess.DEVNULL, env=cls._env)
if result is None:
return None
for p in str(result.decode()).split("\n"):
if p.startswith(package):
return p
return None
@classmethod
def get_outdated(cls) -> bytes:
r"""Gets table of outdated packages
Returns:
Bytes string of the command result
"""
args = [cls._executable, "-m", "pip", "list", "--outdated"]
return subprocess.check_output(args, env=cls._env)
@classmethod
def install(cls, package: str, *args, source: str = None, stdout=None, stderr=None):
r"""Installs given package
Parameter:
package: :class:`str`
The name of the package
args: :class:`list`
Arguments for the command
source: :class:`str`
Extra index URL
stdout: :class:`str`
Stdout of subprocess.run
stderr: :class:`str`
Stderr of subprocess.run
"""
pip_args = [cls._executable, "-m", "pip", "install"]
for arg in args:
pip_args.append(arg)
pip_args.append(package)
if source is not None:
pip_args.append(f"--extra-index-url")
pip_args.append(source)
subprocess.run(pip_args, stdout=stdout, stderr=stderr, env=cls._env)
@classmethod
def uninstall(cls, package: str, stdout=None, stderr=None):
r"""Uninstalls given package
Parameter:
package: :class:`str`
The name of the package
stdout: :class:`str`
Stdout of subprocess.run
stderr: :class:`str`
Stderr of subprocess.run
"""
args = [cls._executable, "-m", "pip", "uninstall", "--yes", package]
subprocess.run(args, stdout=stdout, stderr=stderr, env=cls._env)