Skip to content

Commit 25c673c

Browse files
committed
feat: XRobot 0.2.0 重大功能升级与架构优化
- 全面重写并补充各主命令脚本,支持递归依赖解析、模块唯一 id、模板参数与多源仓库管理 - README.md 文档全面重写,详细说明各命令用法、参数和数据结构 - xrobot_add_mod:支持 namespace/ModuleName[@Version] 形式添加仓库,支持自动分配/指定实例 id,自动解析并写入构造参数和模板参数 - xrobot_gen_main:支持新 YAML 结构和模板参数,自动主函数代码生成,完善实例命名 - xrobot_init_mod:支持依赖递归拉取、依赖冲突检测、sources.yaml 多源聚合,完善命令行与行为 - xrobot_create_mod:标准化生成 V2 MANIFEST,支持 constructor/template/depends 结构参数,CLI 参数增强 - xrobot_mod_parser:统一解析模块信息,完善 manifest 兼容性与格式化输出 - 全部 Python 工具脚本增加 usage 说明和 CLI 参数帮助,支持独立调用和作为库复用 - 版本号升级至 0.2.0 BREAKING CHANGE: 配置文件格式升级,建议手动迁移 modules.yaml 和 xrobot.yaml 内容以兼容新版数据结构。
1 parent 1f3d971 commit 25c673c

File tree

9 files changed

+1061
-771
lines changed

9 files changed

+1061
-771
lines changed

README.md

