Added build command
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s

This commit is contained in:
2025-10-11 15:50:38 +02:00
parent 849dd7a733
commit faedf328cb
18 changed files with 302 additions and 16 deletions

View File

@@ -13,7 +13,19 @@
"devDependencies": {
"black": "~25.9"
},
"references": [],
"references": [
"../core/cpl.project.json"
],
"main": "cpl/cli/main.py",
"directory": "cpl/cli"
"directory": "cpl/cli",
"build": {
"include": [
"_templates/"
],
"exclude": [
"**/__pycache__",
"**/logs",
"**/tests"
]
}
}

View File

@@ -1,2 +1 @@
__version__ = "1.0.0"
__version__ = "1.0.0"

View File

@@ -1,3 +1,5 @@
from pathlib import Path
import click
from cpl.cli.cli import cli
@@ -8,7 +10,8 @@ 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)
def add(reference: str, target: str):
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def add(reference: str, target: str, verbose: bool):
reference_project = get_project_by_name_or_path(reference)
target_project = get_project_by_name_or_path(target)
@@ -17,6 +20,8 @@ def add(reference: str, target: str):
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)
rel_path = Path(reference_project.path).relative_to(Path(target_project.path).parent, walk_up=True)
target_project.references.append(str(rel_path))
target_project.save()
Console.write_line(f"Added '{reference_project.name}' to '{target_project.name}' project")

View File

@@ -12,7 +12,7 @@ from cpl.core.console import Console
@click.argument("package", type=click.STRING, required=False)
@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")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def install(package: str, project: str, dev: bool, verbose: bool):
project = get_project_by_name_or_path(project or "./")

View File

@@ -1,3 +1,5 @@
from pathlib import Path
import click
from cpl.cli.cli import cli
@@ -15,9 +17,10 @@ def remove(reference: str, target: str):
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:
rel_path = str(Path(reference_project.path).relative_to(Path(target_project.path).parent, walk_up=True))
if rel_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.references.remove(rel_path)
target_project.save()
Console.write_line(f"Removed '{reference_project.name}' from '{target_project.name}' project")

View File

@@ -12,7 +12,7 @@ from cpl.core.console import Console
@click.argument("package", required=False)
@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")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def uninstall(package: str, project: str, dev: bool, verbose: bool):
if package is None:
package = Console.read("Package name to uninstall: ").strip()

View File

@@ -12,7 +12,7 @@ from cpl.core.console import Console
@click.argument("package", type=click.STRING, required=False)
@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")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def update(package: str, project: str, dev: bool, verbose: bool):
project = get_project_by_name_or_path(project or "./")

View File

@@ -0,0 +1,31 @@
import os.path
from pathlib import Path
import click
from cpl.cli.cli import cli
from cpl.cli.utils.structure import get_project_by_name_or_path
from cpl.core.configuration import Configuration
from cpl.core.console import Console
@cli.command("build", aliases=["b"])
@click.argument("project", type=click.STRING, required=False)
@click.option("--dist", "-d", type=str)
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def build(project: str, dist: str | None, verbose: bool):
project = get_project_by_name_or_path(project or "./")
dist_path = dist or Path(project.path).parent / "dist"
if dist is None and Configuration.get("workspace") is not None:
dist_path = Path(Configuration.get("workspace").path).parent / "dist"
dist_path = Path(dist_path).resolve().absolute()
if verbose:
Console.write_line(f"Creating dist folder at {dist_path}...")
os.makedirs(dist_path, exist_ok=True)
project.do_build(dist_path, verbose)
Console.write_line("\nDone!")

View File

@@ -1,14 +1,14 @@
import os
from pathlib import Path
from cpl.cli.cli import cli
from cpl.cli.command.execute.run import run
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.remove import remove
from cpl.cli.command.package.uninstall import uninstall
from cpl.cli.command.package.update import update
from cpl.cli.command.project.build import build
from cpl.cli.command.project.run import run
from cpl.cli.command.structure.init import init
from cpl.cli.command.version import version
from cpl.cli.model.workspace import Workspace
from cpl.cli.utils.custom_command import script_command
@@ -61,6 +61,7 @@ def configure():
cli.add_command(remove)
# run
cli.add_command(build)
cli.add_command(run)
# cli.add_command(start)

