Fixed install & added update
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
81
src/cpl-cli/cpl/cli/command/update.py
Normal file
81
src/cpl-cli/cpl/cli/command/update.py
Normal 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()
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1 +1,2 @@
|
|||||||
cpl-core
|
cpl-core
|
||||||
|
click==8.3.0
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user