WIP: dev into master #184

Draft
edraft wants to merge 121 commits from dev into master
10 changed files with 199 additions and 141 deletions
Showing only changes of commit c4334f32ed - Show all commits

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:
@@ -32,9 +34,70 @@ class Pip:
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:
@@ -48,3 +111,25 @@ class Pip:
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
click==8.3.0

View File

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

View File

@@ -1,6 +1,5 @@
from .base64 import Base64
from .credential_manager import CredentialManager
from .json_processor import JSONProcessor
from .pip import Pip
from .string import String
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)