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.
Files changed (51) hide show
  1. bitool/__init__.py +27 -0
  2. bitool/cmd/__init__.py +65 -0
  3. bitool/cmd/_base.py +105 -0
  4. bitool/cmd/_condition.py +60 -0
  5. bitool/cmd/_scheduler.py +548 -0
  6. bitool/cmd/env.py +454 -0
  7. bitool/cmd/git.py +123 -0
  8. bitool/cmd/io.py +248 -0
  9. bitool/cmd/pdf.py +385 -0
  10. bitool/cmd/run.py +300 -0
  11. bitool/cmd/toml.py +237 -0
  12. bitool/cmd/version.py +630 -0
  13. bitool/consts.py +14 -0
  14. bitool/core/__init__.py +7 -0
  15. bitool/core/app.py +142 -0
  16. bitool/core/commands.py +194 -0
  17. bitool/core/config.py +647 -0
  18. bitool/core/env.py +18 -0
  19. bitool/core/logger.py +237 -0
  20. bitool/core/plugin.py +117 -0
  21. bitool/core/workspace.py +76 -0
  22. bitool/models/__init__.py +3 -0
  23. bitool/models/version.py +173 -0
  24. bitool/scripts/__init__.py +1 -0
  25. bitool/scripts/bumpversion.py +189 -0
  26. bitool/scripts/clearscreen.py +37 -0
  27. bitool/scripts/envpy.py +161 -0
  28. bitool/scripts/envrs.py +119 -0
  29. bitool/scripts/filedate.py +246 -0
  30. bitool/scripts/filelevel.py +191 -0
  31. bitool/scripts/gittool.py +178 -0
  32. bitool/scripts/img2pdf.py +151 -0
  33. bitool/scripts/pdf2img.py +139 -0
  34. bitool/scripts/piptool.py +130 -0
  35. bitool/scripts/pymake.py +345 -0
  36. bitool/scripts/sshcopyid.py +491 -0
  37. bitool/scripts/taskkill.py +366 -0
  38. bitool/scripts/which.py +227 -0
  39. bitool/types.py +7 -0
  40. bitool/utils/__init__.py +9 -0
  41. bitool/utils/cli_parser.py +412 -0
  42. bitool/utils/executor.py +881 -0
  43. bitool/utils/profiler.py +369 -0
  44. bitool/utils/task.py +133 -0
  45. bitool/utils/task_group.py +668 -0
  46. bitool/utils/tests/__init__.py +0 -0
  47. bitool/utils/tests/test_profiler.py +487 -0
  48. bitool-0.1.2.dist-info/METADATA +154 -0
  49. bitool-0.1.2.dist-info/RECORD +51 -0
  50. bitool-0.1.2.dist-info/WHEEL +4 -0
  51. bitool-0.1.2.dist-info/entry_points.txt +15 -0