Lines changed: 287 additions & 310 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "xrobot"
7-
version = "0.1.6"
7+
version = "0.2.0"
88
description = "A lightweight application framework for robot module management, lifecycle control, and hardware abstraction."
99
requires-python = ">=3.8"
1010
authors = [

src/xrobot/AddModule.py

Lines changed: 83 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,118 @@
11
#!/usr/bin/env python3
22
"""
3-
xrobot_add_mod.py - 添加模块仓库或追加模块实例配置
3+
xrobot_add_mod.py - Add a module repository to modules.yaml or append a module instance config.
44
5-
Usage 示例:
6-
# 添加模块仓库(默认写入 Modules/modules.yaml)
7-
python xrobot_add_mod.py https://github.com/yourorg/BlinkLED.git --version main
8-
9-
# 追加模块实例(默认写入 User/xrobot.yaml)
5+
Usage examples:
6+
python xrobot_add_mod.py xrobot-org/BlinkLED@main
107
python xrobot_add_mod.py BlinkLED
118
"""
129

1310
import argparse
1411
import yaml
1512
from pathlib import Path
16-
from urllib.parse import urlparse
17-
from collections import OrderedDict
1813

14+
# Default config file paths
1915
DEFAULT_REPO_CONFIG = Path("Modules/modules.yaml")
2016
DEFAULT_INSTANCE_CONFIG = Path("User/xrobot.yaml")
2117
MODULES_DIR = Path("Modules")
2218

23-
def is_repo_url(s: str) -> bool:
24-
return s.startswith("http://") or s.startswith("https://")
25-
26-
def extract_name_from_repo(repo: str) -> str:
27-
return Path(urlparse(repo).path).stem
28-
29-
def load_yaml(path: Path) -> dict:
30-
return yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else {}
31-
32-
def save_yaml(path: Path, data: dict):
33-
path.parent.mkdir(parents=True, exist_ok=True)
34-
path.write_text(yaml.dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
35-
36-
def parse_manifest_from_header(header_path: Path) -> dict:
37-
content = header_path.read_text(encoding="utf-8")
38-
lines = content.splitlines()
39-
block = []
40-
inside = False
41-
42-
for line in lines:
43-
stripped = line.strip()
44-
if stripped == "/* === MODULE MANIFEST ===":
45-
inside = True
46-
continue
47-
if stripped == "=== END MANIFEST === */":
48-
break
49-
if inside:
50-
block.append(line.replace("/*", " ").replace("*/", " ").rstrip())
51-
52-
try:
53-
return yaml.safe_load("\n".join(block)) or {}
54-
except yaml.YAMLError as e:
55-
print(f"[ERROR] Failed to parse manifest: {e}")
56-
return {}
57-
58-
def append_module_instance(module_name: str, config_path: Path):
59-
header_path = MODULES_DIR / module_name / f"{module_name}.hpp"
60-
if not header_path.exists():
61-
print(f"[ERROR] Module header not found: {header_path}")
19+
# Import utility functions from local module
20+
from xrobot.ModuleParser import load_single_module, parse_constructor_args
21+
22+
def is_repo_id(s: str) -> bool:
23+
"""
24+
Determine if string is a repo ID (e.g., 'namespace/ModuleName' or 'namespace/ModuleName@version').
25+
"""
26+
return "/" in s
27+
28+
def get_next_instance_id(modules: list, base_name: str) -> str:
29+
"""
30+
Return a unique instance id for the given module name, such as 'BlinkLED_0', 'BlinkLED_1', etc.
31+
"""
32+
existing = [m.get("id", "") for m in modules if m.get("name") == base_name]
33+
nums = []
34+
for eid in existing:
35+
if eid and eid.startswith(base_name + "_"):
36+
try:
37+
nums.append(int(eid[len(base_name) + 1:]))
38+
except Exception:
39+
pass
40+
next_num = (max(nums) + 1) if nums else 0
41+
return f"{base_name}_{next_num}"
42+
43+
def append_module_instance(module_name: str, config_path: Path, instance_id: str = None):
44+
"""
45+
Append a module instance (with id and constructor args) to the user config YAML.
46+
"""
47+
mod_dir = MODULES_DIR / module_name
48+
manifest = load_single_module(mod_dir)
49+
if not manifest:
50+
print(f"[ERROR] Module manifest not found in: {mod_dir}")
6251
return
6352

64-
manifest = parse_manifest_from_header(header_path)
65-
args = manifest.get("constructor_args", {})
66-
args_ordered = OrderedDict()
67-
if isinstance(args, dict):
68-
args_ordered.update(args)
69-
elif isinstance(args, list):
70-
for item in args:
71-
if isinstance(item, dict):
72-
args_ordered.update(item)
73-
74-
data = load_yaml(config_path)
53+
# Parse constructor and template args in order
54+
args_ordered = parse_constructor_args(manifest.constructor_args)
55+
template_args_ordered = parse_constructor_args(manifest.template_args)
56+
57+
# Load or create config file
58+
if config_path.exists():
59+
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
60+
else:
61+
data = {}
7562
if "modules" not in data or not isinstance(data["modules"], list):
7663
data["modules"] = []
7764

78-
data["modules"].append({
65+
# Auto-generate unique instance id if not given
66+
inst_id = instance_id or get_next_instance_id(data["modules"], module_name)
67+
68+
mod_entry = {
69+
"id": inst_id,
7970
"name": module_name,
8071
"constructor_args": dict(args_ordered)
81-
})
82-
83-
save_yaml(config_path, data)
84-
print(f"[SUCCESS] Appended module instance '{module_name}' to {config_path}")
85-
86-
def add_repo_entry(repo: str, version: str, config_path: Path):
87-
name = extract_name_from_repo(repo)
88-
data = load_yaml(config_path)
89-
modules = data.get("modules", [])
72+
}
73+
if template_args_ordered:
74+
mod_entry["template_args"] = dict(template_args_ordered)
75+
76+
data["modules"].append(mod_entry)
77+
78+
config_path.parent.mkdir(parents=True, exist_ok=True)
79+
config_path.write_text(yaml.dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
80+
print(f"[SUCCESS] Appended module instance '{module_name}' as id '{inst_id}' to {config_path}")
81+
82+
def add_repo_entry(repo_id: str, config_path: Path):
83+
"""
84+
Add a new repo module (as a plain string, e.g., 'namespace/ModuleName[@version]') to modules.yaml.
85+
"""
86+
if config_path.exists():
87+
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
88+
else:
89+
data = {}
90+
if "modules" not in data or not isinstance(data["modules"], list):
91+
data["modules"] = []
9092

91-
if any(m.get("name") == name for m in modules):
92-
print(f"[WARN] Module '{name}' already exists in {config_path}. Skipping.")
93+
if repo_id in data["modules"]:
94+
print(f"[WARN] Module '{repo_id}' already exists in {config_path}. Skipping.")
9395
return
9496

95-
entry = {"name": name, "repo": repo}
96-
if version:
97-
entry["version"] = version
98-
modules.append(entry)
99-
100-
save_yaml(config_path, {"modules": modules})
101-
print(f"[SUCCESS] Added repo module '{name}' to {config_path}")
97+
data["modules"].append(repo_id)
98+
config_path.parent.mkdir(parents=True, exist_ok=True)
99+
config_path.write_text(yaml.dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
100+
print(f"[SUCCESS] Added repo module '{repo_id}' to {config_path}")
102101

103102
def main():
104-
parser = argparse.ArgumentParser(description="Add module repo or instance config to YAML")
105-
parser.add_argument("target", help="Repo URL or local module name")
106-
parser.add_argument("--version", "-v", help="Branch or tag (repo only)")
107-
parser.add_argument("--config", "-c", help="YAML file path to write (auto default)")
103+
parser = argparse.ArgumentParser(description="Add a module repo or instance config to a YAML file")
104+
parser.add_argument("target", help="namespace/ModuleName[@version] or local module name")
105+
parser.add_argument("--config", "-c", help="Path to YAML config file (optional, auto default)")
106+
parser.add_argument("--instance-id", help="Optional: manually set the module instance id")
108107

109108
args = parser.parse_args()
110109

111-
# 判断类型并选择默认 config 路径
112-
if is_repo_url(args.target):
110+
if is_repo_id(args.target):
113111
config_path = Path(args.config) if args.config else DEFAULT_REPO_CONFIG
114-
add_repo_entry(args.target, args.version, config_path)
112+
add_repo_entry(args.target, config_path)
115113
else:
116114
config_path = Path(args.config) if args.config else DEFAULT_INSTANCE_CONFIG
117-
append_module_instance(args.target, config_path)
115+
append_module_instance(args.target, config_path, args.instance_id)
118116

119117
if __name__ == "__main__":
120118
main()

0 commit comments

Comments
 (0)