kdtest-pw 2.0.0__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.
- kdtest_pw/__init__.py +50 -0
- kdtest_pw/action/__init__.py +7 -0
- kdtest_pw/action/base_keyword.py +292 -0
- kdtest_pw/action/element_plus/__init__.py +23 -0
- kdtest_pw/action/element_plus/el_cascader.py +263 -0
- kdtest_pw/action/element_plus/el_datepicker.py +324 -0
- kdtest_pw/action/element_plus/el_dialog.py +317 -0
- kdtest_pw/action/element_plus/el_form.py +443 -0
- kdtest_pw/action/element_plus/el_menu.py +456 -0
- kdtest_pw/action/element_plus/el_select.py +268 -0
- kdtest_pw/action/element_plus/el_table.py +442 -0
- kdtest_pw/action/element_plus/el_tree.py +364 -0
- kdtest_pw/action/element_plus/el_upload.py +313 -0
- kdtest_pw/action/key_retrieval.py +311 -0
- kdtest_pw/action/page_action.py +1129 -0
- kdtest_pw/api/__init__.py +6 -0
- kdtest_pw/api/api_keyword.py +251 -0
- kdtest_pw/api/request_handler.py +232 -0
- kdtest_pw/cases/__init__.py +6 -0
- kdtest_pw/cases/case_collector.py +182 -0
- kdtest_pw/cases/case_executor.py +359 -0
- kdtest_pw/cases/read/__init__.py +6 -0
- kdtest_pw/cases/read/cell_handler.py +305 -0
- kdtest_pw/cases/read/excel_reader.py +223 -0
- kdtest_pw/cli/__init__.py +5 -0
- kdtest_pw/cli/run.py +318 -0
- kdtest_pw/common.py +106 -0
- kdtest_pw/core/__init__.py +7 -0
- kdtest_pw/core/browser_manager.py +196 -0
- kdtest_pw/core/config_loader.py +235 -0
- kdtest_pw/core/page_context.py +228 -0
- kdtest_pw/data/__init__.py +5 -0
- kdtest_pw/data/init_data.py +105 -0
- kdtest_pw/data/static/elementData.yaml +59 -0
- kdtest_pw/data/static/parameters.json +24 -0
- kdtest_pw/plugins/__init__.py +6 -0
- kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
- kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
- kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
- kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
- kdtest_pw/plugins/plugin_base.py +180 -0
- kdtest_pw/plugins/plugin_loader.py +260 -0
- kdtest_pw/product.py +5 -0
- kdtest_pw/reference.py +99 -0
- kdtest_pw/utils/__init__.py +13 -0
- kdtest_pw/utils/built_in_function.py +376 -0
- kdtest_pw/utils/decorator.py +211 -0
- kdtest_pw/utils/log/__init__.py +6 -0
- kdtest_pw/utils/log/html_report.py +336 -0
- kdtest_pw/utils/log/logger.py +123 -0
- kdtest_pw/utils/public_script.py +366 -0
- kdtest_pw-2.0.0.dist-info/METADATA +169 -0
- kdtest_pw-2.0.0.dist-info/RECORD +57 -0
- kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
- kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
- kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
- kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""内置函数工具类"""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import string
|
|
5
|
+
import uuid
|
|
6
|
+
import time
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Any, Optional, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BuiltInFunction:
|
|
13
|
+
"""内置函数工具类
|
|
14
|
+
|
|
15
|
+
提供测试中常用的工具函数。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# ==================== 日期时间函数 ====================
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def today(fmt: str = '%Y-%m-%d', offset: int = 0) -> str:
|
|
22
|
+
"""获取今天日期
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
fmt: 日期格式
|
|
26
|
+
offset: 天数偏移
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: 格式化的日期
|
|
30
|
+
"""
|
|
31
|
+
date = datetime.now() + timedelta(days=offset)
|
|
32
|
+
return date.strftime(fmt)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def now(fmt: str = '%Y-%m-%d %H:%M:%S') -> str:
|
|
36
|
+
"""获取当前时间
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
fmt: 时间格式
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
str: 格式化的时间
|
|
43
|
+
"""
|
|
44
|
+
return datetime.now().strftime(fmt)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def timestamp(unit: str = 'ms') -> int:
|
|
48
|
+
"""获取时间戳
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
unit: 单位 (s, ms)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
int: 时间戳
|
|
55
|
+
"""
|
|
56
|
+
ts = time.time()
|
|
57
|
+
if unit == 'ms':
|
|
58
|
+
return int(ts * 1000)
|
|
59
|
+
return int(ts)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def date_add(
|
|
63
|
+
date_str: str,
|
|
64
|
+
days: int = 0,
|
|
65
|
+
hours: int = 0,
|
|
66
|
+
minutes: int = 0,
|
|
67
|
+
fmt: str = '%Y-%m-%d'
|
|
68
|
+
) -> str:
|
|
69
|
+
"""日期加减
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
date_str: 日期字符串
|
|
73
|
+
days: 天数
|
|
74
|
+
hours: 小时
|
|
75
|
+
minutes: 分钟
|
|
76
|
+
fmt: 日期格式
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
str: 计算后的日期
|
|
80
|
+
"""
|
|
81
|
+
date = datetime.strptime(date_str, fmt)
|
|
82
|
+
date += timedelta(days=days, hours=hours, minutes=minutes)
|
|
83
|
+
return date.strftime(fmt)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def date_diff(date1: str, date2: str, fmt: str = '%Y-%m-%d') -> int:
|
|
87
|
+
"""计算日期差
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
date1: 日期1
|
|
91
|
+
date2: 日期2
|
|
92
|
+
fmt: 日期格式
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
int: 相差天数
|
|
96
|
+
"""
|
|
97
|
+
d1 = datetime.strptime(date1, fmt)
|
|
98
|
+
d2 = datetime.strptime(date2, fmt)
|
|
99
|
+
return (d1 - d2).days
|
|
100
|
+
|
|
101
|
+
# ==================== 随机数函数 ====================
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def random_int(min_val: int = 0, max_val: int = 100) -> int:
|
|
105
|
+
"""生成随机整数
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
min_val: 最小值
|
|
109
|
+
max_val: 最大值
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
int: 随机整数
|
|
113
|
+
"""
|
|
114
|
+
return random.randint(min_val, max_val)
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def random_float(min_val: float = 0, max_val: float = 1.0, precision: int = 2) -> float:
|
|
118
|
+
"""生成随机浮点数
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
min_val: 最小值
|
|
122
|
+
max_val: 最大值
|
|
123
|
+
precision: 小数位数
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
float: 随机浮点数
|
|
127
|
+
"""
|
|
128
|
+
return round(random.uniform(min_val, max_val), precision)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def random_string(length: int = 8, chars: str = 'alphanumeric') -> str:
|
|
132
|
+
"""生成随机字符串
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
length: 长度
|
|
136
|
+
chars: 字符类型
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
str: 随机字符串
|
|
140
|
+
"""
|
|
141
|
+
char_map = {
|
|
142
|
+
'alpha': string.ascii_letters,
|
|
143
|
+
'numeric': string.digits,
|
|
144
|
+
'alphanumeric': string.ascii_letters + string.digits,
|
|
145
|
+
'lower': string.ascii_lowercase,
|
|
146
|
+
'upper': string.ascii_uppercase,
|
|
147
|
+
'hex': string.hexdigits.lower(),
|
|
148
|
+
}
|
|
149
|
+
charset = char_map.get(chars, string.ascii_letters + string.digits)
|
|
150
|
+
return ''.join(random.choices(charset, k=length))
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def random_phone(prefix: str = '1') -> str:
|
|
154
|
+
"""生成随机手机号
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
prefix: 号码前缀
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
str: 随机手机号
|
|
161
|
+
"""
|
|
162
|
+
prefixes = ['13', '14', '15', '16', '17', '18', '19']
|
|
163
|
+
phone_prefix = random.choice(prefixes) if prefix == '1' else prefix
|
|
164
|
+
return phone_prefix + ''.join([str(random.randint(0, 9)) for _ in range(9)])
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def random_email(domain: str = 'test.com') -> str:
|
|
168
|
+
"""生成随机邮箱
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
domain: 邮箱域名
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
str: 随机邮箱
|
|
175
|
+
"""
|
|
176
|
+
username = BuiltInFunction.random_string(8, 'lower')
|
|
177
|
+
return f"{username}@{domain}"
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def random_name(gender: str = 'random') -> str:
|
|
181
|
+
"""生成随机中文姓名
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
gender: 性别 (male, female, random)
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
str: 随机姓名
|
|
188
|
+
"""
|
|
189
|
+
surnames = ['张', '王', '李', '赵', '陈', '刘', '杨', '黄', '周', '吴']
|
|
190
|
+
male_names = ['伟', '强', '磊', '军', '洋', '勇', '杰', '涛', '明', '超']
|
|
191
|
+
female_names = ['芳', '娟', '敏', '静', '丽', '娜', '秀', '玲', '艳', '红']
|
|
192
|
+
|
|
193
|
+
surname = random.choice(surnames)
|
|
194
|
+
|
|
195
|
+
if gender == 'male':
|
|
196
|
+
name = random.choice(male_names)
|
|
197
|
+
elif gender == 'female':
|
|
198
|
+
name = random.choice(female_names)
|
|
199
|
+
else:
|
|
200
|
+
name = random.choice(male_names + female_names)
|
|
201
|
+
|
|
202
|
+
# 随机添加双字名
|
|
203
|
+
if random.random() > 0.5:
|
|
204
|
+
name += random.choice(male_names + female_names)
|
|
205
|
+
|
|
206
|
+
return surname + name
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def random_id_card() -> str:
|
|
210
|
+
"""生成随机身份证号
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
str: 随机身份证号 (模拟,非真实)
|
|
214
|
+
"""
|
|
215
|
+
# 地区码
|
|
216
|
+
area_code = random.choice(['110101', '310101', '440101', '330101', '320101'])
|
|
217
|
+
|
|
218
|
+
# 出生日期
|
|
219
|
+
year = random.randint(1970, 2000)
|
|
220
|
+
month = random.randint(1, 12)
|
|
221
|
+
day = random.randint(1, 28)
|
|
222
|
+
birth = f'{year}{month:02d}{day:02d}'
|
|
223
|
+
|
|
224
|
+
# 顺序码
|
|
225
|
+
seq = f'{random.randint(0, 999):03d}'
|
|
226
|
+
|
|
227
|
+
# 校验码 (简化)
|
|
228
|
+
check = random.choice('0123456789X')
|
|
229
|
+
|
|
230
|
+
return area_code + birth + seq + check
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def uuid4() -> str:
|
|
234
|
+
"""生成 UUID
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
str: UUID 字符串
|
|
238
|
+
"""
|
|
239
|
+
return str(uuid.uuid4())
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def uuid_short() -> str:
|
|
243
|
+
"""生成短 UUID (去掉横线)
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
str: 短 UUID
|
|
247
|
+
"""
|
|
248
|
+
return uuid.uuid4().hex
|
|
249
|
+
|
|
250
|
+
# ==================== 字符串函数 ====================
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def substring(text: str, start: int, end: int = None) -> str:
|
|
254
|
+
"""截取子字符串
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
text: 原字符串
|
|
258
|
+
start: 起始位置
|
|
259
|
+
end: 结束位置
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
str: 子字符串
|
|
263
|
+
"""
|
|
264
|
+
return text[start:end]
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def replace(text: str, old: str, new: str) -> str:
|
|
268
|
+
"""替换字符串
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
text: 原字符串
|
|
272
|
+
old: 被替换的内容
|
|
273
|
+
new: 新内容
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
str: 替换后的字符串
|
|
277
|
+
"""
|
|
278
|
+
return text.replace(old, new)
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def regex_match(text: str, pattern: str) -> Optional[str]:
|
|
282
|
+
"""正则匹配
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
text: 原字符串
|
|
286
|
+
pattern: 正则表达式
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
str: 匹配的内容
|
|
290
|
+
"""
|
|
291
|
+
match = re.search(pattern, text)
|
|
292
|
+
return match.group(0) if match else None
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def regex_find_all(text: str, pattern: str) -> List[str]:
|
|
296
|
+
"""正则查找所有
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
text: 原字符串
|
|
300
|
+
pattern: 正则表达式
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
List[str]: 所有匹配
|
|
304
|
+
"""
|
|
305
|
+
return re.findall(pattern, text)
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def format_string(template: str, **kwargs) -> str:
|
|
309
|
+
"""格式化字符串
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
template: 模板字符串
|
|
313
|
+
**kwargs: 替换参数
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
str: 格式化后的字符串
|
|
317
|
+
"""
|
|
318
|
+
return template.format(**kwargs)
|
|
319
|
+
|
|
320
|
+
# ==================== 数学函数 ====================
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def abs_value(num: float) -> float:
|
|
324
|
+
"""绝对值"""
|
|
325
|
+
return abs(num)
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def round_value(num: float, precision: int = 0) -> float:
|
|
329
|
+
"""四舍五入"""
|
|
330
|
+
return round(num, precision)
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def floor(num: float) -> int:
|
|
334
|
+
"""向下取整"""
|
|
335
|
+
import math
|
|
336
|
+
return math.floor(num)
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def ceil(num: float) -> int:
|
|
340
|
+
"""向上取整"""
|
|
341
|
+
import math
|
|
342
|
+
return math.ceil(num)
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def max_value(*args) -> Any:
|
|
346
|
+
"""最大值"""
|
|
347
|
+
return max(args)
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def min_value(*args) -> Any:
|
|
351
|
+
"""最小值"""
|
|
352
|
+
return min(args)
|
|
353
|
+
|
|
354
|
+
# ==================== 类型转换 ====================
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def to_int(value: Any) -> int:
|
|
358
|
+
"""转换为整数"""
|
|
359
|
+
return int(value)
|
|
360
|
+
|
|
361
|
+
@staticmethod
|
|
362
|
+
def to_float(value: Any) -> float:
|
|
363
|
+
"""转换为浮点数"""
|
|
364
|
+
return float(value)
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def to_string(value: Any) -> str:
|
|
368
|
+
"""转换为字符串"""
|
|
369
|
+
return str(value)
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def to_bool(value: Any) -> bool:
|
|
373
|
+
"""转换为布尔值"""
|
|
374
|
+
if isinstance(value, str):
|
|
375
|
+
return value.lower() in ('true', '1', 'yes', 'on')
|
|
376
|
+
return bool(value)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""装饰器工具"""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import time
|
|
5
|
+
from typing import Callable, Type, Tuple, Any, Optional
|
|
6
|
+
|
|
7
|
+
from ..reference import INFO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def retry(
|
|
11
|
+
max_attempts: int = 3,
|
|
12
|
+
delay: float = 1.0,
|
|
13
|
+
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
14
|
+
on_retry: Optional[Callable] = None
|
|
15
|
+
):
|
|
16
|
+
"""重试装饰器
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
max_attempts: 最大重试次数
|
|
20
|
+
delay: 重试间隔 (秒)
|
|
21
|
+
exceptions: 需要重试的异常类型
|
|
22
|
+
on_retry: 重试时的回调函数
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
@retry(max_attempts=3, delay=1.0)
|
|
26
|
+
def unstable_function():
|
|
27
|
+
...
|
|
28
|
+
"""
|
|
29
|
+
def decorator(func: Callable) -> Callable:
|
|
30
|
+
@functools.wraps(func)
|
|
31
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
32
|
+
last_exception = None
|
|
33
|
+
|
|
34
|
+
for attempt in range(1, max_attempts + 1):
|
|
35
|
+
try:
|
|
36
|
+
return func(*args, **kwargs)
|
|
37
|
+
except exceptions as e:
|
|
38
|
+
last_exception = e
|
|
39
|
+
if attempt < max_attempts:
|
|
40
|
+
INFO(f"第 {attempt} 次尝试失败: {e}, {delay}s 后重试...", "WARNING")
|
|
41
|
+
if on_retry:
|
|
42
|
+
on_retry(attempt, e)
|
|
43
|
+
time.sleep(delay)
|
|
44
|
+
else:
|
|
45
|
+
INFO(f"达到最大重试次数 ({max_attempts}), 放弃", "ERROR")
|
|
46
|
+
|
|
47
|
+
raise last_exception
|
|
48
|
+
|
|
49
|
+
return wrapper
|
|
50
|
+
return decorator
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def timeout(seconds: float, default: Any = None, raise_error: bool = True):
|
|
54
|
+
"""超时装饰器 (使用线程实现)
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
seconds: 超时时间 (秒)
|
|
58
|
+
default: 超时时返回的默认值
|
|
59
|
+
raise_error: 超时时是否抛出异常
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
@timeout(10.0)
|
|
63
|
+
def slow_function():
|
|
64
|
+
...
|
|
65
|
+
"""
|
|
66
|
+
def decorator(func: Callable) -> Callable:
|
|
67
|
+
@functools.wraps(func)
|
|
68
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
69
|
+
import threading
|
|
70
|
+
result = [default]
|
|
71
|
+
exception = [None]
|
|
72
|
+
|
|
73
|
+
def target():
|
|
74
|
+
try:
|
|
75
|
+
result[0] = func(*args, **kwargs)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
exception[0] = e
|
|
78
|
+
|
|
79
|
+
thread = threading.Thread(target=target)
|
|
80
|
+
thread.daemon = True
|
|
81
|
+
thread.start()
|
|
82
|
+
thread.join(timeout=seconds)
|
|
83
|
+
|
|
84
|
+
if thread.is_alive():
|
|
85
|
+
if raise_error:
|
|
86
|
+
raise TimeoutError(f"函数 {func.__name__} 执行超时 ({seconds}s)")
|
|
87
|
+
INFO(f"函数 {func.__name__} 执行超时 ({seconds}s)", "WARNING")
|
|
88
|
+
return default
|
|
89
|
+
|
|
90
|
+
if exception[0]:
|
|
91
|
+
raise exception[0]
|
|
92
|
+
|
|
93
|
+
return result[0]
|
|
94
|
+
|
|
95
|
+
return wrapper
|
|
96
|
+
return decorator
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def catch_exception(
|
|
100
|
+
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
101
|
+
default: Any = None,
|
|
102
|
+
log_error: bool = True
|
|
103
|
+
):
|
|
104
|
+
"""异常捕获装饰器
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
exceptions: 要捕获的异常类型
|
|
108
|
+
default: 异常时返回的默认值
|
|
109
|
+
log_error: 是否记录错误日志
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
@catch_exception(default=None)
|
|
113
|
+
def risky_function():
|
|
114
|
+
...
|
|
115
|
+
"""
|
|
116
|
+
def decorator(func: Callable) -> Callable:
|
|
117
|
+
@functools.wraps(func)
|
|
118
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
119
|
+
try:
|
|
120
|
+
return func(*args, **kwargs)
|
|
121
|
+
except exceptions as e:
|
|
122
|
+
if log_error:
|
|
123
|
+
INFO(f"函数 {func.__name__} 发生异常: {e}", "WARNING")
|
|
124
|
+
return default
|
|
125
|
+
|
|
126
|
+
return wrapper
|
|
127
|
+
return decorator
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def log_execution(log_args: bool = True, log_result: bool = True):
|
|
131
|
+
"""执行日志装饰器
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
log_args: 是否记录参数
|
|
135
|
+
log_result: 是否记录返回值
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
@log_execution()
|
|
139
|
+
def important_function(a, b):
|
|
140
|
+
...
|
|
141
|
+
"""
|
|
142
|
+
def decorator(func: Callable) -> Callable:
|
|
143
|
+
@functools.wraps(func)
|
|
144
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
145
|
+
func_name = func.__name__
|
|
146
|
+
|
|
147
|
+
if log_args:
|
|
148
|
+
args_repr = [repr(a) for a in args]
|
|
149
|
+
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
|
|
150
|
+
signature = ", ".join(args_repr + kwargs_repr)
|
|
151
|
+
INFO(f"调用 {func_name}({signature})")
|
|
152
|
+
else:
|
|
153
|
+
INFO(f"调用 {func_name}()")
|
|
154
|
+
|
|
155
|
+
start_time = time.time()
|
|
156
|
+
result = func(*args, **kwargs)
|
|
157
|
+
elapsed = time.time() - start_time
|
|
158
|
+
|
|
159
|
+
if log_result:
|
|
160
|
+
INFO(f"{func_name} 返回: {result!r} (耗时: {elapsed:.3f}s)")
|
|
161
|
+
else:
|
|
162
|
+
INFO(f"{func_name} 完成 (耗时: {elapsed:.3f}s)")
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
return wrapper
|
|
167
|
+
return decorator
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def singleton(cls: Type) -> Type:
|
|
171
|
+
"""单例装饰器
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
@singleton
|
|
175
|
+
class MyClass:
|
|
176
|
+
...
|
|
177
|
+
"""
|
|
178
|
+
instances = {}
|
|
179
|
+
|
|
180
|
+
@functools.wraps(cls)
|
|
181
|
+
def get_instance(*args, **kwargs):
|
|
182
|
+
if cls not in instances:
|
|
183
|
+
instances[cls] = cls(*args, **kwargs)
|
|
184
|
+
return instances[cls]
|
|
185
|
+
|
|
186
|
+
return get_instance
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def deprecated(message: str = ""):
|
|
190
|
+
"""弃用警告装饰器
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
message: 弃用说明
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
@deprecated("请使用 new_function 代替")
|
|
197
|
+
def old_function():
|
|
198
|
+
...
|
|
199
|
+
"""
|
|
200
|
+
def decorator(func: Callable) -> Callable:
|
|
201
|
+
@functools.wraps(func)
|
|
202
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
203
|
+
import warnings
|
|
204
|
+
warn_msg = f"{func.__name__} 已弃用"
|
|
205
|
+
if message:
|
|
206
|
+
warn_msg += f": {message}"
|
|
207
|
+
warnings.warn(warn_msg, DeprecationWarning, stacklevel=2)
|
|
208
|
+
return func(*args, **kwargs)
|
|
209
|
+
|
|
210
|
+
return wrapper
|
|
211
|
+
return decorator
|