From 45dcb400daa509b0d6e971c7d85a3df39ed513bf Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 11 Oct 2025 11:05:04 +0200 Subject: [PATCH] Added remove & add commands --- cpl.workspace.json | 4 +- src/cli/cpl.project.json | 6 +-- src/cli/cpl/cli/cli.py | 22 +++++++++ src/cli/cpl/cli/command/package/add.py | 51 +++++++++++++++++++ src/cli/cpl/cli/command/package/install.py | 5 +- src/cli/cpl/cli/command/package/remove.py | 52 ++++++++++++++++++++ src/cli/cpl/cli/command/package/uninstall.py | 5 +- src/cli/cpl/cli/main.py | 14 +++--- src/cli/cpl/cli/model/cpl_structure_model.py | 2 +- src/cli/cpl/cli/model/workspace.py | 5 ++ src/cli/cpl/cli/utils/structure.py | 1 + src/cli/run | 12 +++++ 12 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 src/cli/cpl/cli/command/package/add.py create mode 100644 src/cli/cpl/cli/command/package/remove.py create mode 100755 src/cli/run diff --git a/cpl.workspace.json b/cpl.workspace.json index 885653e9..b0e9f4da 100644 --- a/cpl.workspace.json +++ b/cpl.workspace.json @@ -1,8 +1,8 @@ { "name": "cpl", "projects": [ - "src/cpl-core/cpl.project.json", - "src/cpl-cli/cpl.project.json" + "src/cli/cpl.project.json", + "src/core/cpl.project.json" ], "defaultProject": "cpl-cli", "scripts": { diff --git a/src/cli/cpl.project.json b/src/cli/cpl.project.json index edaa4ca7..d7738c6a 100644 --- a/src/cli/cpl.project.json +++ b/src/cli/cpl.project.json @@ -11,11 +11,9 @@ "click": "~8.3.0" }, "devDependencies": { - "black": "~25.9" + "black": "~25.9.0" }, - "references": [ - "../cpl/cpl.project.json" - ], + "references": [], "main": "cpl/cli/main.py", "directory": "cpl/cli" } \ No newline at end of file diff --git a/src/cli/cpl/cli/cli.py b/src/cli/cpl/cli/cli.py index 66d633fc..47ff710b 100644 --- a/src/cli/cpl/cli/cli.py +++ b/src/cli/cpl/cli/cli.py @@ -1,5 +1,9 @@ +import traceback + import click +from cpl.core.console import Console + class AliasedGroup(click.Group): def command(self, *args, **kwargs): @@ -7,6 +11,7 @@ class AliasedGroup(click.Group): def decorator(f): cmd = super(AliasedGroup, self).command(*args, **kwargs)(f) + cmd.callback = self._handle_errors(cmd.callback) for alias in aliases: self.add_command(cmd, alias) @@ -28,6 +33,23 @@ class AliasedGroup(click.Group): with formatter.section("Commands"): formatter.write_dl(commands) + @staticmethod + def _handle_errors(f): + def wrapper(*args, **kwargs): + try: + res = f(*args, **kwargs) + Console.write_line() + return res + except Exception as e: + tb = None + if "verbose" in kwargs and kwargs["verbose"]: + tb = traceback.format_exc() + Console.error(str(e), tb) + Console.write_line() + exit(-1) + + return wrapper + @click.group(cls=AliasedGroup) def cli(): ... diff --git a/src/cli/cpl/cli/command/package/add.py b/src/cli/cpl/cli/command/package/add.py new file mode 100644 index 00000000..e49d8a82 --- /dev/null +++ b/src/cli/cpl/cli/command/package/add.py @@ -0,0 +1,51 @@ +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.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) + + if reference_project.name == target_project.name: + raise ValueError("Cannot add a project as a dependency to itself!") + + if reference_project.path in target_project.references: + raise ValueError(f"Project '{reference_project.name}' is already a reference of '{target_project.name}'") + 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 ae5108dd..d793eb47 100644 --- a/src/cli/cpl/cli/command/package/install.py +++ b/src/cli/cpl/cli/command/package/install.py @@ -24,6 +24,7 @@ from cpl.core.console import Console 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(dev) Console.write_line(f"Installing {package} to '{project.name}':") try: Pip.command("install", package, verbose=verbose, path=project_path) @@ -36,7 +37,9 @@ def install(package: str, project_path: str, project_name: str, dev: bool, verbo 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)) + deps = project.dependencies if not dev else project.dev_dependencies + deps[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 diff --git a/src/cli/cpl/cli/command/package/remove.py b/src/cli/cpl/cli/command/package/remove.py new file mode 100644 index 00000000..c9417859 --- /dev/null +++ b/src/cli/cpl/cli/command/package/remove.py @@ -0,0 +1,52 @@ +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.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) + + if reference_project.name == target_project.name: + raise ValueError("Cannot add a project as a dependency to itself!") + + if reference_project.path not in target_project.references: + raise ValueError(f"Project '{reference_project.name}' isn't a reference of '{target_project.name}'") + + 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 68e7a17e..04aee0b4 100644 --- a/src/cli/cpl/cli/command/package/uninstall.py +++ b/src/cli/cpl/cli/command/package/uninstall.py @@ -29,7 +29,7 @@ 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("install", 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 @@ -38,3 +38,6 @@ def uninstall(package: str, project_path: str, project_name: str, dev: bool, ver del deps[package] project.save() Console.write_line(f"Removed {package} from project dependencies.") + return + + Console.write_line(f"Package {package} was not found in project dependencies.") diff --git a/src/cli/cpl/cli/main.py b/src/cli/cpl/cli/main.py index 69a1e0c8..b18b1025 100644 --- a/src/cli/cpl/cli/main.py +++ b/src/cli/cpl/cli/main.py @@ -2,10 +2,12 @@ import os from pathlib import Path 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.command.package.remove import remove +from cpl.cli.command.package.add import add +from cpl.cli.command.structure.init import init +from cpl.cli.command.package.install import install +from cpl.cli.command.package.uninstall import uninstall +from cpl.cli.command.package.update import update from cpl.cli.command.version import version from cpl.cli.model.workspace import Workspace from cpl.cli.utils.custom_command import script_command @@ -54,8 +56,8 @@ def configure(): cli.add_command(install) cli.add_command(uninstall) cli.add_command(update) - # cli.add_command(add) - # cli.add_command(remove) + cli.add_command(add) + cli.add_command(remove) # run # cli.add_command(run) diff --git a/src/cli/cpl/cli/model/cpl_structure_model.py b/src/cli/cpl/cli/model/cpl_structure_model.py index c9aa02f4..867f30b6 100644 --- a/src/cli/cpl/cli/model/cpl_structure_model.py +++ b/src/cli/cpl/cli/model/cpl_structure_model.py @@ -32,7 +32,7 @@ class CPLStructureModel: kwargs: Dict[str, Any] = {} for name, param in list(sig.parameters.items())[1:]: if name == "path": - kwargs[name] = path + kwargs[name] = str(path) continue if name in data: diff --git a/src/cli/cpl/cli/model/workspace.py b/src/cli/cpl/cli/model/workspace.py index 842ec47c..77d1880f 100644 --- a/src/cli/cpl/cli/model/workspace.py +++ b/src/cli/cpl/cli/model/workspace.py @@ -1,6 +1,7 @@ from typing import Optional, List, Dict from cpl.cli.model.cpl_structure_model import CPLStructureModel +from cpl.cli.model.project import Project class Workspace(CPLStructureModel): @@ -45,6 +46,10 @@ class Workspace(CPLStructureModel): def projects(self, value: List[str]): self._projects = self._require_list_of_str(value, "projects") + @property + def actual_projects(self) -> List[Project]: + return [Project.from_file(p) for p in self._projects] + @property def default_project(self) -> Optional[str]: return self._default_project diff --git a/src/cli/cpl/cli/utils/structure.py b/src/cli/cpl/cli/utils/structure.py index 17266631..c08e71f1 100644 --- a/src/cli/cpl/cli/utils/structure.py +++ b/src/cli/cpl/cli/utils/structure.py @@ -1,4 +1,5 @@ import json +import os from pathlib import Path from cpl.cli.model.project import Project diff --git a/src/cli/run b/src/cli/run new file mode 100755 index 00000000..580c69e9 --- /dev/null +++ b/src/cli/run @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +cd ../ +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +export PYTHONPATH="$ROOT_DIR/core:$ROOT_DIR/cli:$PYTHONPATH" + +old_dir="$(pwd)" +cd ../ +python -m cpl.cli.main "$@" +cd "$old_dir" \ No newline at end of file