@@ -0,0 +1,369 @@
1
+ """性能分析工具模块.
2
+
3
+ 提供基于装饰器的性能分析功能,追踪函数运行时间和内存使用。
4
+
5
+ 示例:
6
+ >>> from bitool import profile
7
+ >>> @profile
8
+ ... def my_function():
9
+ ... pass
10
+ >>> my_function()
11
+
12
+ 性能测试应用:
13
+ 通过环境变量 BITOOL_PROFILE=1 启用性能分析,避免影响发布脚本性能。
14
+ 在性能测试脚本中使用,而不是在生产脚本中使用。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import contextlib
20
+ import functools
21
+ import threading
22
+ import time
23
+ import tracemalloc
24
+ from dataclasses import dataclass, field
25
+ from functools import cached_property
26
+
27
+ from rich.console import Console
28
+ from rich.table import Table
29
+
30
+ from ..core import env_check_enabled, logger
31
+ from ..types import FunctionType, ParamType, ReturnType
32
+
33
+ # 延迟初始化 tracemalloc,避免影响正常性能
34
+ _tracemalloc_started = False
35
+
36
+
37
+ # 常量定义
38
+ _MS_PER_SEC = 1000 # 每秒毫秒数
39
+ _μS_PER_MS = 1000 # 每毫秒微秒数
40
+ _MEMORY_THRESHOLD_MB = 0.01 # 内存变化阈值(MB)
41
+ _MAX_FUNC_NAME_LENGTH = 28 # 函数名最大长度
42
+ _FUNC_NAME_TRUNCATE_LENGTH = 25 # 函数名截断长度
43
+
44
+ # 显示配置常量
45
+ _CONSOLE_WIDTH = 120 # 控制台宽度
46
+ _TABLE_TITLE_STYLE = "bold magenta" # 表格标题样式
47
+ _TABLE_BORDER_STYLE = "blue" # 表格边框样式
48
+ _TABLE_HEADER_STYLE = "bold cyan" # 表头样式
49
+ _COL_FUNC_NAME_STYLE = "green" # 函数名列样式
50
+ _COL_DURATION_STYLE = "yellow" # 耗时列样式
51
+ _COL_PERCENTAGE_STYLE = "blue" # 占比列样式
52
+ _COL_CALL_COUNT_STYLE = "magenta" # 调用次数列样式
53
+ _COL_MEMORY_STYLE = "red" # 内存列样式
54
+
55
+
56
+ _USE_RUST_PROFILER = False
57
+
58
+
59
+ __all__ = ["profile"]
60
+
61
+
62
+ @dataclass
63
+ class _PyProfileRecord:
64
+ """Python 实现的性能记录(用于 Rust 不可用时的降级)."""
65
+
66
+ name: str
67
+ duration_secs: float
68
+ memory_before_mb: float = 0.0
69
+ memory_after_mb: float = 0.0
70
+ memory_delta_mb: float = 0.0
71
+ timestamp: str = ""
72
+
73
+
74
+ class _PythonPerformanceProfiler:
75
+ """Python 实现的性能分析器(用于 Rust 不可用时的降级)."""
76
+
77
+ def __init__(self) -> None:
78
+ self._records: list[_PyProfileRecord] = []
79
+ self._active_timers: dict[str, float] = {}
80
+ self._call_counts: dict[str, int] = {}
81
+ self._ensure_tracemalloc_started()
82
+
83
+ def _ensure_tracemalloc_started(self) -> None:
84
+ """确保 tracemalloc 已启动(延迟初始化)."""
85
+ global _tracemalloc_started
86
+ if not _tracemalloc_started:
87
+ try:
88
+ tracemalloc.start()
89
+ _tracemalloc_started = True
90
+ logger.debug("已启动 tracemalloc 内存追踪")
91
+ except RuntimeError as e:
92
+ logger.warning(f"启动 tracemalloc 失败: {e}")
93
+
94
+ def start_timer(self, name: str) -> None:
95
+ """开始计时."""
96
+ self._active_timers[name] = time.perf_counter()
97
+ # 记录当前内存使用
98
+ try:
99
+ current, _peak = tracemalloc.get_traced_memory()
100
+ self._active_timers[f"{name}_memory"] = current / (1024 * 1024)
101
+ except RuntimeError as e:
102
+ logger.debug(f"获取内存信息失败: {e}")
103
+
104
+ def stop_timer(self, name: str) -> _PyProfileRecord | None:
105
+ """停止计时并记录性能数据."""
106
+ start_time = self._active_timers.pop(name, None)
107
+ if start_time is None:
108
+ logger.warning(f"未找到计时器: {name}")
109
+ return None
110
+
111
+ duration = time.perf_counter() - start_time
112
+
113
+ # 获取内存信息
114
+ memory_before = self._active_timers.pop(f"{name}_memory", 0.0)
115
+ memory_after = 0.0
116
+ memory_delta = 0.0
117
+ try:
118
+ current, _peak = tracemalloc.get_traced_memory()
119
+ memory_after = current / (1024 * 1024)
120
+ memory_delta = memory_after - memory_before
121
+ except RuntimeError as e:
122
+ logger.debug(f"获取内存信息失败: {e}")
123
+
124
+ record = _PyProfileRecord(
125
+ name=name,
126
+ duration_secs=duration,
127
+ memory_before_mb=memory_before,
128
+ memory_after_mb=memory_after,
129
+ memory_delta_mb=memory_delta,
130
+ timestamp=time.strftime("%Y-%m-%d %H:%M:%S"),
131
+ )
132
+ self._records.append(record)
133
+ self._call_counts[name] = self._call_counts.get(name, 0) + 1
134
+ return record
135
+
136
+ def get_records(self) -> list[_PyProfileRecord]:
137
+ """获取所有性能记录."""
138
+ return self._records.copy()
139
+
140
+ def get_call_count(self, name: str) -> int:
141
+ """获取调用次数."""
142
+ return self._call_counts.get(name, 0)
143
+
144
+ def clear(self) -> None:
145
+ """清除所有记录."""
146
+ self._records.clear()
147
+ self._active_timers.clear()
148
+ self._call_counts.clear()
149
+
150
+
151
+ @dataclass
152
+ class _FuncStats:
153
+ """函数性能统计数据."""
154
+
155
+ total_time: float = 0.0 # 总耗时(包含子调用)
156
+ self_time: float = 0.0 # 自身耗时(不包含子调用)
157
+ call_count: int = 0
158
+ memory_delta: float = 0.0
159
+
160
+
161
+ @dataclass
162
+ class _PerformanceProfilerWrapper:
163
+ """性能分析器包装器."""
164
+
165
+ _call_depth: dict[int, int] = field(default_factory=dict)
166
+ _lock: threading.Lock = field(default_factory=threading.Lock)
167
+ _child_time: dict[int, float] = field(default_factory=dict)
168
+ _self_times: dict[str, float] = field(default_factory=dict)
169
+
170
+ def profile(self, func: FunctionType) -> FunctionType:
171
+ """性能分析装饰器."""
172
+ # 动态检查环境变量,支持运行时启用
173
+ if not env_check_enabled("BITOOL_PROFILE"):
174
+ return func
175
+
176
+ func_name: str = getattr(func, "__name__", "Unknown")
177
+
178
+ @functools.wraps(func)
179
+ def wrapper(*args: ParamType.args, **kwargs: ParamType.kwargs) -> ReturnType:
180
+ thread_id: int = threading.current_thread().ident or 0
181
+
182
+ with self._lock:
183
+ self._call_depth.setdefault(thread_id, 0)
184
+ self._call_depth[thread_id] += 1
185
+ is_top_level = self._call_depth[thread_id] == 1
186
+ # 记录进入函数前的子调用耗时(用于计算自身耗时)
187
+ self._child_time.setdefault(thread_id, 0.0)
188
+ entry_child_time = self._child_time[thread_id]
189
+
190
+ self.profiler.start_timer(func_name)
191
+
192
+ try:
193
+ result = func(*args, **kwargs)
194
+ except BaseException:
195
+ # 异常情况下也要停止计时器, 避免计时器泄漏
196
+ with contextlib.suppress(BaseException):
197
+ self.profiler.stop_timer(func_name)
198
+ raise
199
+ else:
200
+ # 正常执行完成,停止计时器
201
+ record = self.profiler.stop_timer(func_name)
202
+
203
+ # 计算自身耗时
204
+ if record is not None:
205
+ # 退出时的子调用耗时 - 进入时的子调用耗时 = 当前函数的子调用耗时
206
+ with self._lock:
207
+ exit_child_time = self._child_time[thread_id]
208
+ children_time = exit_child_time - entry_child_time
209
+ self_time = record.duration_secs - children_time
210
+
211
+ # 保存自身耗时到字典中
212
+ self._self_times[func_name] = (
213
+ self._self_times.get(func_name, 0.0) + self_time
214
+ )
215
+
216
+ # 将当前函数的总耗时累加到父函数的子调用耗时中
217
+ with self._lock:
218
+ if self._call_depth[thread_id] > 1: # 如果有父函数
219
+ self._child_time[thread_id] += record.duration_secs
220
+
221
+ return result
222
+ finally:
223
+ with self._lock:
224
+ self._call_depth[thread_id] -= 1
225
+ if is_top_level and self._call_depth[thread_id] == 0:
226
+ self._show_summary()
227
+
228
+ return wrapper # type: ignore[return-value]
229
+
230
+ def _show_summary(self) -> None:
231
+ """显示性能分析摘要."""
232
+ if not self.records:
233
+ logger.info("无性能数据")
234
+ return
235
+
236
+ # 使用最后一条记录(顶层函数)的耗时作为基准
237
+ baseline_time = self.records[-1].duration_secs
238
+
239
+ # 创建 Rich 表格(使用缓存的 Console 实例)
240
+ console = Console(width=_CONSOLE_WIDTH)
241
+ table = Table(
242
+ title="性能分析报告",
243
+ title_style=_TABLE_TITLE_STYLE,
244
+ border_style=_TABLE_BORDER_STYLE,
245
+ show_header=True,
246
+ header_style=_TABLE_HEADER_STYLE,
247
+ )
248
+
249
+ # 添加列
250
+ table.add_column("函数名", style=_COL_FUNC_NAME_STYLE, max_width=30)
251
+ table.add_column("总耗时", justify="right", style=_COL_DURATION_STYLE)
252
+ table.add_column("自身耗时", justify="right", style=_COL_DURATION_STYLE)
253
+ table.add_column("占比", justify="right", style=_COL_PERCENTAGE_STYLE)
254
+ table.add_column("调用次数", justify="right", style=_COL_CALL_COUNT_STYLE)
255
+ table.add_column("内存变化", justify="right", style=_COL_MEMORY_STYLE)
256
+
257
+ # 添加数据行
258
+ for name, stats in self.sorted_funcs:
259
+ # 格式化总耗时
260
+ total_time_str = self._format_duration(stats.total_time)
261
+ # 格式化自身耗时
262
+ self_time_str = self._format_duration(stats.self_time)
263
+
264
+ # 格式化占比(相对于顶层函数耗时,使用自身耗时)
265
+ percentage = (
266
+ stats.self_time / baseline_time * 100 if baseline_time > 0 else 0
267
+ )
268
+ percentage_str = f"{percentage:.1f}%"
269
+
270
+ # 格式化调用次数
271
+ call_str = f"{stats.call_count}x"
272
+
273
+ # 格式化内存变化
274
+ mem_str = self._format_memory(stats.memory_delta)
275
+
276
+ # 截断过长的函数名
277
+ display_name = self._truncate_func_name(name)
278
+
279
+ table.add_row(
280
+ display_name,
281
+ total_time_str,
282
+ self_time_str,
283
+ percentage_str,
284
+ call_str,
285
+ mem_str,
286
+ )
287
+
288
+ # 添加总计行
289
+ total_str = self._format_duration(baseline_time)
290
+
291
+ # 计算总调用次数
292
+ total_calls = sum(stats.call_count for _, stats in self.func_stats.items())
293
+
294
+ table.add_row(
295
+ "总计", total_str, "", "100.0%", f"{total_calls}次", "", style="bold"
296
+ )
297
+
298
+ # 输出表格
299
+ console.print(table)
300
+
301
+ @cached_property
302
+ def profiler(self) -> _PythonPerformanceProfiler:
303
+ """获取性能分析器实例."""
304
+ return _PythonPerformanceProfiler()
305
+
306
+ @cached_property
307
+ def records(self) -> list[_PyProfileRecord]:
308
+ """获取所有性能记录."""
309
+ return self.profiler.get_records()
310
+
311
+ @cached_property
312
+ def total_time(self) -> float:
313
+ """获取总耗时(秒)."""
314
+ return sum(record.duration_secs for record in self.records)
315
+
316
+ @cached_property
317
+ def func_stats(self) -> dict[str, _FuncStats]:
318
+ """获取所有函数的性能统计数据."""
319
+ from collections import defaultdict
320
+
321
+ func_stats: dict[str, _FuncStats] = defaultdict(_FuncStats)
322
+ for record in self.records:
323
+ name = record.name
324
+ func_stats[name].total_time += record.duration_secs
325
+ func_stats[name].call_count += 1
326
+ func_stats[name].memory_delta += record.memory_delta_mb
327
+ # 使用字典中的自身耗时
328
+ func_stats[name].self_time = self._self_times.get(name, 0.0)
329
+ return dict(func_stats)
330
+
331
+ @cached_property
332
+ def sorted_funcs(self) -> list[tuple[str, _FuncStats]]:
333
+ """获取按总耗时排序的函数性能统计数据."""
334
+ return sorted(
335
+ self.func_stats.items(), key=lambda x: x[1].total_time, reverse=True
336
+ )
337
+
338
+ def _format_duration(self, seconds: float) -> str:
339
+ """格式化时间显示."""
340
+ total_ms = seconds * _MS_PER_SEC
341
+ if total_ms >= _MS_PER_SEC:
342
+ return f"{total_ms / _MS_PER_SEC:.2f}s"
343
+ if total_ms >= 1:
344
+ return f"{total_ms:.0f}ms"
345
+ return f"{total_ms * _μS_PER_MS:.0f}μs"
346
+
347
+ def _format_memory(self, delta_mb: float) -> str:
348
+ """格式化内存变化显示."""
349
+ if abs(delta_mb) > _MEMORY_THRESHOLD_MB:
350
+ return f"{delta_mb:+.2f}MB"
351
+ return "~0MB"
352
+
353
+ def _truncate_func_name(self, name: str) -> str:
354
+ """截断过长的函数名."""
355
+ if len(name) > _MAX_FUNC_NAME_LENGTH:
356
+ return name[:_FUNC_NAME_TRUNCATE_LENGTH] + "..."
357
+ return name
358
+
359
+ def clear(self) -> None:
360
+ """清除所有性能记录."""
361
+ self.profiler.clear()
362
+ with self._lock:
363
+ self._call_depth.clear()
364
+ self._child_time.clear()
365
+ self._self_times.clear()
366
+
367
+
368
+ _profiler_wrapper = _PerformanceProfilerWrapper()
369
+ profile = _profiler_wrapper.profile
bitool/utils/task.py ADDED
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from functools import cached_property
5
+ from typing import Any, Callable
6
+
7
+ from .executor import RunResult
8
+
9
+
10
+ @dataclass
11
+ class Task:
12
+ """单个任务项.
13
+
14
+ Attributes
15
+ ----------
16
+ name : str
17
+ 任务唯一标识,用于依赖引用
18
+ cmd : list[str], optional
19
+ 命令及其参数列表
20
+ func : Callable[[], RunResult], optional
21
+ 可执行的函数
22
+ condition : Callable[[], bool], optional
23
+ 执行条件函数,返回True才执行
24
+ description : str
25
+ 任务描述信息
26
+ parallel : bool
27
+ 是否允许并行执行,默认 False (顺序执行)
28
+ depends_on : list[str]
29
+ 依赖的任务名称列表,用于DAG调度
30
+ """
31
+
32
+ cmd: list[str] | None = None
33
+ func: Callable[[], RunResult] | None = None
34
+ condition: Callable[[], bool] | None = None
35
+ description: str = ""
36
+ parallel: bool = False
37
+ name: str = ""
38
+ depends_on: list[str] = field(default_factory=list)
39
+
40
+ def __post_init__(self) -> None:
41
+ """验证任务项配置."""
42
+ if self.cmd is None and self.func is None:
43
+ msg = "Task 必须提供 cmd 或 func 参数"
44
+ raise ValueError(msg)
45
+
46
+ # 如果声明了依赖,必须有名称
47
+ if self.depends_on and not self.name:
48
+ msg = "Task 如果声明了 depends_on,必须提供 name 参数"
49
+ raise ValueError(msg)
50
+
51
+ @cached_property
52
+ def is_executable(self) -> bool:
53
+ """检查是否应该执行此任务."""
54
+ if self.condition is None:
55
+ return True
56
+
57
+ return self.condition()
58
+
59
+ @cached_property
60
+ def executable_cmd(self) -> list[str] | Callable[[], RunResult] | None:
61
+ """转换为可执行对象."""
62
+ if self.cmd is not None:
63
+ return self.cmd
64
+
65
+ return self.func
66
+
67
+ @classmethod
68
+ def from_dict(cls, config: dict[str, Any]) -> Task:
69
+ """从字典配置创建任务.
70
+
71
+ Parameters
72
+ ----------
73
+ config : dict[str, Any]
74
+ 任务配置字典
75
+
76
+ Returns
77
+ -------
78
+ Task
79
+ 任务实例
80
+ """
81
+ return cls(
82
+ name=config.get("name", ""),
83
+ cmd=config.get("cmd"),
84
+ func=config.get("func"),
85
+ condition=config.get("condition"),
86
+ description=config.get("description", ""),
87
+ parallel=config.get("parallel", False),
88
+ depends_on=config.get("depends_on", []),
89
+ )
90
+
91
+ @classmethod
92
+ def from_callable(
93
+ cls,
94
+ func: Callable[[], RunResult],
95
+ description: str = "",
96
+ ) -> Task:
97
+ """从可调用对象创建任务.
98
+
99
+ Parameters
100
+ ----------
101
+ func : Callable[[], RunResult]
102
+ 可执行函数
103
+ description : str
104
+ 任务描述
105
+
106
+ Returns
107
+ -------
108
+ Task
109
+ 任务实例
110
+ """
111
+ return cls(func=func, description=description)
112
+
113
+ @classmethod
114
+ def from_list(
115
+ cls,
116
+ cmd: list[str],
117
+ description: str = "",
118
+ ) -> Task:
119
+ """从命令列表创建任务.
120
+
121
+ Parameters
122
+ ----------
123
+ cmd : list[str]
124
+ 命令及其参数列表
125
+ description : str
126
+ 任务描述
127
+
128
+ Returns
129
+ -------
130
+ Task
131
+ 任务实例
132
+ """
133
+ return cls(cmd=cmd, description=description)