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"
},
"devDependencies": {
"black": "25.1.0"
"black": "~25.9"
},
"references": [
"../cpl/cpl.project.json"

View File

@@ -10,6 +10,7 @@ from cpl.core.console import Console
@cli.command("install", aliases=["i"])
@click.argument("package", type=click.STRING, required=False)
@click.option(
"--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("--dev", is_flag=True, help="Include dev dependencies")
@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)
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
if dev:
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)
Console.write_line(f" -> {dep}")
try:
Pip.command(project_path, "install", dep, verbose=verbose)
Pip.command("install", dep, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e:
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
try:
Pip.command(project_path, "install", package, verbose=verbose)
Pip.command("install", package, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to uninstall {package}: exit code {e.returncode}")
return
if package in deps:
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.install import install
from cpl.cli.command.uninstall import uninstall
from cpl.cli.command.update import update
from cpl.cli.model.workspace import Workspace
from cpl.cli.utils.custom_command import script_command
from cpl.core.configuration import Configuration
@@ -44,6 +45,7 @@ def configure():
cli.add_command(init)
cli.add_command(install)
cli.add_command(uninstall)
cli.add_command(update)
def main():

View File

@@ -1,3 +1,4 @@
import re
import subprocess
from pathlib import Path
@@ -6,6 +7,7 @@ from cpl.core.console import Console
class Pip:
_ANY_PREFIX_RE = re.compile(r"(===|~=|==|!=|>=|<=|>>|<<|>|<|!|~|=|\^)")
@staticmethod
def normalize_dep(name: str, raw: str) -> str:
@@ -27,14 +29,75 @@ class Pip:
for prefix, pip_op in table.items():
if raw.startswith(prefix):
op = pip_op
raw = raw[len(prefix) :]
raw = raw[len(prefix):]
break
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
def command(project_path: str, command: str, *args,verbose:bool=False):
venv_path = ensure_venv(Path(project_path))
def command(command: str, *args,verbose:bool=False, path: str=None):
venv_path = ensure_venv(Path(path or './'))
pip = get_venv_pip(venv_path)
if verbose:
@@ -47,4 +110,26 @@ class Pip:
stdin=subprocess.DEVNULL if not verbose else None,
stdout=subprocess.DEVNULL 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