bitool 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bitool/__init__.py +27 -0
- bitool/cmd/__init__.py +65 -0
- bitool/cmd/_base.py +105 -0
- bitool/cmd/_condition.py +60 -0
- bitool/cmd/_scheduler.py +548 -0
- bitool/cmd/env.py +454 -0
- bitool/cmd/git.py +123 -0
- bitool/cmd/io.py +248 -0
- bitool/cmd/pdf.py +385 -0
- bitool/cmd/run.py +300 -0
- bitool/cmd/toml.py +237 -0
- bitool/cmd/version.py +630 -0
- bitool/consts.py +14 -0
- bitool/core/__init__.py +7 -0
- bitool/core/app.py +142 -0
- bitool/core/commands.py +194 -0
- bitool/core/config.py +647 -0
- bitool/core/env.py +18 -0
- bitool/core/logger.py +237 -0
- bitool/core/plugin.py +117 -0
- bitool/core/workspace.py +76 -0
- bitool/models/__init__.py +3 -0
- bitool/models/version.py +173 -0
- bitool/scripts/__init__.py +1 -0
- bitool/scripts/bumpversion.py +189 -0
- bitool/scripts/clearscreen.py +37 -0
- bitool/scripts/envpy.py +161 -0
- bitool/scripts/envrs.py +119 -0
- bitool/scripts/filedate.py +246 -0
- bitool/scripts/filelevel.py +191 -0
- bitool/scripts/gittool.py +178 -0
- bitool/scripts/img2pdf.py +151 -0
- bitool/scripts/pdf2img.py +139 -0
- bitool/scripts/piptool.py +130 -0
- bitool/scripts/pymake.py +345 -0
- bitool/scripts/sshcopyid.py +491 -0
- bitool/scripts/taskkill.py +366 -0
- bitool/scripts/which.py +227 -0
- bitool/types.py +7 -0
- bitool/utils/__init__.py +9 -0
- bitool/utils/cli_parser.py +412 -0
- bitool/utils/executor.py +881 -0
- bitool/utils/profiler.py +369 -0
- bitool/utils/task.py +133 -0
- bitool/utils/task_group.py +668 -0
- bitool/utils/tests/__init__.py +0 -0
- bitool/utils/tests/test_profiler.py +487 -0
- bitool-0.1.2.dist-info/METADATA +154 -0
- bitool-0.1.2.dist-info/RECORD +51 -0
- bitool-0.1.2.dist-info/WHEEL +4 -0
- bitool-0.1.2.dist-info/entry_points.txt +15 -0
bitool/cmd/run.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Callable, List, cast
|
|
8
|
+
|
|
9
|
+
from bitool.cmd import BaseCommand
|
|
10
|
+
from bitool.core import logger
|
|
11
|
+
from bitool.utils.executor import RunResult, execute
|
|
12
|
+
from bitool.utils.task import Task
|
|
13
|
+
from bitool.utils.task_group import TaskGroup
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_shell_command(command: str) -> tuple[list[str], str | None, bool, bool]:
|
|
17
|
+
"""安全地解析包含重定向的 shell 命令。
|
|
18
|
+
|
|
19
|
+
手动解析常见的 shell 操作符(>, >>, |),避免使用 shell=True。
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
command: shell 命令字符串。
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
元组 (cmd_args, stdout_redirect, append_mode, has_pipe)
|
|
26
|
+
- cmd_args: 命令及其参数列表
|
|
27
|
+
- stdout_redirect: 标准输出重定向文件路径(如果有)
|
|
28
|
+
- append_mode: 是否为追加模式(>>)
|
|
29
|
+
- has_pipe: 是否包含管道(暂不支持)
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: 当命令包含不支持的操作符时。
|
|
33
|
+
"""
|
|
34
|
+
# 检查管道操作符(暂不支持)
|
|
35
|
+
if "|" in command:
|
|
36
|
+
msg = "不支持管道操作符, 请使用多个命令分别执行"
|
|
37
|
+
raise ValueError(msg)
|
|
38
|
+
|
|
39
|
+
# 解析重定向操作符
|
|
40
|
+
stdout_redirect: str | None = None
|
|
41
|
+
append_mode = False
|
|
42
|
+
|
|
43
|
+
# 处理 >> (追加)
|
|
44
|
+
if ">>" in command:
|
|
45
|
+
parts_redirect: list[str] = command.split(">>", 1)
|
|
46
|
+
cmd_main: str = parts_redirect[0].strip()
|
|
47
|
+
stdout_redirect = parts_redirect[1].strip()
|
|
48
|
+
append_mode = True
|
|
49
|
+
# 处理 > (覆盖)
|
|
50
|
+
elif ">" in command:
|
|
51
|
+
parts_redirect = command.split(">", 1)
|
|
52
|
+
cmd_main = parts_redirect[0].strip()
|
|
53
|
+
stdout_redirect = parts_redirect[1].strip()
|
|
54
|
+
else:
|
|
55
|
+
cmd_main = command
|
|
56
|
+
|
|
57
|
+
# 使用 shlex 安全地分割命令
|
|
58
|
+
cmd_args: list[str] = shlex.split(cmd_main)
|
|
59
|
+
|
|
60
|
+
return cmd_args, stdout_redirect, append_mode, False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"RunCommand",
|
|
65
|
+
"RunDAGCommands",
|
|
66
|
+
"RunParallelCommands",
|
|
67
|
+
"RunResult",
|
|
68
|
+
"RunSequentialCommands",
|
|
69
|
+
"RunShellCommand",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BaseRunCommand(BaseCommand):
|
|
75
|
+
cmd: list[str] | Callable[[], None] = field(default_factory=list)
|
|
76
|
+
cwd: Path | None = None
|
|
77
|
+
env: dict[str, str] | None = None
|
|
78
|
+
timeout: int | None = None
|
|
79
|
+
check: bool = False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class RunCommand(BaseRunCommand):
|
|
84
|
+
"""执行单个命令"""
|
|
85
|
+
|
|
86
|
+
name = "run"
|
|
87
|
+
description = "执行单个命令"
|
|
88
|
+
|
|
89
|
+
def run(self) -> bool:
|
|
90
|
+
"""执行单个命令"""
|
|
91
|
+
if isinstance(self.cmd, list):
|
|
92
|
+
cmd_list = cast(List[str], self.cmd)
|
|
93
|
+
cmd_str = " ".join(str(arg) for arg in cmd_list)
|
|
94
|
+
try:
|
|
95
|
+
execute(
|
|
96
|
+
cmd_list,
|
|
97
|
+
cwd=self.cwd,
|
|
98
|
+
env=self.env,
|
|
99
|
+
timeout=self.timeout,
|
|
100
|
+
check=self.check,
|
|
101
|
+
)
|
|
102
|
+
# 使用列表推导确保类型安全
|
|
103
|
+
logger.info(f"命令执行成功: `{cmd_str}`")
|
|
104
|
+
except Exception as e: # noqa: BLE001
|
|
105
|
+
logger.error(f"命令执行失败: `{cmd_str}`, 错误: {e}")
|
|
106
|
+
return False
|
|
107
|
+
else:
|
|
108
|
+
# 命令是 Callable[[], None] 类型,直接调用
|
|
109
|
+
self.cmd()
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class RunShellCommand(BaseRunCommand):
|
|
115
|
+
"""执行shell命令字符串"""
|
|
116
|
+
|
|
117
|
+
name = "run_shell"
|
|
118
|
+
description = "执行shell命令字符串"
|
|
119
|
+
shell_cmd: str = ""
|
|
120
|
+
|
|
121
|
+
def run(self) -> bool:
|
|
122
|
+
"""执行shell命令字符串(安全版本,支持重定向)。"""
|
|
123
|
+
try:
|
|
124
|
+
# 安全解析命令(避免使用 shell=True)
|
|
125
|
+
cmd_args, stdout_redirect, append_mode, _ = _parse_shell_command(
|
|
126
|
+
self.shell_cmd
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if not cmd_args:
|
|
130
|
+
logger.error(f"Shell命令为空: `{self.shell_cmd}`")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# 执行命令并处理重定向
|
|
134
|
+
if stdout_redirect:
|
|
135
|
+
# 确保输出目录存在
|
|
136
|
+
redirect_path = Path(stdout_redirect)
|
|
137
|
+
if redirect_path.parent != Path("."):
|
|
138
|
+
redirect_path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
# 以追加或覆盖模式打开文件
|
|
141
|
+
mode = "a" if append_mode else "w"
|
|
142
|
+
with redirect_path.open(mode, encoding="utf-8") as outfile:
|
|
143
|
+
result = subprocess.run( # noqa: S603
|
|
144
|
+
cmd_args,
|
|
145
|
+
cwd=self.cwd,
|
|
146
|
+
timeout=self.timeout,
|
|
147
|
+
stdout=outfile,
|
|
148
|
+
stderr=subprocess.PIPE,
|
|
149
|
+
text=True,
|
|
150
|
+
check=False,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if result.returncode == 0:
|
|
154
|
+
logger.info(f"Shell命令执行成功: `{self.shell_cmd}`")
|
|
155
|
+
return True
|
|
156
|
+
else:
|
|
157
|
+
error_output = result.stderr.strip()
|
|
158
|
+
logger.error(
|
|
159
|
+
f"Shell命令执行失败: `{self.shell_cmd}`, 返回码: {result.returncode}, 错误: {error_output}"
|
|
160
|
+
)
|
|
161
|
+
return False
|
|
162
|
+
else:
|
|
163
|
+
# 无重定向,正常执行
|
|
164
|
+
result = subprocess.run( # noqa: S603
|
|
165
|
+
cmd_args,
|
|
166
|
+
cwd=self.cwd,
|
|
167
|
+
timeout=self.timeout,
|
|
168
|
+
capture_output=True,
|
|
169
|
+
text=True,
|
|
170
|
+
check=False,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if result.returncode == 0:
|
|
174
|
+
logger.info(f"Shell命令执行成功: `{self.shell_cmd}`")
|
|
175
|
+
if result.stdout.strip():
|
|
176
|
+
for line in result.stdout.strip().split("\n"):
|
|
177
|
+
if line.strip():
|
|
178
|
+
logger.info(f" 输出: `{line.strip()}`")
|
|
179
|
+
return True
|
|
180
|
+
else:
|
|
181
|
+
error_output = result.stderr.strip()
|
|
182
|
+
logger.error(
|
|
183
|
+
f"Shell命令执行失败: `{self.shell_cmd}`, 返回码: {result.returncode}, 错误: {error_output}"
|
|
184
|
+
)
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
except subprocess.TimeoutExpired:
|
|
188
|
+
logger.error(f"Shell命令执行超时: `{self.shell_cmd}`")
|
|
189
|
+
return False
|
|
190
|
+
except ValueError as e:
|
|
191
|
+
logger.error(f"Shell命令解析失败: `{self.shell_cmd}`, 错误: {e}")
|
|
192
|
+
return False
|
|
193
|
+
except Exception as e: # noqa: BLE001
|
|
194
|
+
logger.error(f"Shell命令执行异常: `{self.shell_cmd}`, 错误: {e}")
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class BaseRunMultipleCommands(BaseCommand):
|
|
200
|
+
commands: list[list[str]] = field(default_factory=list)
|
|
201
|
+
stop_on_failure: bool = False
|
|
202
|
+
max_workers: int | None = None
|
|
203
|
+
results: list[RunResult] = field(default_factory=list)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class RunSequentialCommands(BaseRunMultipleCommands):
|
|
208
|
+
"""顺序执行多个命令"""
|
|
209
|
+
|
|
210
|
+
name = "run_sequential"
|
|
211
|
+
description = "顺序执行多个命令"
|
|
212
|
+
|
|
213
|
+
def run(self) -> bool:
|
|
214
|
+
"""顺序执行多个命令"""
|
|
215
|
+
if not self.commands:
|
|
216
|
+
logger.info("命令列表为空,直接返回")
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
tasks = [Task(cmd=cmd) for cmd in self.commands]
|
|
220
|
+
group = TaskGroup(
|
|
221
|
+
name="sequential-execution",
|
|
222
|
+
commands=tasks,
|
|
223
|
+
parallel=False,
|
|
224
|
+
stop_on_failure=self.stop_on_failure,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
logger.info(f"开始顺序执行 {len(self.commands)} 个命令")
|
|
228
|
+
self.results = group.execute()
|
|
229
|
+
return all(r.returncode == 0 for r in self.results)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class RunParallelCommands(BaseRunMultipleCommands):
|
|
234
|
+
"""并行执行多个命令"""
|
|
235
|
+
|
|
236
|
+
name = "run_parallel"
|
|
237
|
+
description = "并行执行多个命令"
|
|
238
|
+
|
|
239
|
+
def run(self) -> bool:
|
|
240
|
+
"""并行执行多个命令"""
|
|
241
|
+
if not self.commands:
|
|
242
|
+
logger.info("命令列表为空,直接返回")
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
tasks = [Task(cmd=cmd, parallel=True) for cmd in self.commands]
|
|
246
|
+
group = TaskGroup(
|
|
247
|
+
name="parallel-execution",
|
|
248
|
+
commands=tasks,
|
|
249
|
+
parallel=True,
|
|
250
|
+
max_workers=self.max_workers,
|
|
251
|
+
stop_on_failure=False,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
logger.info(f"开始并行执行 {len(self.commands)} 个命令")
|
|
255
|
+
self.results = group.execute()
|
|
256
|
+
return all(r.returncode == 0 for r in self.results)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass
|
|
260
|
+
class RunDAGCommands(BaseRunMultipleCommands):
|
|
261
|
+
"""基于DAG依赖调度执行命令"""
|
|
262
|
+
|
|
263
|
+
name = "run_dag"
|
|
264
|
+
description = "基于DAG依赖调度执行命令"
|
|
265
|
+
tasks: list[dict[str, object]] = field(default_factory=list)
|
|
266
|
+
|
|
267
|
+
def run(self) -> bool:
|
|
268
|
+
"""基于DAG依赖调度执行命令"""
|
|
269
|
+
if not self.tasks:
|
|
270
|
+
logger.info("任务列表为空,直接返回")
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
task_objects = []
|
|
274
|
+
for task_config in self.tasks:
|
|
275
|
+
# 类型守卫: 从 dict 中提取具体类型
|
|
276
|
+
name = str(task_config.get("name", ""))
|
|
277
|
+
cmd = task_config.get("cmd")
|
|
278
|
+
func = task_config.get("func")
|
|
279
|
+
depends_on_val = task_config.get("depends_on", [])
|
|
280
|
+
description = str(task_config.get("description", ""))
|
|
281
|
+
|
|
282
|
+
task = Task(
|
|
283
|
+
name=name,
|
|
284
|
+
cmd=cmd if isinstance(cmd, list) else None, # type: ignore
|
|
285
|
+
func=func if callable(func) else None, # type: ignore
|
|
286
|
+
depends_on=depends_on_val if isinstance(depends_on_val, list) else [], # type: ignore
|
|
287
|
+
description=description,
|
|
288
|
+
)
|
|
289
|
+
task_objects.append(task)
|
|
290
|
+
|
|
291
|
+
group = TaskGroup(
|
|
292
|
+
name="dag-execution",
|
|
293
|
+
commands=task_objects,
|
|
294
|
+
max_workers=self.max_workers,
|
|
295
|
+
stop_on_failure=self.stop_on_failure,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
logger.info(f"开始DAG调度执行 {len(self.tasks)} 个任务")
|
|
299
|
+
self.results = group.execute()
|
|
300
|
+
return all(r.returncode == 0 for r in self.results)
|
bitool/cmd/toml.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""TOML文件操作命令模块.
|
|
2
|
+
|
|
3
|
+
提供TOML文件的读取、写入和版本号更新等功能.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import tomli
|
|
14
|
+
import tomli_w
|
|
15
|
+
except ImportError:
|
|
16
|
+
try:
|
|
17
|
+
import tomli_w
|
|
18
|
+
import tomllib as tomli # ty: ignore[unresolved-import]
|
|
19
|
+
except ImportError:
|
|
20
|
+
tomli = None # ty: ignore
|
|
21
|
+
tomli_w = None # ty: ignore
|
|
22
|
+
|
|
23
|
+
from bitool.cmd._base import BaseCommand
|
|
24
|
+
from bitool.core import logger
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ReadTomlCommand(BaseCommand):
|
|
29
|
+
"""读取TOML文件命令.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
filepath: TOML文件路径
|
|
33
|
+
data: 解析后的TOML数据
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
name: str = "read-toml"
|
|
37
|
+
description: str = "读取TOML文件"
|
|
38
|
+
filepath: Path = Path()
|
|
39
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
def run(self) -> bool:
|
|
42
|
+
"""执行TOML文件读取.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
bool
|
|
47
|
+
读取成功返回True, 否则返回False
|
|
48
|
+
"""
|
|
49
|
+
if not self.filepath.exists():
|
|
50
|
+
logger.warning(f"TOML文件不存在: {self.filepath}")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
with self.filepath.open("rb") as f:
|
|
55
|
+
if tomli is not None:
|
|
56
|
+
self.data = tomli.load(f)
|
|
57
|
+
else:
|
|
58
|
+
logger.error("未安装tomli或tomllib库")
|
|
59
|
+
return False
|
|
60
|
+
except Exception as e: # noqa: BLE001
|
|
61
|
+
logger.error(f"读取TOML文件失败: {e}")
|
|
62
|
+
return False
|
|
63
|
+
else:
|
|
64
|
+
logger.info(f"成功读取TOML文件: {self.filepath}")
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class WriteTomlCommand(BaseCommand):
|
|
70
|
+
"""写入TOML文件命令.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
filepath: TOML文件路径
|
|
74
|
+
data: 要写入的TOML数据
|
|
75
|
+
overwrite: 是否覆盖已存在的文件
|
|
76
|
+
backup: 写入前是否备份原文件
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
name: str = "write-toml"
|
|
80
|
+
description: str = "写入TOML文件"
|
|
81
|
+
filepath: Path = Path()
|
|
82
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
83
|
+
overwrite: bool = True
|
|
84
|
+
backup: bool = False
|
|
85
|
+
|
|
86
|
+
def run(self) -> bool:
|
|
87
|
+
"""执行TOML文件写入.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
bool
|
|
92
|
+
写入成功返回True, 否则返回False
|
|
93
|
+
"""
|
|
94
|
+
if tomli_w is None:
|
|
95
|
+
logger.error("未安装tomli_w库")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
if self.filepath.exists() and not self.overwrite:
|
|
99
|
+
logger.warning(f"TOML文件已存在且不允许覆盖: {self.filepath}")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# 备份原文件
|
|
104
|
+
if self.backup and self.filepath.exists():
|
|
105
|
+
backup_path = self.filepath.with_suffix(self.filepath.suffix + ".bak")
|
|
106
|
+
backup_path.write_bytes(self.filepath.read_bytes())
|
|
107
|
+
logger.info(f"已备份TOML文件: {self.filepath} -> {backup_path}")
|
|
108
|
+
|
|
109
|
+
# 确保父目录存在
|
|
110
|
+
if not self.filepath.parent.exists():
|
|
111
|
+
self.filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
|
|
113
|
+
# 写入TOML文件
|
|
114
|
+
with self.filepath.open("wb") as f:
|
|
115
|
+
tomli_w.dump(self.data, f)
|
|
116
|
+
except Exception as e: # noqa: BLE001
|
|
117
|
+
logger.error(f"写入TOML文件失败: {e}")
|
|
118
|
+
return False
|
|
119
|
+
else:
|
|
120
|
+
logger.info(f"成功写入TOML文件: {self.filepath}")
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class UpdateTomlVersionCommand(BaseCommand):
|
|
126
|
+
"""更新TOML文件版本号命令.
|
|
127
|
+
|
|
128
|
+
Attributes:
|
|
129
|
+
filepath: TOML文件路径
|
|
130
|
+
version: 新版本号字符串
|
|
131
|
+
version_path: 版本号在TOML中的路径, 如 ["project", "version"]
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
name: str = "update-toml-version"
|
|
135
|
+
description: str = "更新TOML文件中的版本号"
|
|
136
|
+
filepath: Path = Path()
|
|
137
|
+
version: str = ""
|
|
138
|
+
version_path: list[str] = field(default_factory=lambda: ["project", "version"])
|
|
139
|
+
|
|
140
|
+
def run(self) -> bool:
|
|
141
|
+
"""执行TOML文件版本号更新.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
bool
|
|
146
|
+
更新成功返回True, 否则返回False
|
|
147
|
+
"""
|
|
148
|
+
if tomli is None or tomli_w is None:
|
|
149
|
+
logger.error("未安装tomli或tomli_w库")
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
if not self.filepath.exists():
|
|
153
|
+
logger.warning(f"TOML文件不存在: {self.filepath}")
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# 读取原文件
|
|
158
|
+
with self.filepath.open("rb") as f:
|
|
159
|
+
data = tomli.load(f)
|
|
160
|
+
|
|
161
|
+
# 导航到版本号位置并更新
|
|
162
|
+
current = data
|
|
163
|
+
for key in self.version_path[:-1]:
|
|
164
|
+
if key not in current:
|
|
165
|
+
current[key] = {}
|
|
166
|
+
current = current[key]
|
|
167
|
+
|
|
168
|
+
old_version = current.get(self.version_path[-1], "未知")
|
|
169
|
+
current[self.version_path[-1]] = self.version
|
|
170
|
+
|
|
171
|
+
# 写回文件
|
|
172
|
+
with self.filepath.open("wb") as f:
|
|
173
|
+
tomli_w.dump(data, f)
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
f"已更新版本号: {self.filepath} ({old_version} -> {self.version})"
|
|
177
|
+
)
|
|
178
|
+
except Exception as e: # noqa: BLE001
|
|
179
|
+
logger.error(f"更新TOML版本号失败: {e}")
|
|
180
|
+
return False
|
|
181
|
+
else:
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class ReadTomlVersionCommand(BaseCommand):
|
|
187
|
+
"""读取TOML文件版本号命令.
|
|
188
|
+
|
|
189
|
+
Attributes:
|
|
190
|
+
filepath: TOML文件路径
|
|
191
|
+
version: 读取到的版本号
|
|
192
|
+
version_path: 版本号在TOML中的路径
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
name: str = "read-toml-version"
|
|
196
|
+
description: str = "读取TOML文件中的版本号"
|
|
197
|
+
filepath: Path = Path()
|
|
198
|
+
version: str = ""
|
|
199
|
+
version_path: list[str] = field(default_factory=lambda: ["project", "version"])
|
|
200
|
+
|
|
201
|
+
def run(self) -> bool:
|
|
202
|
+
"""执行TOML文件版本号读取.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
bool
|
|
207
|
+
读取成功返回True, 否则返回False
|
|
208
|
+
"""
|
|
209
|
+
if tomli is None:
|
|
210
|
+
logger.error("未安装tomli或tomllib库")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
if not self.filepath.exists():
|
|
214
|
+
logger.warning(f"TOML文件不存在: {self.filepath}")
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
with self.filepath.open("rb") as f:
|
|
219
|
+
data = tomli.load(f)
|
|
220
|
+
|
|
221
|
+
# 导航到版本号位置
|
|
222
|
+
current = data
|
|
223
|
+
for key in self.version_path:
|
|
224
|
+
if key not in current:
|
|
225
|
+
logger.warning(
|
|
226
|
+
f"在 {self.filepath} 中未找到版本号路径: {'.'.join(self.version_path)}"
|
|
227
|
+
)
|
|
228
|
+
return False
|
|
229
|
+
current = current[key]
|
|
230
|
+
|
|
231
|
+
self.version = str(current)
|
|
232
|
+
logger.info(f"读取到版本号: {self.filepath} -> {self.version}")
|
|
233
|
+
except Exception as e: # noqa: BLE001
|
|
234
|
+
logger.error(f"读取TOML版本号失败: {e}")
|
|
235
|
+
return False
|
|
236
|
+
else:
|
|
237
|
+
return True
|