diff --git a/src/cli/cpl.project.json b/src/cli/cpl.project.json index d7738c6a..f097547a 100644 --- a/src/cli/cpl.project.json +++ b/src/cli/cpl.project.json @@ -11,7 +11,7 @@ "click": "~8.3.0" }, "devDependencies": { - "black": "~25.9.0" + "black": "~25.9" }, "references": [], "main": "cpl/cli/main.py", diff --git a/src/cli/cpl/cli/command/package/add.py b/src/cli/cpl/cli/command/package/add.py index e49d8a82..b06bd675 100644 --- a/src/cli/cpl/cli/command/package/add.py +++ b/src/cli/cpl/cli/command/package/add.py @@ -1,26 +1,16 @@ -from pathlib import Path - import click from cpl.cli.cli import cli -from cpl.cli.model.project import Project -from cpl.cli.model.workspace import Workspace -from cpl.core.configuration import Configuration +from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.core.console import Console @cli.command("add", aliases=["a"]) @click.argument("reference", type=click.STRING, required=True) @click.argument("target", type=click.STRING, required=True) -@click.option("--verbose", is_flag=True, help="Enable verbose output") -def add(reference: str, target: str, verbose: bool): - workspace = None - - if Configuration.get("workspace_path") is not None: - workspace = Workspace.from_file(Configuration.get("workspace_path")) - - reference_project = _get_project_by_name_or_path(reference, workspace) - target_project = _get_project_by_name_or_path(target, workspace) +def add(reference: str, target: str): + reference_project = get_project_by_name_or_path(reference) + target_project = get_project_by_name_or_path(target) if reference_project.name == target_project.name: raise ValueError("Cannot add a project as a dependency to itself!") @@ -30,22 +20,3 @@ def add(reference: str, target: str, verbose: bool): target_project.references.append(reference_project.path) target_project.save() Console.write_line(f"Added '{reference_project.name}' to '{target_project.name}' project") - - -def _get_project_by_name_or_path(project: str, workspace=None) -> Project: - path = Path(project) - if path.exists() and path.is_dir(): - return Project.from_file(path / "cpl.project.json") - - if path.exists() and path.is_file(): - if not path.name.endswith("cpl.project.json"): - raise ValueError(f"File '{path}' is not a valid cpl.project.json file.") - - return Project.from_file(path) - - if workspace is not None: - for p in workspace.actual_projects: - if p.name == project: - return Project.from_file(Path(p.path)) - - raise ValueError(f"Project '{project}' not found.") diff --git a/src/cli/cpl/cli/command/package/install.py b/src/cli/cpl/cli/command/package/install.py index d793eb47..f20e76fe 100644 --- a/src/cli/cpl/cli/command/package/install.py +++ b/src/cli/cpl/cli/command/package/install.py @@ -1,38 +1,31 @@ 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.cli.utils.structure import get_project_by_name_or_path from cpl.core.console import Console @cli.command("install", aliases=["i"]) @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.argument("project", type=click.STRING, required=False) @click.option("--dev", is_flag=True, help="Include dev dependencies") @click.option("--verbose", is_flag=True, help="Enable verbose output") -def install(package: str, project_path: str, project_name: str, dev: bool, verbose: bool): - project = resolve_project(Path(project_path), project_name) +def install(package: str, project: str, dev: bool, verbose: bool): + project = get_project_by_name_or_path(project or "./") + if package is not None: - Console.write_line(dev) Console.write_line(f"Installing {package} to '{project.name}':") try: - Pip.command("install", package, verbose=verbose, path=project_path) + 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}") + return package_name = Pip.get_package_without_version(package) - installed_version = Pip.get_package_version(package_name, path=project_path) + 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 @@ -58,6 +51,6 @@ def install(package: str, project_path: str, project_name: str, dev: bool, verbo dep = Pip.normalize_dep(name, version) Console.write_line(f" -> {dep}") try: - Pip.command("install", dep, verbose=verbose, path=project_path) + 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}") diff --git a/src/cli/cpl/cli/command/package/remove.py b/src/cli/cpl/cli/command/package/remove.py index c9417859..be337efb 100644 --- a/src/cli/cpl/cli/command/package/remove.py +++ b/src/cli/cpl/cli/command/package/remove.py @@ -1,26 +1,16 @@ -from pathlib import Path - import click from cpl.cli.cli import cli -from cpl.cli.model.project import Project -from cpl.cli.model.workspace import Workspace -from cpl.core.configuration import Configuration +from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.core.console import Console @cli.command("remove", aliases=["rm"]) @click.argument("reference", type=click.STRING, required=True) @click.argument("target", type=click.STRING, required=True) -@click.option("--verbose", is_flag=True, help="Enable verbose output") -def remove(reference: str, target: str, verbose: bool): - workspace = None - - if Configuration.get("workspace_path") is not None: - workspace = Workspace.from_file(Configuration.get("workspace_path")) - - reference_project = _get_project_by_name_or_path(reference, workspace) - target_project = _get_project_by_name_or_path(target, workspace) +def remove(reference: str, target: str): + reference_project = get_project_by_name_or_path(reference) + target_project = get_project_by_name_or_path(target) if reference_project.name == target_project.name: raise ValueError("Cannot add a project as a dependency to itself!") @@ -31,22 +21,3 @@ def remove(reference: str, target: str, verbose: bool): target_project.references.remove(reference_project.path) target_project.save() Console.write_line(f"Removed '{reference_project.name}' from '{target_project.name}' project") - - -def _get_project_by_name_or_path(project: str, workspace=None) -> Project: - path = Path(project) - if path.exists() and path.is_dir(): - return Project.from_file(path / "cpl.project.json") - - if path.exists() and path.is_file(): - if not path.name.endswith("cpl.project.json"): - raise ValueError(f"File '{path}' is not a valid cpl.project.json file.") - - return Project.from_file(path) - - if workspace is not None: - for p in workspace.actual_projects: - if p.name == project: - return Project.from_file(Path(p.path)) - - raise ValueError(f"Project '{project}' not found.") diff --git a/src/cli/cpl/cli/command/package/uninstall.py b/src/cli/cpl/cli/command/package/uninstall.py index 04aee0b4..ba70495a 100644 --- a/src/cli/cpl/cli/command/package/uninstall.py +++ b/src/cli/cpl/cli/command/package/uninstall.py @@ -1,35 +1,28 @@ 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.cli.utils.structure import get_project_by_name_or_path from cpl.core.console import Console @cli.command("uninstall", aliases=["ui"]) @click.argument("package", 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.argument("project", type=click.STRING, required=False) @click.option("--dev", is_flag=True, help="Include dev dependencies") @click.option("--verbose", is_flag=True, help="Enable verbose output") -def uninstall(package: str, project_path: str, project_name: str, dev: bool, verbose: bool): +def uninstall(package: str, project: str, dev: bool, verbose: bool): if package is None: package = Console.read("Package name to uninstall: ").strip() - project = resolve_project(Path(project_path), project_name) + project = get_project_by_name_or_path(project or "./") + deps = project.dependencies if not dev else project.dev_dependencies try: - Pip.command("uninstall -y", package, verbose=verbose, path=project_path) + Pip.command("uninstall -y", package, verbose=verbose, path=project.path) except subprocess.CalledProcessError as e: Console.error(f"Failed to uninstall {package}: exit code {e.returncode}") return diff --git a/src/cli/cpl/cli/command/package/update.py b/src/cli/cpl/cli/command/package/update.py index a10206a0..ea116b84 100644 --- a/src/cli/cpl/cli/command/package/update.py +++ b/src/cli/cpl/cli/command/package/update.py @@ -1,28 +1,21 @@ 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.cli.utils.structure import get_project_by_name_or_path 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.argument("project", type=click.STRING, required=False) @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) +def update(package: str, project: str, dev: bool, verbose: bool): + project = get_project_by_name_or_path(project or "./") + deps: dict = project.dependencies if dev: deps = project.dev_dependencies @@ -40,13 +33,13 @@ def update(package: str, project_path: str, project_name: str, dev: bool, verbos "install --upgrade", f"{Pip.normalize_dep(package, old_spec)}", verbose=verbose, - path=project_path, + 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) + 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 @@ -66,12 +59,12 @@ def update(package: str, project_path: str, project_name: str, dev: bool, verbos dep = Pip.normalize_dep(name, version) Console.write_line(f" -> {dep}") try: - Pip.command("install --upgrade", dep, verbose=verbose, path=project_path) + 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) + 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 diff --git a/src/cli/cpl/cli/utils/pip.py b/src/cli/cpl/cli/utils/pip.py index 5c00eb92..70fbf343 100644 --- a/src/cli/cpl/cli/utils/pip.py +++ b/src/cli/cpl/cli/utils/pip.py @@ -1,3 +1,4 @@ +import os.path import re import subprocess from pathlib import Path @@ -97,19 +98,22 @@ class Pip: @staticmethod def command(command: str, *args, verbose: bool = False, path: str = None): + if path is not None and Path(path).is_file(): + path = os.path.dirname(path) + venv_path = ensure_venv(Path(path or "./")) pip = get_venv_pip(venv_path) if verbose: Console.write_line() + Console.write_line(f"Running: {pip} {command} {''.join(args)}") subprocess.run( - f"{pip} {command} {''.join(args)}", - shell=True, + [*pip.split(), command, *args], check=True, stdin=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.DEVNULL if not verbose else None, ) @staticmethod @@ -119,8 +123,7 @@ class Pip: try: result = subprocess.run( - f"{pip} show {package}", - shell=True, + [*pip.split(), "show", package], check=True, capture_output=True, text=True, @@ -140,8 +143,7 @@ class Pip: pip = get_venv_pip(venv_path) try: result = subprocess.run( - f"{pip} list --format=freeze", - shell=True, + [*pip.split(), "list", "--format=freeze"], check=True, capture_output=True, text=True, @@ -163,8 +165,7 @@ class Pip: try: result = subprocess.run( - f"{pip} --version", - shell=True, + [*pip.split(), "--version"], check=True, capture_output=True, text=True, diff --git a/src/cli/cpl/cli/utils/structure.py b/src/cli/cpl/cli/utils/structure.py index c08e71f1..87f2e4cb 100644 --- a/src/cli/cpl/cli/utils/structure.py +++ b/src/cli/cpl/cli/utils/structure.py @@ -1,9 +1,8 @@ -import json -import os from pathlib import Path from cpl.cli.model.project import Project from cpl.cli.model.workspace import Workspace +from cpl.core.configuration import Configuration from cpl.core.console import Console @@ -29,3 +28,33 @@ def resolve_project(path: Path, name: str | None) -> Project: Console.error(f"Could not find project file '{path}'") exit(1) + +def get_project_by_name_or_path(project: str) -> Project: + if project is None: + raise ValueError("Project name or path must be provided.") + + workspace = None + if Configuration.get("workspace_path") is not None: + workspace = Workspace.from_file(Configuration.get("workspace_path")) + + path = Path(project) + if path.exists() and path.is_dir() and (path / "cpl.project.json").exists(): + return Project.from_file(path / "cpl.project.json") + + if path.exists() and path.is_file(): + if not path.name.endswith("cpl.project.json"): + raise ValueError(f"File '{path}' is not a valid cpl.project.json file.") + + return Project.from_file(path) + + if workspace is not None: + for p in workspace.actual_projects: + if p.name == project: + return Project.from_file(Path(p.path)) + + if workspace.default_project is not None: + for p in workspace.actual_projects: + if p.name == workspace.default_project: + return Project.from_file(Path(p.path)) + + raise ValueError(f"Project '{project}' not found.") \ No newline at end of file