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
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""性能分析工具模块测试."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from typing import Generator, NoReturn
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from bitool.utils.profiler import (
|
|
11
|
+
_MS_PER_SEC,
|
|
12
|
+
_FuncStats,
|
|
13
|
+
_PyProfileRecord,
|
|
14
|
+
_PythonPerformanceProfiler,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(autouse=True)
|
|
19
|
+
def enable_profiling() -> Generator[None, None, None]:
|
|
20
|
+
"""启用性能分析环境变量."""
|
|
21
|
+
with patch.dict(os.environ, {"BITOOL_PROFILE": "1"}):
|
|
22
|
+
yield
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def python_profiler() -> _PythonPerformanceProfiler:
|
|
27
|
+
"""创建 Python 性能分析器实例."""
|
|
28
|
+
return _PythonPerformanceProfiler()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestPyProfileRecord:
|
|
32
|
+
"""测试性能记录数据结构."""
|
|
33
|
+
|
|
34
|
+
def test_create_record(self) -> None:
|
|
35
|
+
"""测试创建性能记录."""
|
|
36
|
+
record = _PyProfileRecord(
|
|
37
|
+
name="test_func",
|
|
38
|
+
duration_secs=1.5,
|
|
39
|
+
memory_before_mb=10.0,
|
|
40
|
+
memory_after_mb=12.5,
|
|
41
|
+
memory_delta_mb=2.5,
|
|
42
|
+
timestamp="2024-01-01 12:00:00",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert record.name == "test_func"
|
|
46
|
+
assert record.duration_secs == 1.5
|
|
47
|
+
assert record.memory_before_mb == 10.0
|
|
48
|
+
assert record.memory_after_mb == 12.5
|
|
49
|
+
assert record.memory_delta_mb == 2.5
|
|
50
|
+
assert record.timestamp == "2024-01-01 12:00:00"
|
|
51
|
+
|
|
52
|
+
def test_default_values(self) -> None:
|
|
53
|
+
"""测试默认值."""
|
|
54
|
+
record = _PyProfileRecord(name="test", duration_secs=1.0)
|
|
55
|
+
|
|
56
|
+
assert record.memory_before_mb == 0.0
|
|
57
|
+
assert record.memory_after_mb == 0.0
|
|
58
|
+
assert record.memory_delta_mb == 0.0
|
|
59
|
+
assert record.timestamp == ""
|
|
60
|
+
|
|
61
|
+
def test_record_with_zero_duration(self) -> None:
|
|
62
|
+
"""测试零耗时记录."""
|
|
63
|
+
record = _PyProfileRecord(name="instant_func", duration_secs=0.0)
|
|
64
|
+
assert record.duration_secs == 0.0
|
|
65
|
+
assert record.name == "instant_func"
|
|
66
|
+
|
|
67
|
+
def test_record_with_negative_memory(self) -> None:
|
|
68
|
+
"""测试负内存变化记录."""
|
|
69
|
+
record = _PyProfileRecord(
|
|
70
|
+
name="memory_release_func",
|
|
71
|
+
duration_secs=0.5,
|
|
72
|
+
memory_before_mb=20.0,
|
|
73
|
+
memory_after_mb=15.0,
|
|
74
|
+
memory_delta_mb=-5.0,
|
|
75
|
+
)
|
|
76
|
+
assert record.memory_delta_mb == -5.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestPythonPerformanceProfiler:
|
|
80
|
+
"""测试 Python 性能分析器实现."""
|
|
81
|
+
|
|
82
|
+
def test_start_and_stop_timer(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
83
|
+
"""测试开始和停止计时."""
|
|
84
|
+
python_profiler.start_timer("test_func")
|
|
85
|
+
time.sleep(0.01)
|
|
86
|
+
record = python_profiler.stop_timer("test_func")
|
|
87
|
+
|
|
88
|
+
assert record is not None
|
|
89
|
+
assert record.name == "test_func"
|
|
90
|
+
assert record.duration_secs >= 0.005 # 降低阈值,考虑时间精度问题
|
|
91
|
+
assert record.duration_secs < 1.0 # 应该小于 1 秒
|
|
92
|
+
|
|
93
|
+
def test_stop_nonexistent_timer(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
94
|
+
"""测试停止不存在的计时器."""
|
|
95
|
+
record = python_profiler.stop_timer("nonexistent")
|
|
96
|
+
|
|
97
|
+
assert record is None
|
|
98
|
+
|
|
99
|
+
def test_multiple_timers(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
100
|
+
"""测试多个计时器."""
|
|
101
|
+
python_profiler.start_timer("func1")
|
|
102
|
+
python_profiler.start_timer("func2")
|
|
103
|
+
time.sleep(0.01)
|
|
104
|
+
|
|
105
|
+
record1 = python_profiler.stop_timer("func1")
|
|
106
|
+
record2 = python_profiler.stop_timer("func2")
|
|
107
|
+
|
|
108
|
+
assert record1 is not None
|
|
109
|
+
assert record2 is not None
|
|
110
|
+
assert record1.name == "func1"
|
|
111
|
+
assert record2.name == "func2"
|
|
112
|
+
|
|
113
|
+
def test_get_records(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
114
|
+
"""测试获取记录."""
|
|
115
|
+
python_profiler.start_timer("func1")
|
|
116
|
+
python_profiler.stop_timer("func1")
|
|
117
|
+
python_profiler.start_timer("func2")
|
|
118
|
+
python_profiler.stop_timer("func2")
|
|
119
|
+
|
|
120
|
+
records = python_profiler.get_records()
|
|
121
|
+
|
|
122
|
+
assert len(records) == 2
|
|
123
|
+
assert records[0].name == "func1"
|
|
124
|
+
assert records[1].name == "func2"
|
|
125
|
+
|
|
126
|
+
def test_get_call_count(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
127
|
+
"""测试获取调用次数."""
|
|
128
|
+
python_profiler.start_timer("func1")
|
|
129
|
+
python_profiler.stop_timer("func1")
|
|
130
|
+
python_profiler.start_timer("func1")
|
|
131
|
+
python_profiler.stop_timer("func1")
|
|
132
|
+
python_profiler.start_timer("func2")
|
|
133
|
+
python_profiler.stop_timer("func2")
|
|
134
|
+
|
|
135
|
+
assert python_profiler.get_call_count("func1") == 2
|
|
136
|
+
assert python_profiler.get_call_count("func2") == 1
|
|
137
|
+
assert python_profiler.get_call_count("nonexistent") == 0
|
|
138
|
+
|
|
139
|
+
def test_clear(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
140
|
+
"""测试清除记录."""
|
|
141
|
+
python_profiler.start_timer("func1")
|
|
142
|
+
python_profiler.stop_timer("func1")
|
|
143
|
+
|
|
144
|
+
python_profiler.clear()
|
|
145
|
+
|
|
146
|
+
assert len(python_profiler.get_records()) == 0
|
|
147
|
+
assert python_profiler.get_call_count("func1") == 0
|
|
148
|
+
|
|
149
|
+
def test_memory_tracking(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
150
|
+
"""测试内存追踪."""
|
|
151
|
+
python_profiler.start_timer("memory_test")
|
|
152
|
+
# 分配一些内存
|
|
153
|
+
_data = list(range(10000))
|
|
154
|
+
record = python_profiler.stop_timer("memory_test")
|
|
155
|
+
|
|
156
|
+
assert record is not None
|
|
157
|
+
# 内存追踪应该工作(可能为 0 如果 tracemalloc 未启动)
|
|
158
|
+
assert isinstance(record.memory_before_mb, float)
|
|
159
|
+
assert isinstance(record.memory_after_mb, float)
|
|
160
|
+
assert isinstance(record.memory_delta_mb, float)
|
|
161
|
+
|
|
162
|
+
def test_records_are_copied(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
163
|
+
"""测试获取记录返回副本."""
|
|
164
|
+
python_profiler.start_timer("func1")
|
|
165
|
+
python_profiler.stop_timer("func1")
|
|
166
|
+
|
|
167
|
+
records1 = python_profiler.get_records()
|
|
168
|
+
records2 = python_profiler.get_records()
|
|
169
|
+
|
|
170
|
+
# 应该是不同的列表对象
|
|
171
|
+
assert records1 is not records2
|
|
172
|
+
# 但内容应该相同
|
|
173
|
+
assert len(records1) == len(records2)
|
|
174
|
+
assert records1[0].name == records2[0].name
|
|
175
|
+
|
|
176
|
+
def test_timer_overwrite(self, python_profiler: _PythonPerformanceProfiler) -> None:
|
|
177
|
+
"""测试同名计时器覆盖."""
|
|
178
|
+
python_profiler.start_timer("func1")
|
|
179
|
+
time.sleep(0.01)
|
|
180
|
+
# 再次启动同名计时器会覆盖
|
|
181
|
+
python_profiler.start_timer("func1")
|
|
182
|
+
time.sleep(0.01)
|
|
183
|
+
record = python_profiler.stop_timer("func1")
|
|
184
|
+
|
|
185
|
+
assert record is not None
|
|
186
|
+
# 应该只记录了第二次的时间
|
|
187
|
+
assert record.duration_secs >= 0.005
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class TestProfileDecorator:
|
|
191
|
+
"""测试 @profile 装饰器."""
|
|
192
|
+
|
|
193
|
+
def test_profile_disabled(self) -> None:
|
|
194
|
+
"""测试性能分析禁用时."""
|
|
195
|
+
with patch.dict(os.environ, {"BITOOL_PROFILE": "0"}, clear=True):
|
|
196
|
+
# 重新导入以应用环境变量
|
|
197
|
+
import importlib
|
|
198
|
+
|
|
199
|
+
from bitool.utils import profiler
|
|
200
|
+
|
|
201
|
+
importlib.reload(profiler)
|
|
202
|
+
|
|
203
|
+
@profiler.profile
|
|
204
|
+
def test_func() -> int:
|
|
205
|
+
return 42
|
|
206
|
+
|
|
207
|
+
result = test_func()
|
|
208
|
+
assert result == 42
|
|
209
|
+
|
|
210
|
+
def test_profile_enabled(self) -> None:
|
|
211
|
+
"""测试性能分析启用时."""
|
|
212
|
+
# 创建新的包装器实例以避免缓存问题
|
|
213
|
+
from bitool.utils.profiler import _PerformanceProfilerWrapper
|
|
214
|
+
|
|
215
|
+
wrapper = _PerformanceProfilerWrapper()
|
|
216
|
+
|
|
217
|
+
@wrapper.profile
|
|
218
|
+
def test_func() -> str:
|
|
219
|
+
time.sleep(0.01)
|
|
220
|
+
return "test_result"
|
|
221
|
+
|
|
222
|
+
result = test_func()
|
|
223
|
+
|
|
224
|
+
assert result == "test_result"
|
|
225
|
+
# 应该有记录
|
|
226
|
+
assert len(wrapper.records) > 0
|
|
227
|
+
assert wrapper.records[-1].name == "test_func"
|
|
228
|
+
|
|
229
|
+
def test_profile_with_exception(self) -> None:
|
|
230
|
+
"""测试装饰器处理异常."""
|
|
231
|
+
from bitool.utils.profiler import _PerformanceProfilerWrapper
|
|
232
|
+
|
|
233
|
+
wrapper = _PerformanceProfilerWrapper()
|
|
234
|
+
|
|
235
|
+
@wrapper.profile
|
|
236
|
+
def failing_func() -> NoReturn:
|
|
237
|
+
raise ValueError("测试异常")
|
|
238
|
+
|
|
239
|
+
with pytest.raises(ValueError, match="测试异常"):
|
|
240
|
+
failing_func()
|
|
241
|
+
|
|
242
|
+
def test_profile_preserves_function_metadata(self) -> None:
|
|
243
|
+
"""测试装饰器保留函数元数据."""
|
|
244
|
+
from bitool.utils.profiler import _PerformanceProfilerWrapper
|
|
245
|
+
|
|
246
|
+
wrapper = _PerformanceProfilerWrapper()
|
|
247
|
+
|
|
248
|
+
@wrapper.profile
|
|
249
|
+
def documented_func() -> str:
|
|
250
|
+
"""这是一个有文档的函数."""
|
|
251
|
+
return "test"
|
|
252
|
+
|
|
253
|
+
# functools.wraps 应该保留这些属性
|
|
254
|
+
assert documented_func.__name__ == "documented_func"
|
|
255
|
+
# 注意:装饰器可能会修改 __doc__,这不是必须的
|
|
256
|
+
|
|
257
|
+
def test_profile_recursive_function(self) -> None:
|
|
258
|
+
"""测试装饰器处理递归函数."""
|
|
259
|
+
from bitool.utils.profiler import _PerformanceProfilerWrapper
|
|
260
|
+
|
|
261
|
+
wrapper = _PerformanceProfilerWrapper()
|
|
262
|
+
call_count_holder = {"count": 0}
|
|
263
|
+
|
|
264
|
+
def factorial_impl(n: int) -> int:
|
|
265
|
+
"""不使用装饰器的递归实现."""
|
|
266
|
+
call_count_holder["count"] += 1
|
|
267
|
+
if n <= 1:
|
|
268
|
+
return 1
|
|
269
|
+
return n * factorial_impl(n - 1)
|
|
270
|
+
|
|
271
|
+
@wrapper.profile
|
|
272
|
+
def factorial(n: int) -> int:
|
|
273
|
+
return factorial_impl(n)
|
|
274
|
+
|
|
275
|
+
result = factorial(5)
|
|
276
|
+
assert result == 120
|
|
277
|
+
# 应该只有 1 次 profiler 记录(外层调用)
|
|
278
|
+
assert len(wrapper.records) == 1
|
|
279
|
+
|
|
280
|
+
def test_profile_nested_calls(self) -> None:
|
|
281
|
+
"""测试嵌套函数调用."""
|
|
282
|
+
from bitool.utils.profiler import _PerformanceProfilerWrapper
|
|
283
|
+
|
|
284
|
+
wrapper = _PerformanceProfilerWrapper()
|
|
285
|
+
|
|
286
|
+
@wrapper.profile
|
|
287
|
+
def inner_func() -> str:
|
|
288
|
+
time.sleep(0.01)
|
|
289
|
+
return "inner"
|
|
290
|
+
|
|
291
|
+
@wrapper.profile
|
|
292
|
+
def outer_func() -> str:
|
|
293
|
+
result = inner_func()
|
|
294
|
+
time.sleep(0.01)
|
|
295
|
+
return f"outer_{result}"
|
|
296
|
+
|
|
297
|
+
result = outer_func()
|
|
298
|
+
|
|
299
|
+
assert result == "outer_inner"
|
|
300
|
+
# 应该有两个函数的记录
|
|
301
|
+
assert len(wrapper.records) >= 2
|
|
302
|
+
|
|
303
|
+
def test_profile_with_args(self) -> None:
|
|
304
|
+
"""测试装饰器处理带参数的函数."""
|
|
305
|
+
from bitool.utils.profiler import _PerformanceProfilerWrapper
|
|
306
|
+
|
|
307
|
+
wrapper = _PerformanceProfilerWrapper()
|
|
308
|
+
|
|
309
|
+
@wrapper.profile
|
|
310
|
+
def add(a: int, b: int) -> int:
|
|
311
|
+
return a + b
|
|
312
|
+
|
|
313
|
+
result = add(3, 5)
|
|
314
|
+
|
|
315
|
+
assert result == 8
|
|
316
|
+
assert len(wrapper.records) > 0
|
|
317
|
+
|
|
318
|
+
def test_profile_with_kwargs(self) -> None:
|
|
319
|
+
"""测试装饰器处理带关键字参数的函数."""
|
|
320
|
+
from bitool.utils.profiler import _PerformanceProfilerWrapper
|
|
321
|
+
|
|
322
|
+
wrapper = _PerformanceProfilerWrapper()
|
|
323
|
+
|
|
324
|
+
@wrapper.profile
|
|
325
|
+
def greet(name: str, greeting: str = "Hello") -> str:
|
|
326
|
+
return f"{greeting}, {name}!"
|
|
327
|
+
|
|
328
|
+
result = greet("World", greeting="Hi")
|
|
329
|
+
|
|
330
|
+
assert result == "Hi, World!"
|
|
331
|
+
assert len(wrapper.records) > 0
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class TestFuncStats:
|
|
335
|
+
"""测试函数统计数据."""
|
|
336
|
+
|
|
337
|
+
def test_default_values(self) -> None:
|
|
338
|
+
"""测试默认值."""
|
|
339
|
+
stats = _FuncStats()
|
|
340
|
+
|
|
341
|
+
assert stats.total_time == 0.0
|
|
342
|
+
assert stats.call_count == 0
|
|
343
|
+
assert stats.memory_delta == 0.0
|
|
344
|
+
|
|
345
|
+
def test_accumulation(self) -> None:
|
|
346
|
+
"""测试数据累加."""
|
|
347
|
+
stats = _FuncStats()
|
|
348
|
+
stats.total_time += 1.0
|
|
349
|
+
stats.call_count += 1
|
|
350
|
+
stats.memory_delta += 0.5
|
|
351
|
+
|
|
352
|
+
assert stats.total_time == 1.0
|
|
353
|
+
assert stats.call_count == 1
|
|
354
|
+
assert stats.memory_delta == 0.5
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestProfilerConstants:
|
|
358
|
+
"""测试常量定义."""
|
|
359
|
+
|
|
360
|
+
def test_time_constants(self) -> None:
|
|
361
|
+
"""测试时间相关常量."""
|
|
362
|
+
assert _MS_PER_SEC == 1000
|
|
363
|
+
|
|
364
|
+
def test_display_constants_types(self) -> None:
|
|
365
|
+
"""测试显示常量类型."""
|
|
366
|
+
from bitool.utils.profiler import (
|
|
367
|
+
_COL_CALL_COUNT_STYLE,
|
|
368
|
+
_COL_DURATION_STYLE,
|
|
369
|
+
_COL_FUNC_NAME_STYLE,
|
|
370
|
+
_COL_MEMORY_STYLE,
|
|
371
|
+
_COL_PERCENTAGE_STYLE,
|
|
372
|
+
_CONSOLE_WIDTH,
|
|
373
|
+
_TABLE_BORDER_STYLE,
|
|
374
|
+
_TABLE_HEADER_STYLE,
|
|
375
|
+
_TABLE_TITLE_STYLE,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
assert isinstance(_CONSOLE_WIDTH, int)
|
|
379
|
+
assert isinstance(_TABLE_TITLE_STYLE, str)
|
|
380
|
+
assert isinstance(_TABLE_BORDER_STYLE, str)
|
|
381
|
+
assert isinstance(_TABLE_HEADER_STYLE, str)
|
|
382
|
+
assert isinstance(_COL_FUNC_NAME_STYLE, str)
|
|
383
|
+
assert isinstance(_COL_DURATION_STYLE, str)
|
|
384
|
+
assert isinstance(_COL_PERCENTAGE_STYLE, str)
|
|
385
|
+
assert isinstance(_COL_CALL_COUNT_STYLE, str)
|
|
386
|
+
assert isinstance(_COL_MEMORY_STYLE, str)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class TestProfilerWrapper:
|
|
390
|
+
"""测试性能分析器包装器."""
|
|
391
|
+
|
|
392
|
+
def test_clear_resets_state(self) -> None:
|
|
393
|
+
"""测试清除操作重置状态."""
|
|
394
|
+
from bitool.utils.profiler import _profiler_wrapper
|
|
395
|
+
|
|
396
|
+
# 添加一些记录
|
|
397
|
+
_profiler_wrapper.profiler.start_timer("test")
|
|
398
|
+
_profiler_wrapper.profiler.stop_timer("test")
|
|
399
|
+
|
|
400
|
+
# 清除
|
|
401
|
+
_profiler_wrapper.clear()
|
|
402
|
+
|
|
403
|
+
# 记录应该被清除
|
|
404
|
+
# 注意:cached_property 会缓存,所以需要重新访问
|
|
405
|
+
assert len(_profiler_wrapper.profiler.get_records()) == 0
|
|
406
|
+
|
|
407
|
+
def test_cached_properties(self) -> None:
|
|
408
|
+
"""测试缓存属性."""
|
|
409
|
+
from bitool.utils.profiler import _PerformanceProfilerWrapper
|
|
410
|
+
|
|
411
|
+
wrapper = _PerformanceProfilerWrapper()
|
|
412
|
+
|
|
413
|
+
# 添加记录
|
|
414
|
+
wrapper.profiler.start_timer("func1")
|
|
415
|
+
time.sleep(0.01)
|
|
416
|
+
wrapper.profiler.stop_timer("func1")
|
|
417
|
+
|
|
418
|
+
# 访问缓存属性
|
|
419
|
+
records = wrapper.records
|
|
420
|
+
total_time = wrapper.total_time
|
|
421
|
+
func_stats = wrapper.func_stats
|
|
422
|
+
sorted_funcs = wrapper.sorted_funcs
|
|
423
|
+
|
|
424
|
+
assert isinstance(records, list)
|
|
425
|
+
assert isinstance(total_time, (int, float)) # 可能是 0 或 float
|
|
426
|
+
assert isinstance(func_stats, dict)
|
|
427
|
+
assert isinstance(sorted_funcs, list)
|
|
428
|
+
|
|
429
|
+
# 验证排序
|
|
430
|
+
if len(sorted_funcs) > 1:
|
|
431
|
+
for i in range(len(sorted_funcs) - 1):
|
|
432
|
+
assert sorted_funcs[i][1].total_time >= sorted_funcs[i + 1][1].total_time
|
|
433
|
+
|
|
434
|
+
def test_format_duration(self) -> None:
|
|
435
|
+
"""测试时间格式化."""
|
|
436
|
+
from bitool.utils.profiler import _profiler_wrapper
|
|
437
|
+
|
|
438
|
+
# 测试秒级
|
|
439
|
+
assert _profiler_wrapper._format_duration(1.5) == "1.50s"
|
|
440
|
+
assert _profiler_wrapper._format_duration(2.0) == "2.00s"
|
|
441
|
+
assert _profiler_wrapper._format_duration(1.0) == "1.00s"
|
|
442
|
+
|
|
443
|
+
# 测试毫秒级
|
|
444
|
+
assert _profiler_wrapper._format_duration(0.5) == "500ms"
|
|
445
|
+
assert _profiler_wrapper._format_duration(0.001) == "1ms"
|
|
446
|
+
assert _profiler_wrapper._format_duration(0.999) == "999ms"
|
|
447
|
+
|
|
448
|
+
# 测试微秒级
|
|
449
|
+
assert _profiler_wrapper._format_duration(0.0005) == "500μs"
|
|
450
|
+
assert _profiler_wrapper._format_duration(0.0001) == "100μs"
|
|
451
|
+
assert _profiler_wrapper._format_duration(0.0) == "0μs"
|
|
452
|
+
|
|
453
|
+
def test_format_memory(self) -> None:
|
|
454
|
+
"""测试内存格式化."""
|
|
455
|
+
from bitool.utils.profiler import _profiler_wrapper
|
|
456
|
+
|
|
457
|
+
# 测试显著变化
|
|
458
|
+
assert _profiler_wrapper._format_memory(1.5) == "+1.50MB"
|
|
459
|
+
assert _profiler_wrapper._format_memory(-0.5) == "-0.50MB"
|
|
460
|
+
assert _profiler_wrapper._format_memory(0.0) == "~0MB" # 0 在阈值内
|
|
461
|
+
|
|
462
|
+
# 测试阈值内
|
|
463
|
+
assert _profiler_wrapper._format_memory(0.005) == "~0MB"
|
|
464
|
+
assert _profiler_wrapper._format_memory(-0.005) == "~0MB"
|
|
465
|
+
assert _profiler_wrapper._format_memory(0.009) == "~0MB" # 略小于阈值
|
|
466
|
+
assert _profiler_wrapper._format_memory(0.011) == "+0.01MB" # 略大于阈值
|
|
467
|
+
|
|
468
|
+
def test_truncate_func_name(self) -> None:
|
|
469
|
+
"""测试函数名截断."""
|
|
470
|
+
from bitool.utils.profiler import _profiler_wrapper
|
|
471
|
+
|
|
472
|
+
# 短名称不截断
|
|
473
|
+
assert _profiler_wrapper._truncate_func_name("short_name") == "short_name"
|
|
474
|
+
assert _profiler_wrapper._truncate_func_name("exact_28_chars_long_name_") == "exact_28_chars_long_name_"
|
|
475
|
+
|
|
476
|
+
# 长名称截断
|
|
477
|
+
long_name = "very_very_very_very_long_function_name"
|
|
478
|
+
truncated = _profiler_wrapper._truncate_func_name(long_name)
|
|
479
|
+
assert len(truncated) == 28 # 25 + 3 for "..."
|
|
480
|
+
assert truncated.endswith("...")
|
|
481
|
+
assert truncated == "very_very_very_very_long_..." # 修正期望值
|
|
482
|
+
|
|
483
|
+
# 边界情况
|
|
484
|
+
name_29_chars = "a" * 29
|
|
485
|
+
truncated_29 = _profiler_wrapper._truncate_func_name(name_29_chars)
|
|
486
|
+
assert len(truncated_29) == 28
|
|
487
|
+
assert truncated_29 == "a" * 25 + "..."
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bitool
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Bitool - Rust + Python 混合架构工具集
|
|
5
|
+
Project-URL: Homepage, https://gitee.com/gooker_young/bitool
|
|
6
|
+
Project-URL: Issues, https://gitee.com/gooker_young/bitool/issues
|
|
7
|
+
Author-email: gooker_young <gooker_young@qq.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Programming Language :: Rust
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Requires-Dist: psutil>=7.2.2
|
|
19
|
+
Requires-Dist: pymupdf>=1.24.11
|
|
20
|
+
Requires-Dist: rich>=14.3.4
|
|
21
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
22
|
+
Requires-Dist: tomli>=2.4.1
|
|
23
|
+
Requires-Dist: typing-extensions>=4.13.2
|
|
24
|
+
Provides-Extra: build
|
|
25
|
+
Requires-Dist: maturin<2.0,>=1.0; extra == 'build'
|
|
26
|
+
Requires-Dist: pip>=25.0.1; extra == 'build'
|
|
27
|
+
Requires-Dist: prek>=0.3.3; extra == 'build'
|
|
28
|
+
Requires-Dist: ruff>=0.15.1; extra == 'build'
|
|
29
|
+
Requires-Dist: setuptools>=75.3.4; extra == 'build'
|
|
30
|
+
Requires-Dist: tox-uv>=1.13.1; extra == 'build'
|
|
31
|
+
Requires-Dist: tox>=4.24.1; extra == 'build'
|
|
32
|
+
Requires-Dist: wheel>=0.45.1; extra == 'build'
|
|
33
|
+
Provides-Extra: core
|
|
34
|
+
Provides-Extra: docs
|
|
35
|
+
Requires-Dist: sphinx-autobuild>=2021.3.14; extra == 'docs'
|
|
36
|
+
Requires-Dist: sphinx-rtd-theme>=1.5.2; extra == 'docs'
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: bandit>=1.7.10; extra == 'test'
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'test'
|
|
40
|
+
Requires-Dist: pytest-benchmark>=4.0.0; extra == 'test'
|
|
41
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
|
|
42
|
+
Requires-Dist: pytest-html>=4.1.1; extra == 'test'
|
|
43
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == 'test'
|
|
44
|
+
Requires-Dist: pytest-timeout>=2.4.0; extra == 'test'
|
|
45
|
+
Requires-Dist: pytest-xdist>=3.6.1; extra == 'test'
|
|
46
|
+
Requires-Dist: pytest>=8.3.4; extra == 'test'
|
|
47
|
+
Provides-Extra: typecheck
|
|
48
|
+
Requires-Dist: ty>=0.0.24; extra == 'typecheck'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# Bitool
|
|
52
|
+
|
|
53
|
+
基于 Maturin 的 Rust + Python 混合架构工具集,将性能瓶颈和共性部分使用 Rust 实现,接口和逻辑部分使用 Python 实现。
|
|
54
|
+
|
|
55
|
+
## 特性
|
|
56
|
+
|
|
57
|
+
- **高性能**:核心计算和 I/O 操作使用 Rust 实现
|
|
58
|
+
- **易用性**:Python 提供简洁的 API 接口
|
|
59
|
+
- **可扩展**:插件系统支持功能扩展
|
|
60
|
+
- **跨平台**:支持 Windows、Linux、macOS
|
|
61
|
+
|
|
62
|
+
## 安装
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install bitool
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 开发
|
|
69
|
+
|
|
70
|
+
### 快速开始(推荐)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# 安装开发环境(自动构建 Rust + Python 双包)
|
|
74
|
+
pymake ia
|
|
75
|
+
|
|
76
|
+
# 验证安装
|
|
77
|
+
pymake t
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 常用命令
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 构建所有包(Rust + Python)
|
|
84
|
+
pymake ba
|
|
85
|
+
|
|
86
|
+
# 代码格式化与检查
|
|
87
|
+
pymake lint
|
|
88
|
+
|
|
89
|
+
# 运行测试
|
|
90
|
+
pymake t
|
|
91
|
+
|
|
92
|
+
# 清理所有构建产物
|
|
93
|
+
pymake ca
|
|
94
|
+
|
|
95
|
+
# 查看完整帮助
|
|
96
|
+
pymake help
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 手动构建(可选)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# 构建 Rust 核心模块
|
|
103
|
+
cd bitool-core && maturin develop
|
|
104
|
+
|
|
105
|
+
# 构建 Python 主包
|
|
106
|
+
cd .. && pip install -e .
|
|
107
|
+
|
|
108
|
+
# 构建发布包
|
|
109
|
+
cd bitool-core && maturin build -r
|
|
110
|
+
cd .. && python -m build
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> 📖 详细文档:[PyMake 使用指南](docs/PYMAKE_GUIDE.md) | [双包构建指南](docs/DUAL_PACKAGE_BUILD.md)
|
|
114
|
+
|
|
115
|
+
## 项目结构
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
bitool/
|
|
119
|
+
├── bitool-core/ # Rust 核心模块
|
|
120
|
+
│ ├── src/ # Rust 源代码
|
|
121
|
+
│ │ ├── core/ # 核心功能模块
|
|
122
|
+
│ │ ├── plugins/ # 插件系统核心
|
|
123
|
+
│ │ └── python/ # Python 绑定
|
|
124
|
+
│ ├── Cargo.toml # Rust 依赖配置
|
|
125
|
+
│ └── pyproject.toml # Maturin 构建配置
|
|
126
|
+
├── src/bitool/ # Python 主包
|
|
127
|
+
│ ├── core/ # 核心业务逻辑
|
|
128
|
+
│ ├── cmd/ # 命令系统
|
|
129
|
+
│ ├── scripts/ # CLI 工具集
|
|
130
|
+
│ ├── utils/ # 工具模块
|
|
131
|
+
│ └── cli/ # 命令行入口
|
|
132
|
+
├── docs/ # 项目文档
|
|
133
|
+
├── pyproject.toml # Python 包配置
|
|
134
|
+
├── Makefile # 构建脚本(可选)
|
|
135
|
+
└── build_all.py # Python 构建脚本
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 使用示例
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from bitool import FileOps, TextProcessor
|
|
142
|
+
|
|
143
|
+
# 文件操作
|
|
144
|
+
file_ops = FileOps()
|
|
145
|
+
files = file_ops.scan_directory("/path/to/dir")
|
|
146
|
+
|
|
147
|
+
# 文本处理
|
|
148
|
+
processor = TextProcessor()
|
|
149
|
+
result = processor.regex_search(pattern, text)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 许可证
|
|
153
|
+
|
|
154
|
+
MIT License
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
bitool/__init__.py,sha256=_BTVmBvdG90-R1OmF_sP_uWB3JnLNygYV5sztqAkUsY,706
|
|
2
|
+
bitool/consts.py,sha256=ylKOKSMZCyjtSkHZoa4XvJfgeVgwdcDaXLdn3omTh3s,497
|
|
3
|
+
bitool/types.py,sha256=O9Oj5CvSIfmZkMwazXTYWxIOMaO5E0LT4p-9_HHseaU,203
|
|
4
|
+
bitool/cmd/__init__.py,sha256=iaw0W9n2bIOXqLo7JhxNxJsMfo7nPoZkmfQvmeO5uIc,1640
|
|
5
|
+
bitool/cmd/_base.py,sha256=0PAcMm__o9PMuz5RPdqmbsC7B7NQtYFVtQZKMgmclhQ,3416
|
|
6
|
+
bitool/cmd/_condition.py,sha256=shXfkfPcs1QwY-tZEhtOVAjQfuwmK2CPci0TykgiZFs,1654
|
|
7
|
+
bitool/cmd/_scheduler.py,sha256=ndn0vXyB4cOQKBz6qm953M6JWCHAqzo-_pl3UG3O4jg,19816
|
|
8
|
+
bitool/cmd/env.py,sha256=986OP91TmTlhKMDf2okr2fP_ALUUB-ACALM6sF9K0Qc,15373
|
|
9
|
+
bitool/cmd/git.py,sha256=J8gvgEOq-hQk2PQAJ1lN9E-2YFlo-ZQoov1J781TAck,3461
|
|
10
|
+
bitool/cmd/io.py,sha256=GA0KF-UY8nKj-PJLv0iK6rKAfcCGCDnU0S4Ie2Bip4w,7716
|
|
11
|
+
bitool/cmd/pdf.py,sha256=noRg20dsduzXr84acAfQ72nN0wtI8HHVW-4Yf6pqFP8,12262
|
|
12
|
+
bitool/cmd/run.py,sha256=61fyZ77fYredwKLO5U6Y5-dvlhVEgFtv55kb1Aum_Ow,10454
|
|
13
|
+
bitool/cmd/toml.py,sha256=Ud02H7d3yEnNJQt5dVU7Uuf6hhXMUpWLZ9mWrSSHZp8,7249
|
|
14
|
+
bitool/cmd/version.py,sha256=cuPUu-Cz0ORKu3sVqHhWoRfCYZagzbhE_2PFlhtgBN4,21334
|
|
15
|
+
bitool/core/__init__.py,sha256=62w3qCwQ7ncMVUCTAFelcHEitXjOKLuHIYKpdB1K2hk,192
|
|
16
|
+
bitool/core/app.py,sha256=jcUTavzgL71c8h5rfsxKVNA67552jiUIvu1pVMi_M1Y,4406
|
|
17
|
+
bitool/core/commands.py,sha256=sxwq6Bac6nUan4uEU0hwtfYbkV2L_4kLjaJN2AsAcUw,5134
|
|
18
|
+
bitool/core/config.py,sha256=wwR08-EEYOtnodK_WB1txWkLKmqd4OJoX7XRT-N26YQ,23311
|
|
19
|
+
bitool/core/env.py,sha256=jkEUTaiJxXPCUJfjU_9ZXF6XVsRnYZMwAkoKL2J92Gs,324
|
|
20
|
+
bitool/core/logger.py,sha256=6JS5YezKVp8_2f3-mgGBqgCq8r5Fq6PoD8sM8ditl5c,8531
|
|
21
|
+
bitool/core/plugin.py,sha256=WZ2squ0gVSjLFwpLU9JKVLaZGf0B3K13HS1C2UtqKpg,3594
|
|
22
|
+
bitool/core/workspace.py,sha256=dhLg0IZtMOkQ0zfEBa6PDUwju9k_hx_U80b5N2KN-WU,1924
|
|
23
|
+
bitool/models/__init__.py,sha256=26KXRuM5SPWi_WJH_D4dJ1D5ri7yJty6LsC4sxYaP8g,113
|
|
24
|
+
bitool/models/version.py,sha256=vwm1dCLKFGW7KNU3scLU8RNPfvlI1FxRP1qaBgpnbis,5054
|
|
25
|
+
bitool/scripts/__init__.py,sha256=mzcYTQ2M2-KyXwrgbp2-j2vhCS6HTF26PyIJl6fS2lc,29
|
|
26
|
+
bitool/scripts/bumpversion.py,sha256=GiRUA4I6ucThrIIMDAzMd-evqExnoCbulnVPpUUAx1E,6107
|
|
27
|
+
bitool/scripts/clearscreen.py,sha256=Hp1WbburnSUTqJaIf7WtxufjwFFNY-9LiCJpBvPh0Ic,899
|
|
28
|
+
bitool/scripts/envpy.py,sha256=b7Qz3uBqgnfN-KHVF-03jvCCCdjBFko9w6cBRwj1yt0,6485
|
|
29
|
+
bitool/scripts/envrs.py,sha256=AgpaDc_0Aqp4nyZcaAnvyH6ECnuu7TYYETD7pVsfhRs,3487
|
|
30
|
+
bitool/scripts/filedate.py,sha256=_dPjFKSS5sbIJYQzfP-jRPKBTZJCAJhDEHq7ANOS1rw,7799
|
|
31
|
+
bitool/scripts/filelevel.py,sha256=jTOSvYC0xfYTPiWD5A1z7PtSBGio64qgqiE5DQ1zmJo,6181
|
|
32
|
+
bitool/scripts/gittool.py,sha256=hOSeNxxTfYQJh2m30Sq3Rz3N6qqQdPnw4nlaMw6YYRg,5494
|
|
33
|
+
bitool/scripts/img2pdf.py,sha256=m2jABrwAUShhzDym3CKDwom1stSn4CRiqFH_Zfsgmfc,4741
|
|
34
|
+
bitool/scripts/pdf2img.py,sha256=ew9cSfYmDuyz-qeAeRrzSU2CwUp7_AYna64CPNwcvJ0,4415
|
|
35
|
+
bitool/scripts/piptool.py,sha256=BXwIPSYqlPf2dPPdfajyIb43FR2xZ4sekwXkjaTtS1o,4062
|
|
36
|
+
bitool/scripts/pymake.py,sha256=XNW9Ny8fhX6fh6UMcyUlylxfdjc6D3qUHW4w-7tUzko,11401
|
|
37
|
+
bitool/scripts/sshcopyid.py,sha256=DdggEUD9GUBJXEpXbKPBEcqL7QZEli5BEXtn9LU9aHw,15524
|
|
38
|
+
bitool/scripts/taskkill.py,sha256=6jp5M0ZDB__zxD9fLWp0DxyFaGlM-PjUfwSpBfEAmH4,12260
|
|
39
|
+
bitool/scripts/which.py,sha256=N7PP0bh2PpF41XcrKkIQ6YIhH-PwOit_Fmo2Ayf49J8,5285
|
|
40
|
+
bitool/utils/__init__.py,sha256=32lxkAm_Cnjji36ncMijzo4MRfoFbDpCblNfzZ_zXJs,188
|
|
41
|
+
bitool/utils/cli_parser.py,sha256=XiZZi1kTPnqlv0dzo1q-CXaUnoh0oZgmNZm1sS2Iai4,13693
|
|
42
|
+
bitool/utils/executor.py,sha256=ErPqApPlCR7a1VKfJ1Qlf2YZ-d3E-KcysGUhzPfCRlo,30277
|
|
43
|
+
bitool/utils/profiler.py,sha256=v9rKKsqgG1DYcH3D9sx_EasJzJZnAfzJIlNUWlIPV1M,13368
|
|
44
|
+
bitool/utils/task.py,sha256=garkjLmcQnGfDsLIki_g5oAhKCwBcFQ6mFd_qveQ75s,3579
|
|
45
|
+
bitool/utils/task_group.py,sha256=Ri5JCXOeaMlckhoucIvjNYB2OXLgfbHdWhnIloRqpB4,23581
|
|
46
|
+
bitool/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
|
+
bitool/utils/tests/test_profiler.py,sha256=oGeRO0L4z7_aD5AzvtzSJFTLiQPKsyZ2ZnccHnyKHtA,17160
|
|
48
|
+
bitool-0.1.2.dist-info/METADATA,sha256=e18xbVLf4WvtAAfESiX1asEe2TyeudYkDZFssh44xzw,4465
|
|
49
|
+
bitool-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
50
|
+
bitool-0.1.2.dist-info/entry_points.txt,sha256=Ulq4kMZtxfqYti8h3J11WKvyp01JK8CGFRkTdH-ZnRM,545
|
|
51
|
+
bitool-0.1.2.dist-info/RECORD,,
|