View File

@@ -0,0 +1,18 @@
from cpl.cli.model.cpl_sub_structure_model import CPLSubStructureModel
class Build(CPLSubStructureModel):
def __init__(self, include: list[str], exclude: list[str]):
CPLSubStructureModel.__init__(self)
self._include = include
self._exclude = exclude
@property
def include(self) -> list[str]:
return self._include
@property
def exclude(self) -> list[str]:
return self._exclude

View File

@@ -1,8 +1,11 @@
import inspect
import json
from inspect import isclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Type, TypeVar
from cpl.cli.model.cpl_sub_structure_model import CPLSubStructureModel
T = TypeVar("T", bound="CPLStructureModel")
@@ -35,6 +38,10 @@ class CPLStructureModel:
kwargs[name] = str(path)
continue
if isclass(param.annotation) and issubclass(param.annotation, CPLSubStructureModel):
kwargs[name] = param.annotation.from_json(data[name])
continue
if name in data:
kwargs[name] = data[name]
continue
@@ -63,6 +70,10 @@ class CPLStructureModel:
if not key.startswith("_") or key == "_path":
continue
out_key = _self_or_cls_snake_to_camel(key[1:])
if isinstance(value, CPLSubStructureModel):
value = value.to_json()
result[out_key] = value
return result

View File

@@ -0,0 +1,104 @@
import inspect
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Type, TypeVar
T = TypeVar("T", bound="CPLSubStructureModel")
class CPLSubStructureModel:
def __init__(self, path: Optional[str] = None):
self._path = path
@classmethod
def from_json(cls: Type[T], data: Dict[str, Any]) -> T:
sig = inspect.signature(cls.__init__)
kwargs: Dict[str, Any] = {}
for name, param in list(sig.parameters.items())[1:]:
if name in data:
kwargs[name] = data[name]
continue
priv = "_" + name
if priv in data:
kwargs[name] = data[priv]
continue
camel = _self_or_cls_snake_to_camel(name)
if camel in data:
kwargs[name] = data[camel]
continue
if param.default is not inspect._empty:
kwargs[name] = param.default
continue
raise KeyError(f"Missing required field '{name}' for {cls.__name__}.")
return cls(**kwargs)
def to_json(self) -> Dict[str, Any]:
result: Dict[str, Any] = {}
for key, value in self.__dict__.items():
if not key.startswith("_") or key == "_path":
continue
out_key = _self_or_cls_snake_to_camel(key[1:])
result[out_key] = value
return result
@staticmethod
def _require_str(value, field: str, allow_empty: bool = True) -> str:
if not isinstance(value, str):
raise TypeError(f"{field} must be of type str")
if not allow_empty and not value.strip():
raise ValueError(f"{field} must not be empty")
return value
@staticmethod
def _require_optional_non_empty_str(value, field: str) -> Optional[str]:
if value is None:
return None
if not isinstance(value, str):
raise TypeError(f"{field} must be str or None")
s = value.strip()
if not s:
raise ValueError(f"{field} must not be empty when set")
return s
@staticmethod
def _require_list_of_str(value, field: str) -> List[str]:
if not isinstance(value, list):
raise TypeError(f"{field} must be a list")
out: List[str] = []
for i, v in enumerate(value):
if not isinstance(v, str):
raise TypeError(f"{field}[{i}] must be of type str")
s = v.strip()
if s:
out.append(s)
seen = set()
uniq: List[str] = []
for s in out:
if s not in seen:
seen.add(s)
uniq.append(s)
return uniq
@staticmethod
def _require_dict_str_str(value, field: str) -> Dict[str, str]:
if not isinstance(value, dict):
raise TypeError(f"{field} must be a dict")
out: Dict[str, str] = {}
for k, v in value.items():
if not isinstance(k, str) or not k.strip():
raise TypeError(f"Keys in {field} must be non-empty strings")
if not isinstance(v, str) or not v.strip():
raise TypeError(f"Values in {field} must be non-empty strings")
out[k.strip()] = v.strip()
return out
def _self_or_cls_snake_to_camel(s: str) -> str:
parts = s.split("_")
return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:])

