mdbq 4.0.23__py3-none-any.whl → 4.0.25__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.
- mdbq/__version__.py +1 -1
- mdbq/aggregation/query_data.py +14 -10
- mdbq/myconf/myconf.py +278 -374
- mdbq/myconf/{myconf2.py → myconf_bak.py} +296 -74
- {mdbq-4.0.23.dist-info → mdbq-4.0.25.dist-info}/METADATA +1 -1
- {mdbq-4.0.23.dist-info → mdbq-4.0.25.dist-info}/RECORD +8 -8
- {mdbq-4.0.23.dist-info → mdbq-4.0.25.dist-info}/WHEEL +0 -0
- {mdbq-4.0.23.dist-info → mdbq-4.0.25.dist-info}/top_level.txt +0 -0
mdbq/myconf/myconf.py
CHANGED
@@ -21,21 +21,16 @@ logger = mylogger.MyLogger(
|
|
21
21
|
T = TypeVar('T') # 类型变量
|
22
22
|
|
23
23
|
|
24
|
-
class
|
25
|
-
"""
|
26
|
-
|
27
|
-
Attributes:
|
28
|
-
message: 错误消息
|
29
|
-
file_path: 配置文件路径
|
30
|
-
section: 配置节名称
|
31
|
-
key: 配置键名称
|
32
|
-
"""
|
24
|
+
class ConfigException(Exception):
|
25
|
+
"""配置异常基类"""
|
33
26
|
def __init__(self, message: str, file_path: Optional[Union[str, Path]] = None,
|
34
|
-
section: Optional[str] = None, key: Optional[str] = None
|
27
|
+
section: Optional[str] = None, key: Optional[str] = None,
|
28
|
+
original_error: Optional[Exception] = None):
|
35
29
|
self.message = message
|
36
30
|
self.file_path = str(file_path) if file_path else None
|
37
31
|
self.section = section
|
38
32
|
self.key = key
|
33
|
+
self.original_error = original_error
|
39
34
|
super().__init__(self._format_message())
|
40
35
|
|
41
36
|
def _format_message(self) -> str:
|
@@ -47,70 +42,69 @@ class ConfigError(Exception):
|
|
47
42
|
parts.append(f"节: [{self.section}]")
|
48
43
|
if self.key:
|
49
44
|
parts.append(f"键: {self.key}")
|
45
|
+
if self.original_error:
|
46
|
+
parts.append(f"原始错误: {str(self.original_error)}")
|
50
47
|
return " | ".join(parts)
|
51
48
|
|
49
|
+
@classmethod
|
50
|
+
def file_not_found(cls, file_path: Union[str, Path]) -> 'ConfigException':
|
51
|
+
"""配置文件不存在异常"""
|
52
|
+
return cls("配置文件不存在", file_path=file_path)
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
54
|
+
@classmethod
|
55
|
+
def read_error(cls, file_path: Union[str, Path], error: Exception) -> 'ConfigException':
|
56
|
+
"""读取配置文件失败异常"""
|
57
|
+
return cls("读取配置文件失败", file_path=file_path, original_error=error)
|
57
58
|
|
59
|
+
@classmethod
|
60
|
+
def write_error(cls, file_path: Union[str, Path], error: Exception) -> 'ConfigException':
|
61
|
+
"""写入配置文件失败异常"""
|
62
|
+
return cls("写入配置文件失败", file_path=file_path, original_error=error)
|
58
63
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
"""
|
65
|
-
def __init__(self, file_path: Union[str, Path], original_error: Exception):
|
66
|
-
super().__init__(
|
67
|
-
f"读取配置文件失败: {str(original_error)}",
|
68
|
-
file_path=file_path
|
69
|
-
)
|
70
|
-
self.original_error = original_error
|
64
|
+
@classmethod
|
65
|
+
def value_error(cls, message: str, file_path: Union[str, Path],
|
66
|
+
section: Optional[str] = None, key: Optional[str] = None) -> 'ConfigException':
|
67
|
+
"""配置值无效异常"""
|
68
|
+
return cls(message, file_path=file_path, section=section, key=key)
|
71
69
|
|
70
|
+
@classmethod
|
71
|
+
def section_not_found(cls, file_path: Union[str, Path], section: str) -> 'ConfigException':
|
72
|
+
"""配置节不存在异常"""
|
73
|
+
return cls("配置节不存在", file_path=file_path, section=section)
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
original_error: 原始错误对象
|
78
|
-
"""
|
79
|
-
def __init__(self, file_path: Union[str, Path], original_error: Exception):
|
80
|
-
super().__init__(
|
81
|
-
f"写入配置文件失败: {str(original_error)}",
|
82
|
-
file_path=file_path
|
83
|
-
)
|
84
|
-
self.original_error = original_error
|
85
|
-
|
86
|
-
|
87
|
-
class ConfigValueError(ConfigError):
|
88
|
-
"""当配置值无效时抛出的异常"""
|
89
|
-
def __init__(self, message: str, file_path: Union[str, Path],
|
90
|
-
section: Optional[str] = None, key: Optional[str] = None):
|
91
|
-
super().__init__(message, file_path=file_path, section=section, key=key)
|
75
|
+
@classmethod
|
76
|
+
def key_not_found(cls, file_path: Union[str, Path], section: str, key: str) -> 'ConfigException':
|
77
|
+
"""配置键不存在异常"""
|
78
|
+
return cls("配置键不存在", file_path=file_path, section=section, key=key)
|
92
79
|
|
80
|
+
@classmethod
|
81
|
+
def validation_error(cls, message: str, file_path: Union[str, Path],
|
82
|
+
section: Optional[str] = None, key: Optional[str] = None) -> 'ConfigException':
|
83
|
+
"""配置验证失败异常"""
|
84
|
+
return cls(f"配置验证失败: {message}", file_path=file_path, section=section, key=key)
|
93
85
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
section=section
|
101
|
-
)
|
102
|
-
|
103
|
-
|
104
|
-
class ConfigKeyNotFoundError(ConfigError):
|
105
|
-
"""当指定的配置键不存在时抛出的异常"""
|
106
|
-
def __init__(self, file_path: Union[str, Path], section: str, key: str):
|
107
|
-
super().__init__(
|
108
|
-
f"配置键不存在",
|
86
|
+
@classmethod
|
87
|
+
def conversion_error(cls, value: Any, target_type: Type, file_path: Union[str, Path],
|
88
|
+
section: Optional[str] = None, key: Optional[str] = None) -> 'ConfigException':
|
89
|
+
"""类型转换失败异常"""
|
90
|
+
return cls(
|
91
|
+
f"无法将值 '{value}' 转换为类型 {target_type.__name__}",
|
109
92
|
file_path=file_path,
|
110
93
|
section=section,
|
111
94
|
key=key
|
112
95
|
)
|
113
96
|
|
97
|
+
@classmethod
|
98
|
+
def invalid_key_error(cls, key: str, file_path: Union[str, Path],
|
99
|
+
section: Optional[str] = None) -> 'ConfigException':
|
100
|
+
"""无效的键名异常"""
|
101
|
+
return cls(f"无效的键名: {key}", file_path=file_path, section=section, key=key)
|
102
|
+
|
103
|
+
@classmethod
|
104
|
+
def invalid_section_error(cls, section: str, file_path: Union[str, Path]) -> 'ConfigException':
|
105
|
+
"""无效的节名异常"""
|
106
|
+
return cls(f"无效的节名: {section}", file_path=file_path, section=section)
|
107
|
+
|
114
108
|
|
115
109
|
class CommentStyle(Enum):
|
116
110
|
"""配置文件支持的注释风格"""
|
@@ -121,21 +115,7 @@ class CommentStyle(Enum):
|
|
121
115
|
|
122
116
|
@dataclass
|
123
117
|
class ConfigOptions:
|
124
|
-
"""
|
125
|
-
|
126
|
-
Attributes:
|
127
|
-
comment_styles: 支持的注释风格列表
|
128
|
-
encoding: 文件编码
|
129
|
-
auto_create: 是否自动创建不存在的配置文件
|
130
|
-
strip_values: 是否去除配置值的首尾空白
|
131
|
-
preserve_comments: 是否保留注释
|
132
|
-
default_section: 默认配置节名称
|
133
|
-
separators: 支持的分隔符列表
|
134
|
-
cache_ttl: 缓存过期时间(秒)
|
135
|
-
validate_keys: 是否验证键名
|
136
|
-
key_pattern: 键名正则表达式模式
|
137
|
-
case_sensitive: 是否区分大小写
|
138
|
-
"""
|
118
|
+
"""配置解析器选项"""
|
139
119
|
comment_styles: List[CommentStyle] = field(default_factory=lambda: [CommentStyle.HASH, CommentStyle.DOUBLE_SLASH])
|
140
120
|
encoding: str = 'utf-8'
|
141
121
|
auto_create: bool = False
|
@@ -150,83 +130,44 @@ class ConfigOptions:
|
|
150
130
|
|
151
131
|
|
152
132
|
class ConfigParser:
|
153
|
-
"""
|
154
|
-
|
155
|
-
Attributes:
|
156
|
-
options: 解析器配置选项
|
157
|
-
_config_cache: 配置缓存,用于存储已读取的配置
|
158
|
-
_cache_timestamps: 缓存时间戳,用于管理缓存过期
|
159
|
-
_comments_cache: 注释缓存,用于存储每个配置节的注释
|
160
|
-
_section_map: 用于存储大小写映射
|
161
|
-
_current_file: 当前正在处理的文件路径
|
162
|
-
"""
|
133
|
+
"""配置文件解析器"""
|
163
134
|
|
164
135
|
def __init__(self, options: Optional[ConfigOptions] = None):
|
165
136
|
self.options = options or ConfigOptions()
|
166
137
|
self._config_cache: Dict[str, Dict[str, Any]] = {}
|
167
138
|
self._cache_timestamps: Dict[str, float] = {}
|
168
139
|
self._comments_cache: Dict[str, Dict[str, List[str]]] = {}
|
169
|
-
self._section_map: Dict[str, Dict[str, str]] = {}
|
170
|
-
self._current_file: Optional[Path] = None
|
140
|
+
self._section_map: Dict[str, Dict[str, str]] = {}
|
141
|
+
self._current_file: Optional[Path] = None
|
171
142
|
|
172
143
|
def __enter__(self) -> 'ConfigParser':
|
173
|
-
"""进入上下文管理器
|
174
|
-
|
175
|
-
Returns:
|
176
|
-
ConfigParser: 返回当前实例
|
177
|
-
"""
|
178
144
|
return self
|
179
145
|
|
180
146
|
def __exit__(self, exc_type: Optional[Type[BaseException]],
|
181
147
|
exc_val: Optional[BaseException],
|
182
148
|
exc_tb: Optional[Any]) -> None:
|
183
|
-
"""退出上下文管理器
|
184
|
-
|
185
|
-
Args:
|
186
|
-
exc_type: 异常类型
|
187
|
-
exc_val: 异常值
|
188
|
-
exc_tb: 异常追踪信息
|
189
|
-
"""
|
190
149
|
self._current_file = None
|
191
150
|
|
192
151
|
def open(self, file_path: Union[str, Path]) -> 'ConfigParser':
|
193
|
-
"""打开配置文件
|
194
|
-
|
195
|
-
Args:
|
196
|
-
file_path: 配置文件路径
|
197
|
-
|
198
|
-
Returns:
|
199
|
-
ConfigParser: 返回当前实例,支持链式调用
|
200
|
-
|
201
|
-
Raises:
|
202
|
-
ConfigFileNotFoundError: 当配置文件不存在且未启用自动创建时
|
203
|
-
"""
|
152
|
+
"""打开配置文件"""
|
204
153
|
file_path = Path(file_path)
|
205
154
|
if not file_path.exists() and not self.options.auto_create:
|
206
|
-
raise
|
155
|
+
raise ConfigException.file_not_found(file_path)
|
207
156
|
self._current_file = file_path
|
208
157
|
return self
|
209
158
|
|
210
159
|
def _ensure_file_open(self) -> None:
|
211
|
-
"""确保文件已打开
|
212
|
-
|
213
|
-
Raises:
|
214
|
-
ConfigError: 当文件未打开时
|
215
|
-
"""
|
160
|
+
"""确保文件已打开"""
|
216
161
|
if self._current_file is None:
|
217
|
-
raise
|
162
|
+
raise ConfigException("未打开任何配置文件,请先调用 open() 方法")
|
218
163
|
|
219
164
|
def _is_comment_line(self, line: str) -> bool:
|
220
|
-
"""
|
165
|
+
"""判断是否为注释行"""
|
221
166
|
stripped = line.strip()
|
222
167
|
return any(stripped.startswith(style.value) for style in self.options.comment_styles)
|
223
168
|
|
224
169
|
def _extract_comment(self, line: str) -> Tuple[str, str]:
|
225
|
-
"""从行中提取注释
|
226
|
-
|
227
|
-
Returns:
|
228
|
-
Tuple[str, str]: (去除注释后的行内容, 注释内容)
|
229
|
-
"""
|
170
|
+
"""从行中提取注释"""
|
230
171
|
for style in self.options.comment_styles:
|
231
172
|
comment_match = re.search(fr'\s+{re.escape(style.value)}.*$', line)
|
232
173
|
if comment_match:
|
@@ -234,14 +175,7 @@ class ConfigParser:
|
|
234
175
|
return line.strip(), ''
|
235
176
|
|
236
177
|
def _split_key_value(self, line: str) -> Optional[Tuple[str, str]]:
|
237
|
-
"""分割配置行为键值对
|
238
|
-
|
239
|
-
Args:
|
240
|
-
line: 要分割的配置行
|
241
|
-
|
242
|
-
Returns:
|
243
|
-
Optional[Tuple[str, str]]: 键值对元组,如果无法分割则返回None
|
244
|
-
"""
|
178
|
+
"""分割配置行为键值对"""
|
245
179
|
for sep in self.options.separators:
|
246
180
|
if sep in line:
|
247
181
|
key_part, value_part = line.split(sep, 1)
|
@@ -263,11 +197,8 @@ class ConfigParser:
|
|
263
197
|
return bool(re.match(self.options.key_pattern, key))
|
264
198
|
|
265
199
|
def _get_cached_config(self, file_path: str) -> Optional[Dict[str, Any]]:
|
266
|
-
"""
|
267
|
-
if file_path not in self._config_cache:
|
268
|
-
return None
|
269
|
-
|
270
|
-
if file_path not in self._cache_timestamps:
|
200
|
+
"""获取缓存的配置"""
|
201
|
+
if file_path not in self._config_cache or file_path not in self._cache_timestamps:
|
271
202
|
return None
|
272
203
|
|
273
204
|
if time.time() - self._cache_timestamps[file_path] > self.options.cache_ttl:
|
@@ -281,10 +212,8 @@ class ConfigParser:
|
|
281
212
|
self._cache_timestamps[file_path] = time.time()
|
282
213
|
|
283
214
|
def _normalize_section(self, section: str) -> str:
|
284
|
-
"""
|
285
|
-
|
286
|
-
return section
|
287
|
-
return section.lower()
|
215
|
+
"""标准化节名称"""
|
216
|
+
return section.replace(' ', '').lower()
|
288
217
|
|
289
218
|
def _get_original_section(self, file_path: str, normalized_section: str) -> Optional[str]:
|
290
219
|
"""获取原始节名称"""
|
@@ -313,73 +242,50 @@ class ConfigParser:
|
|
313
242
|
self._comments_cache.clear()
|
314
243
|
self._section_map.clear()
|
315
244
|
|
316
|
-
def _convert_value(self, value: str, target_type: Type[T]) -> T:
|
317
|
-
"""转换配置值到指定类型
|
318
|
-
|
319
|
-
Args:
|
320
|
-
value: 要转换的值
|
321
|
-
target_type: 目标类型
|
322
|
-
|
323
|
-
Returns:
|
324
|
-
T: 转换后的值
|
325
|
-
|
326
|
-
Raises:
|
327
|
-
ConfigValueError: 当值无法转换为指定类型时
|
328
|
-
"""
|
245
|
+
def _convert_value(self, value: str, target_type: Type[T], file_path: Optional[Union[str, Path]] = None, key: Optional[str] = None) -> T:
|
246
|
+
"""转换配置值到指定类型"""
|
329
247
|
try:
|
330
248
|
if target_type == bool:
|
331
249
|
return bool(value.lower() in ('true', 'yes', '1', 'on'))
|
332
250
|
elif target_type == list:
|
333
|
-
# 支持多种分隔符的列表
|
334
251
|
if not value.strip():
|
335
252
|
return []
|
336
|
-
# 尝试不同的分隔符
|
337
253
|
for sep in [',', ';', '|', ' ']:
|
338
254
|
if sep in value:
|
339
255
|
return [item.strip() for item in value.split(sep) if item.strip()]
|
340
|
-
# 如果没有分隔符,则作为单个元素返回
|
341
256
|
return [value.strip()]
|
342
257
|
elif target_type == tuple:
|
343
|
-
# 支持元组类型
|
344
258
|
if not value.strip():
|
345
259
|
return ()
|
346
|
-
# 尝试不同的分隔符
|
347
260
|
for sep in [',', ';', '|', ' ']:
|
348
261
|
if sep in value:
|
349
262
|
return tuple(item.strip() for item in value.split(sep) if item.strip())
|
350
|
-
# 如果没有分隔符,则作为单个元素返回
|
351
263
|
return (value.strip(),)
|
352
|
-
elif target_type == set:
|
353
|
-
# 支持集合类型
|
264
|
+
elif target_type == set or target_type == frozenset:
|
354
265
|
if not value.strip():
|
355
|
-
return set()
|
356
|
-
# 尝试不同的分隔符
|
266
|
+
return set() if target_type == set else frozenset()
|
357
267
|
for sep in [',', ';', '|', ' ']:
|
358
268
|
if sep in value:
|
359
|
-
|
360
|
-
|
361
|
-
return
|
269
|
+
items = [item.strip() for item in value.split(sep) if item.strip()]
|
270
|
+
return set(items) if target_type == set else frozenset(items)
|
271
|
+
return set([value.strip()]) if target_type == set else frozenset([value.strip()])
|
362
272
|
elif target_type == dict:
|
363
|
-
# 支持字典类型,格式:key1=value1,key2=value2
|
364
273
|
if not value.strip():
|
365
274
|
return {}
|
366
275
|
result = {}
|
367
|
-
# 尝试不同的分隔符
|
368
276
|
for sep in [',', ';', '|']:
|
369
277
|
if sep in value:
|
370
278
|
pairs = [pair.strip() for pair in value.split(sep) if pair.strip()]
|
371
279
|
for pair in pairs:
|
372
280
|
if '=' in pair:
|
373
|
-
|
374
|
-
result[
|
281
|
+
key_, val = pair.split('=', 1)
|
282
|
+
result[key_.strip()] = val.strip()
|
375
283
|
return result
|
376
|
-
# 如果没有分隔符,尝试单个键值对
|
377
284
|
if '=' in value:
|
378
|
-
|
379
|
-
return {
|
285
|
+
key_, val = value.split('=', 1)
|
286
|
+
return {key_.strip(): val.strip()}
|
380
287
|
return {}
|
381
288
|
elif target_type == int:
|
382
|
-
# 支持十六进制、八进制、二进制
|
383
289
|
value = value.strip().lower()
|
384
290
|
if value.startswith('0x'):
|
385
291
|
return int(value, 16)
|
@@ -396,12 +302,7 @@ class ConfigParser:
|
|
396
302
|
return value.encode('utf-8')
|
397
303
|
elif target_type == bytearray:
|
398
304
|
return bytearray(value.encode('utf-8'))
|
399
|
-
elif target_type == set:
|
400
|
-
return set(value.split(','))
|
401
|
-
elif target_type == frozenset:
|
402
|
-
return frozenset(value.split(','))
|
403
305
|
elif target_type == range:
|
404
|
-
# 支持 range 类型,格式:start:stop:step 或 start:stop
|
405
306
|
parts = value.split(':')
|
406
307
|
if len(parts) == 2:
|
407
308
|
return range(int(parts[0]), int(parts[1]))
|
@@ -410,81 +311,39 @@ class ConfigParser:
|
|
410
311
|
raise ValueError("Invalid range format")
|
411
312
|
return target_type(value)
|
412
313
|
except (ValueError, TypeError) as e:
|
413
|
-
raise
|
414
|
-
f"无法将值 '{value}' 转换为类型 {target_type.__name__}",
|
415
|
-
file_path=None,
|
416
|
-
key=None
|
417
|
-
)
|
314
|
+
raise ConfigException.conversion_error(value, target_type, file_path, key=key)
|
418
315
|
|
419
|
-
def get_value(self,
|
420
|
-
|
421
|
-
|
422
|
-
"""获取指定配置项的值
|
423
|
-
|
424
|
-
Args:
|
425
|
-
file_path: 配置文件路径,如果为None则使用当前打开的文件
|
426
|
-
key: 配置键
|
427
|
-
section: 配置节名称,如果为None则使用默认节
|
428
|
-
default: 当配置项不存在时返回的默认值
|
429
|
-
value_type: 期望的值的类型
|
430
|
-
|
431
|
-
Returns:
|
432
|
-
T: 配置值
|
433
|
-
|
434
|
-
Raises:
|
435
|
-
ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
|
436
|
-
ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
|
437
|
-
ConfigValueError: 当值无法转换为指定类型时
|
438
|
-
"""
|
316
|
+
def get_value(self, section: Optional[str] = None, key: str = None,
|
317
|
+
default: Any = None, value_type: Optional[Type[T]] = None,
|
318
|
+
file_path: Optional[Union[str, Path]] = None) -> T:
|
319
|
+
"""获取指定配置项的值"""
|
439
320
|
if file_path is None:
|
440
321
|
self._ensure_file_open()
|
441
322
|
file_path = self._current_file
|
442
323
|
if not self._validate_key(key):
|
443
|
-
raise
|
444
|
-
|
324
|
+
raise ConfigException.invalid_key_error(key, file_path, section)
|
445
325
|
config = self.read(file_path)
|
446
326
|
section = section or self.options.default_section
|
447
327
|
normalized_section = self._normalize_section(section)
|
448
|
-
|
449
|
-
# 获取原始节名称
|
450
328
|
original_section = self._get_original_section(str(file_path), normalized_section)
|
451
329
|
if original_section is None:
|
452
330
|
if default is not None:
|
453
331
|
return default
|
454
|
-
raise
|
455
|
-
|
332
|
+
raise ConfigException.section_not_found(file_path, section)
|
456
333
|
if key not in config[original_section]:
|
457
334
|
if default is not None:
|
458
335
|
return default
|
459
|
-
raise
|
460
|
-
|
336
|
+
raise ConfigException.key_not_found(file_path, original_section, key)
|
461
337
|
value = config[original_section][key]
|
462
|
-
|
463
338
|
if value_type is not None:
|
464
|
-
return self._convert_value(value, value_type)
|
465
|
-
|
339
|
+
return self._convert_value(value, value_type, file_path=file_path, key=key)
|
466
340
|
return value
|
467
341
|
|
468
|
-
def get_values(self, keys: List[Tuple[str, str]],
|
469
|
-
file_path: Optional[Union[str, Path]] = None,
|
342
|
+
def get_values(self, section: Optional[str] = None, keys: List[Tuple[str, str]] = None,
|
470
343
|
defaults: Optional[Dict[str, Any]] = None,
|
471
|
-
value_types: Optional[Dict[str, Type]] = None
|
472
|
-
|
473
|
-
|
474
|
-
Args:
|
475
|
-
keys: 配置项列表,每个元素为 (section, key) 元组
|
476
|
-
file_path: 配置文件路径,如果为None则使用当前打开的文件
|
477
|
-
defaults: 默认值字典,格式为 {key: default_value}
|
478
|
-
value_types: 值类型字典,格式为 {key: type}
|
479
|
-
|
480
|
-
Returns:
|
481
|
-
Dict[str, Any]: 配置值字典,格式为 {key: value}
|
482
|
-
|
483
|
-
Raises:
|
484
|
-
ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
|
485
|
-
ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
|
486
|
-
ConfigValueError: 当值无法转换为指定类型时
|
487
|
-
"""
|
344
|
+
value_types: Optional[Dict[str, Type]] = None,
|
345
|
+
file_path: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
|
346
|
+
"""批量获取多个配置项的值"""
|
488
347
|
if file_path is None:
|
489
348
|
self._ensure_file_open()
|
490
349
|
file_path = self._current_file
|
@@ -495,14 +354,15 @@ class ConfigParser:
|
|
495
354
|
for section, key in keys:
|
496
355
|
try:
|
497
356
|
value = self.get_value(
|
498
|
-
file_path=file_path,
|
499
|
-
key=key,
|
500
357
|
section=section,
|
358
|
+
key=key,
|
501
359
|
default=defaults.get(key),
|
502
|
-
value_type=value_types.get(key)
|
360
|
+
value_type=value_types.get(key),
|
361
|
+
file_path=file_path
|
503
362
|
)
|
504
363
|
result[key] = value
|
505
|
-
except
|
364
|
+
except ConfigException as e:
|
365
|
+
logger.error(f"读取配置项失败: section={section}, key={key}, error={e}")
|
506
366
|
if key in defaults:
|
507
367
|
result[key] = defaults[key]
|
508
368
|
else:
|
@@ -510,28 +370,11 @@ class ConfigParser:
|
|
510
370
|
|
511
371
|
return result
|
512
372
|
|
513
|
-
def get_section_values(self, keys: List[str],
|
514
|
-
section: Optional[str] = None,
|
515
|
-
file_path: Optional[Union[str, Path]] = None,
|
373
|
+
def get_section_values(self, section: Optional[str] = None, keys: List[str] = None,
|
516
374
|
defaults: Optional[Dict[str, Any]] = None,
|
517
|
-
value_types: Optional[Dict[str, Type]] = None
|
518
|
-
|
519
|
-
|
520
|
-
Args:
|
521
|
-
keys: 要获取的键列表
|
522
|
-
section: 配置节名称,默认为 DEFAULT
|
523
|
-
file_path: 配置文件路径,如果为None则使用当前打开的文件
|
524
|
-
defaults: 默认值字典,格式为 {key: default_value}
|
525
|
-
value_types: 值类型字典,格式为 {key: type}
|
526
|
-
|
527
|
-
Returns:
|
528
|
-
Tuple[Any, ...]: 按键列表顺序返回的值元组
|
529
|
-
|
530
|
-
Raises:
|
531
|
-
ConfigSectionNotFoundError: 当指定的节不存在且未提供默认值时
|
532
|
-
ConfigKeyNotFoundError: 当指定的键不存在且未提供默认值时
|
533
|
-
ConfigValueError: 当值无法转换为指定类型时
|
534
|
-
"""
|
375
|
+
value_types: Optional[Dict[str, Type]] = None,
|
376
|
+
file_path: Optional[Union[str, Path]] = None) -> Tuple[Any, ...]:
|
377
|
+
"""获取指定节点下多个键的值元组"""
|
535
378
|
if file_path is None:
|
536
379
|
self._ensure_file_open()
|
537
380
|
file_path = self._current_file
|
@@ -542,14 +385,15 @@ class ConfigParser:
|
|
542
385
|
for key in keys:
|
543
386
|
try:
|
544
387
|
value = self.get_value(
|
545
|
-
file_path=file_path,
|
546
|
-
key=key,
|
547
388
|
section=section,
|
389
|
+
key=key,
|
548
390
|
default=defaults.get(key),
|
549
|
-
value_type=value_types.get(key)
|
391
|
+
value_type=value_types.get(key),
|
392
|
+
file_path=file_path
|
550
393
|
)
|
551
394
|
result.append(value)
|
552
|
-
except
|
395
|
+
except ConfigException as e:
|
396
|
+
logger.error(f"读取配置项失败: section={section}, key={key}, error={e}")
|
553
397
|
if key in defaults:
|
554
398
|
result.append(defaults[key])
|
555
399
|
else:
|
@@ -557,41 +401,23 @@ class ConfigParser:
|
|
557
401
|
|
558
402
|
return tuple(result)
|
559
403
|
|
560
|
-
def set_value(self, key: str, value: Any,
|
561
|
-
|
562
|
-
file_path: Optional[Union[str, Path]] = None
|
563
|
-
|
564
|
-
"""设置指定配置项的值,保持原始文件的格式和注释
|
565
|
-
|
566
|
-
Args:
|
567
|
-
key: 配置键
|
568
|
-
value: 要设置的值
|
569
|
-
section: 配置节名称,如果为None则使用默认节
|
570
|
-
file_path: 配置文件路径,如果为None则使用当前打开的文件
|
571
|
-
value_type: 值的类型,用于验证和转换
|
572
|
-
|
573
|
-
Raises:
|
574
|
-
ConfigValueError: 当值无法转换为指定类型时
|
575
|
-
ConfigError: 当其他配置错误发生时
|
576
|
-
"""
|
404
|
+
def set_value(self, section: Optional[str] = None, key: str = None, value: Any = None,
|
405
|
+
value_type: Optional[Type] = None,
|
406
|
+
file_path: Optional[Union[str, Path]] = None) -> None:
|
407
|
+
"""设置指定配置项的值"""
|
577
408
|
if file_path is None:
|
578
409
|
self._ensure_file_open()
|
579
410
|
file_path = self._current_file
|
580
411
|
if not self._validate_key(key):
|
581
|
-
raise
|
582
|
-
|
583
|
-
# 读取原始文件內容
|
412
|
+
raise ConfigException.invalid_key_error(key, file_path, section)
|
413
|
+
section = section or self.options.default_section
|
584
414
|
original_lines = []
|
585
415
|
if file_path.exists():
|
586
416
|
with open(file_path, 'r', encoding=self.options.encoding) as file:
|
587
417
|
original_lines = file.readlines()
|
588
|
-
|
589
|
-
# 读取当前配置
|
590
418
|
config = self.read(file_path)
|
591
|
-
|
592
419
|
if section not in config:
|
593
420
|
config[section] = {}
|
594
|
-
|
595
421
|
if value_type is not None:
|
596
422
|
try:
|
597
423
|
if value_type == bool:
|
@@ -602,109 +428,106 @@ class ConfigParser:
|
|
602
428
|
else:
|
603
429
|
value = value_type(value)
|
604
430
|
except (ValueError, TypeError) as e:
|
605
|
-
raise
|
606
|
-
f"无法将值 '{value}' 转换为类型 {value_type.__name__}",
|
607
|
-
file_path=file_path,
|
608
|
-
section=section,
|
609
|
-
key=key
|
610
|
-
)
|
611
|
-
|
431
|
+
raise ConfigException.conversion_error(value, value_type, file_path, section=section, key=key)
|
612
432
|
if isinstance(value, bool):
|
613
433
|
value = str(value).lower()
|
614
434
|
else:
|
615
435
|
value = str(value)
|
616
|
-
|
617
|
-
# 更新配置
|
618
436
|
config[section][key] = value
|
619
|
-
|
620
|
-
# 写入文件,保持原始格式
|
621
437
|
try:
|
622
438
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
623
|
-
|
624
439
|
with open(file_path, 'w', encoding=self.options.encoding) as file:
|
625
440
|
current_section = self.options.default_section
|
626
|
-
section_separators = {}
|
627
|
-
|
628
|
-
# 解析原始文件,提取格式信息
|
441
|
+
section_separators = {}
|
629
442
|
for line in original_lines:
|
630
443
|
stripped_line = line.strip()
|
631
|
-
|
632
444
|
if not stripped_line:
|
633
|
-
file.write(line)
|
445
|
+
file.write(line)
|
634
446
|
continue
|
635
|
-
|
636
447
|
if stripped_line.startswith('[') and stripped_line.endswith(']'):
|
637
448
|
current_section = stripped_line[1:-1]
|
638
|
-
file.write(line)
|
449
|
+
file.write(line)
|
639
450
|
continue
|
640
|
-
|
641
451
|
if self._is_comment_line(stripped_line):
|
642
|
-
file.write(line)
|
452
|
+
file.write(line)
|
643
453
|
continue
|
644
|
-
|
645
454
|
key_value = self._split_key_value(stripped_line)
|
646
455
|
if key_value:
|
647
456
|
orig_key, orig_value = key_value
|
648
|
-
# 检测使用的分隔符
|
649
457
|
for sep in self.options.separators:
|
650
458
|
if sep in line:
|
651
459
|
section_separators.setdefault(current_section, {})[orig_key] = sep
|
652
460
|
break
|
653
|
-
|
654
|
-
# 如果是当前要修改的键,则写入新值
|
655
461
|
if current_section == section and orig_key == key:
|
656
462
|
separator = section_separators.get(current_section, {}).get(orig_key, self.options.separators[0])
|
657
|
-
# 提取行尾注释
|
658
463
|
comment = ''
|
659
464
|
for style in self.options.comment_styles:
|
660
465
|
comment_match = re.search(fr'\s+{re.escape(style.value)}.*$', line)
|
661
466
|
if comment_match:
|
662
467
|
comment = comment_match.group(0)
|
663
468
|
break
|
664
|
-
# 写入新值并保留注释
|
665
469
|
file.write(f'{key}{separator}{value}{comment}\n')
|
666
470
|
else:
|
667
|
-
file.write(line)
|
471
|
+
file.write(line)
|
668
472
|
else:
|
669
|
-
file.write(line)
|
670
|
-
|
671
|
-
# 如果section不存在,则添加新的section
|
473
|
+
file.write(line)
|
672
474
|
if section not in [line.strip()[1:-1] for line in original_lines if line.strip().startswith('[') and line.strip().endswith(']')]:
|
673
475
|
file.write(f'\n[{section}]\n')
|
674
476
|
file.write(f'{key}={value}\n')
|
675
|
-
|
676
477
|
self._clear_cache(str(file_path))
|
677
|
-
|
678
478
|
except Exception as e:
|
679
|
-
raise
|
479
|
+
raise ConfigException.write_error(file_path, e)
|
480
|
+
|
481
|
+
def set_values(self, section: Optional[str] = None, values: Dict[str, Any] = None,
|
482
|
+
value_types: Optional[Dict[str, Type]] = None,
|
483
|
+
file_path: Optional[Union[str, Path]] = None) -> None:
|
484
|
+
"""批量设置多个配置项的值"""
|
485
|
+
for key, value in values.items():
|
486
|
+
value_type = value_types.get(key) if value_types else None
|
487
|
+
self.set_value(section, key, value, value_type, file_path)
|
488
|
+
|
489
|
+
def validate_config(self, section: Optional[str] = None, schema: Dict[str, Type] = None,
|
490
|
+
file_path: Optional[Union[str, Path]] = None) -> bool:
|
491
|
+
"""验证配置是否符合指定的模式"""
|
492
|
+
config = self.read(file_path)
|
493
|
+
if section:
|
494
|
+
if section not in config:
|
495
|
+
return False
|
496
|
+
for key, expected_type in schema.items():
|
497
|
+
if key not in config[section]:
|
498
|
+
return False
|
499
|
+
try:
|
500
|
+
self._convert_value(config[section][key], expected_type)
|
501
|
+
except ConfigException:
|
502
|
+
return False
|
503
|
+
else:
|
504
|
+
for section, keys in schema.items():
|
505
|
+
if section not in config:
|
506
|
+
return False
|
507
|
+
for key, expected_type in keys.items():
|
508
|
+
if key not in config[section]:
|
509
|
+
return False
|
510
|
+
try:
|
511
|
+
self._convert_value(config[section][key], expected_type)
|
512
|
+
except ConfigException:
|
513
|
+
return False
|
514
|
+
return True
|
680
515
|
|
681
516
|
def read(self, file_path: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
|
682
|
-
"""读取配置文件内容
|
683
|
-
|
684
|
-
Args:
|
685
|
-
file_path: 配置文件路径,如果为None则使用当前打开的文件
|
686
|
-
|
687
|
-
Returns:
|
688
|
-
Dict[str, Any]: 配置字典,格式为 {section: {key: value}}
|
689
|
-
|
690
|
-
Raises:
|
691
|
-
ConfigFileNotFoundError: 当配置文件不存在且未启用自动创建时
|
692
|
-
ConfigReadError: 当读取配置文件失败时
|
693
|
-
"""
|
517
|
+
"""读取配置文件内容"""
|
694
518
|
if file_path is None:
|
695
519
|
self._ensure_file_open()
|
696
520
|
file_path = self._current_file
|
697
521
|
else:
|
698
|
-
file_path = Path(file_path)
|
522
|
+
file_path = Path(file_path)
|
699
523
|
|
700
|
-
# 检查缓存
|
701
524
|
cached_config = self._get_cached_config(str(file_path))
|
702
525
|
if cached_config is not None:
|
703
526
|
return cached_config
|
704
527
|
|
705
528
|
if not file_path.exists():
|
706
529
|
if not self.options.auto_create:
|
707
|
-
raise
|
530
|
+
raise ConfigException.file_not_found(file_path)
|
708
531
|
logger.info(f'配置文件不存在,将创建: {file_path}')
|
709
532
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
710
533
|
file_path.touch()
|
@@ -727,11 +550,7 @@ class ConfigParser:
|
|
727
550
|
if stripped_line.startswith('[') and stripped_line.endswith(']'):
|
728
551
|
current_section = stripped_line[1:-1]
|
729
552
|
if not self._validate_key(current_section):
|
730
|
-
raise
|
731
|
-
f"无效的节名: {current_section}",
|
732
|
-
file_path=file_path,
|
733
|
-
section=current_section
|
734
|
-
)
|
553
|
+
raise ConfigException.invalid_section_error(current_section, file_path)
|
735
554
|
self._update_section_map(str(file_path), current_section)
|
736
555
|
if current_section not in config:
|
737
556
|
config[current_section] = {}
|
@@ -744,12 +563,7 @@ class ConfigParser:
|
|
744
563
|
if key_value:
|
745
564
|
key, value = key_value
|
746
565
|
if not self._validate_key(key):
|
747
|
-
raise
|
748
|
-
f"无效的键名: {key}",
|
749
|
-
file_path=file_path,
|
750
|
-
section=current_section,
|
751
|
-
key=key
|
752
|
-
)
|
566
|
+
raise ConfigException.invalid_key_error(key, file_path, current_section)
|
753
567
|
value, comment = self._extract_comment(value)
|
754
568
|
|
755
569
|
if self.options.strip_values:
|
@@ -766,51 +580,141 @@ class ConfigParser:
|
|
766
580
|
return config
|
767
581
|
|
768
582
|
except Exception as e:
|
769
|
-
raise
|
770
|
-
|
583
|
+
raise ConfigException.read_error(file_path, e)
|
584
|
+
|
771
585
|
|
772
586
|
def main() -> None:
|
773
|
-
|
774
|
-
config_file = Path('/Users/xigua/
|
775
|
-
|
587
|
+
"""示例用法"""
|
588
|
+
config_file = Path('/Users/xigua/spd_副本.txt')
|
589
|
+
|
590
|
+
# 创建配置解析器实例
|
591
|
+
parser = ConfigParser()
|
592
|
+
# 设置配置选项
|
593
|
+
options = ConfigOptions(
|
594
|
+
comment_styles=[CommentStyle.HASH, CommentStyle.DOUBLE_SLASH],
|
595
|
+
encoding='utf-8',
|
596
|
+
auto_create=True,
|
597
|
+
strip_values=True,
|
598
|
+
preserve_comments=True,
|
599
|
+
default_section='DEFAULT',
|
600
|
+
separators=['=', ':', ':'],
|
601
|
+
cache_ttl=300,
|
602
|
+
validate_keys=True,
|
603
|
+
key_pattern=r'^[a-zA-Z0-9_\-\.]+$',
|
604
|
+
case_sensitive=False
|
605
|
+
)
|
606
|
+
parser = ConfigParser(options)
|
607
|
+
|
776
608
|
# 方式1:使用上下文管理器
|
777
609
|
with ConfigParser() as parser:
|
778
610
|
parser.open(config_file)
|
611
|
+
# 读取配置 - 最常用的参数放在前面
|
779
612
|
host, port, username, password = parser.get_section_values(
|
780
|
-
|
781
|
-
|
613
|
+
section='mysql', # 最常用的section参数放在前面
|
614
|
+
keys=['host', 'port', 'username', 'password']
|
782
615
|
)
|
783
|
-
print("
|
784
|
-
|
616
|
+
print("2.1 使用上下文管理器 读取结果:", host, port, username, password)
|
785
617
|
# 修改配置
|
786
|
-
parser.set_value(
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
618
|
+
parser.set_value(
|
619
|
+
section='mysql',
|
620
|
+
key='username',
|
621
|
+
value='root'
|
622
|
+
)
|
623
|
+
parser.set_value(
|
624
|
+
section='mysql',
|
625
|
+
key='port',
|
626
|
+
value=3306,
|
627
|
+
value_type=int
|
628
|
+
)
|
629
|
+
|
797
630
|
# 方式2:链式调用
|
798
631
|
parser = ConfigParser()
|
799
632
|
host, port, username, password = parser.open(config_file).get_section_values(
|
800
|
-
|
801
|
-
|
633
|
+
section='mysql',
|
634
|
+
keys=['host', 'port', 'username', 'password']
|
802
635
|
)
|
803
|
-
print("
|
804
|
-
|
636
|
+
print("2.2 链式调用 读取结果:", host, port, username, password)
|
637
|
+
|
805
638
|
# 方式3:传统方式
|
806
639
|
parser = ConfigParser()
|
807
640
|
host, port, username, password = parser.get_section_values(
|
808
|
-
file_path=config_file,
|
809
641
|
section='mysql',
|
810
|
-
keys=['host', 'port', 'username', 'password']
|
642
|
+
keys=['host', 'port', 'username', 'password'],
|
643
|
+
file_path=config_file # 文件路径参数放在最后
|
644
|
+
)
|
645
|
+
print("2.3 传统方式 读取结果:", host, port, username, password)
|
646
|
+
|
647
|
+
# 3.1 读取单个值 - 最常用的参数放在前面
|
648
|
+
value = parser.get_value(
|
649
|
+
section='mysql',
|
650
|
+
key='host',
|
651
|
+
default='localhost', # 默认值参数放在中间
|
652
|
+
value_type=str,
|
653
|
+
file_path=config_file
|
654
|
+
)
|
655
|
+
print("3.1 单个值读取结果:", value)
|
656
|
+
|
657
|
+
# 3.2 批量读取多个值 - 最常用的参数放在前面
|
658
|
+
values = parser.get_values(
|
659
|
+
section='mysql',
|
660
|
+
keys=[('host', 'host'), ('host', 'port')],
|
661
|
+
defaults={'host': 'localhost', 'port': 3306},
|
662
|
+
value_types={'port': int},
|
663
|
+
file_path=config_file
|
664
|
+
)
|
665
|
+
print("3.2 批量读取多个值结果:", values)
|
666
|
+
|
667
|
+
# 3.3 读取整个配置
|
668
|
+
config = parser.read(config_file)
|
669
|
+
print("3.3 读取完整配置:", config)
|
670
|
+
|
671
|
+
# 4.1 写入单个值 - 最常用的参数放在前面
|
672
|
+
parser.set_value(
|
673
|
+
section='mysql',
|
674
|
+
key='host',
|
675
|
+
value='127.0.0.1',
|
676
|
+
value_type=str,
|
677
|
+
file_path=config_file
|
678
|
+
)
|
679
|
+
print("4.1 写入单个值结果:", config_file)
|
680
|
+
|
681
|
+
# 4.2 写入不同类型的数据 - 最常用的参数放在前面
|
682
|
+
parser.open(config_file)
|
683
|
+
parser.set_value(
|
684
|
+
section='mysql',
|
685
|
+
key='port',
|
686
|
+
value=3306,
|
687
|
+
value_type=int
|
688
|
+
)
|
689
|
+
parser.set_value(
|
690
|
+
section='mysql',
|
691
|
+
key='debug',
|
692
|
+
value=True,
|
693
|
+
value_type=bool
|
811
694
|
)
|
812
|
-
|
695
|
+
parser.set_value(
|
696
|
+
section='mysql',
|
697
|
+
key='servers',
|
698
|
+
value=['server1', 'server2'],
|
699
|
+
value_type=list
|
700
|
+
)
|
701
|
+
print("4.2 写入不同类型的数据")
|
702
|
+
|
703
|
+
try:
|
704
|
+
# 尝试读取不存在的配置项 - 最常用的参数放在前面
|
705
|
+
parser.get_value(
|
706
|
+
section='mysql',
|
707
|
+
key='nonexistent'
|
708
|
+
)
|
709
|
+
except ConfigException as e:
|
710
|
+
if "配置键不存在" in str(e):
|
711
|
+
print("5.1 错误处理 配置键不存在错误:", e)
|
712
|
+
elif "配置节不存在" in str(e):
|
713
|
+
print("5.2 错误处理 配置节不存在错误:", e)
|
714
|
+
else:
|
715
|
+
print("5.3 错误处理 配置错误:", e)
|
813
716
|
|
814
717
|
|
815
718
|
if __name__ == '__main__':
|
816
719
|
main()
|
720
|
+
|