View File

@@ -1,8 +1,14 @@
import fnmatch
import os
import re
import shutil
from pathlib import Path
from typing import Optional, List, Dict
from urllib.parse import urlparse
from cpl.cli.model.build import Build
from cpl.cli.model.cpl_structure_model import CPLStructureModel
from cpl.core.console import Console
class Project(CPLStructureModel):
@@ -44,6 +50,7 @@ class Project(CPLStructureModel):
references: List[str],
main: Optional[str],
directory: str,
build: Build,
):
CPLStructureModel.__init__(self, path)
@@ -60,6 +67,7 @@ class Project(CPLStructureModel):
self._references = references
self._main = main
self._directory = directory
self._build = build
@property
def name(self) -> str:
@@ -176,3 +184,86 @@ class Project(CPLStructureModel):
@directory.setter
def directory(self, value: str):
self._directory = self._require_str(value, "directory", allow_empty=False).strip()
@property
def build(self) -> Build:
return self._build
def _collect_files(self, rel_dir: Path) -> List[Path]:
files: List[Path] = []
exclude_patterns = [p.strip() for p in self._build.exclude or []]
for root, dirnames, filenames in os.walk(rel_dir, topdown=True):
root_path = Path(root)
rel_root = root_path.relative_to(rel_dir).as_posix()
dirnames[:] = [
d
for d in dirnames
if not any(
fnmatch.fnmatch(f"{rel_root}/{d}" if rel_root else d, pattern) or fnmatch.fnmatch(d, pattern)
for pattern in exclude_patterns
)
]
for filename in filenames:
rel_path = f"{rel_root}/{filename}" if rel_root else filename
if any(
fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(filename, pattern)
for pattern in exclude_patterns
):
continue
files.append(root_path / filename)
return files
def build_references(self, dist: Path, verbose: bool = False):
references = []
old_dir = os.getcwd()
os.chdir(Path(self.path).parent)
for ref in self.references:
os.chdir(Path(ref).parent)
references.append(Project.from_file(ref))
for p in references:
os.chdir(Path(p.path).parent)
p.do_build(dist, verbose)
os.chdir(old_dir)
def do_build(self, dist: Path, verbose: bool = False):
self.build_references(dist, verbose)
Console.write_line(f"Building project {self.name}...")
if isinstance(dist, str):
dist = Path(dist)
dist_path = dist / self.name
rel_dir = Path(self.path).parent / Path(self.directory)
if verbose:
Console.write_line(f" Collecting project '{self.name}' files...")
files = self._collect_files(rel_dir)
if len(files) == 0:
return
if verbose:
Console.write_line(f" Cleaning dist folder at {dist_path.absolute()}...")
shutil.rmtree(str(dist_path), ignore_errors=True)
for file in files:
rel_path = file.relative_to(rel_dir)
dest_file_path = dist_path / rel_path
if not dest_file_path.parent.exists():
os.makedirs(dest_file_path.parent, exist_ok=True)
shutil.copy(file, dest_file_path)
if verbose:
Console.write_line(f" Copied {len(files)} files from {rel_dir.absolute()} to {dist_path.absolute()}")
Console.write_line(" Done!")

View File

@@ -25,6 +25,10 @@ def get_project_by_name_or_path(project: str) -> Project:
if p.name == project:
return Project.from_file(Path(p.path))
if not path.is_dir() and not path.is_file():
raise ValueError(f"Unknown project {project}")
if workspace.default_project is not None:
for p in workspace.actual_projects:
if p.name == workspace.default_project:

View File

@@ -8,6 +8,5 @@ export PYTHONPATH="$ROOT_DIR/core:$ROOT_DIR/cli:$PYTHONPATH"
old_dir="$(pwd)"
cd ../
echo "$@"
python -m cpl.cli.main "$@"
cd "$old_dir"

View File

@@ -20,5 +20,13 @@
},
"references": [],
"main": null,
"directory": "cpl/core"
"directory": "cpl/core",
"build": {
"include": [],
"exclude": [
"**/__pycache__",
"**/logs",
"**/tests"
]
}
}