Coverage for src/c41811/config/errors.py: 100%
153 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-04 11:16 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-04 11:16 +0000
1# cython: language_level = 3 # noqa: ERA001
4"""错误类"""
6import functools
7from collections.abc import Callable
8from collections.abc import Iterable
9from collections.abc import Mapping
10from collections.abc import Sequence
11from dataclasses import dataclass
12from enum import Enum
13from typing import Any
14from typing import Never
15from typing import Self
16from typing import cast
17from typing import override
19from .abc import ABCPath
20from .abc import AnyKey
23class DependencyNotFoundError(ImportError):
24 """
25 依赖缺失
27 .. versionadded:: 0.3.0
28 """
30 def __init__(
31 self,
32 dep_name: str,
33 description: str | None = None,
34 ):
35 """
36 :param dep_name: 依赖名称
37 :type dep_name: str
38 :param description: 描述信息
39 :type description: str | None
40 """ # noqa: D205
41 self.dep_name = dep_name
42 if description is None:
43 super().__init__(f"`{dep_name}` is required.")
44 else:
45 super().__init__(description.format(dep_name=dep_name))
48# noinspection PyNewStyleGenericSyntax
49def _unavailable_method[M: Callable[..., Never]](method: M) -> M:
50 """
51 被装饰的方法在调用时会抛出实例 :py:attr:`~UnavailableAttribute._exception` 上的异常
53 :param method: 被装饰的方法
54 :type method: M
55 :return: 被装饰的方法
56 :rtype: M
58 .. versionadded:: 0.3.0
59 """
61 # noinspection PyUnusedLocal
62 @functools.wraps(method)
63 def wrapper(self: "UnavailableAttribute", *args: Any, **kwargs: Any) -> Never: # noqa: ARG001
64 raise object.__getattribute__(self, "_reason")
66 return cast(M, wrapper)
69class UnavailableAttribute:
70 """
71 占位代理对象,在任意访问时抛出异常
73 .. versionadded:: 0.3.0
74 """ # noqa: RUF002
76 __slots__ = ("_name", "_reason")
78 def __init__(self, name: str, reason: Exception):
79 """
80 :param name: 属性名
81 :type name: str
82 :param reason: 抛出的异常
83 :type reason: Exception
84 """ # noqa: D205
85 object.__setattr__(self, "_name", name)
86 object.__setattr__(self, "_reason", reason)
88 @_unavailable_method
89 @override
90 def __getattribute__(self, name: str) -> Never: ... # type: ignore[empty-body]
92 @_unavailable_method
93 @override
94 def __setattr__(self, name: str, value: Any) -> Never: ... # type: ignore[empty-body]
96 @_unavailable_method
97 @override
98 def __delattr__(self, name: str) -> Never: ... # type: ignore[empty-body]
100 @_unavailable_method
101 def __call__(self, *args: Any, **kwargs: Any) -> Never: ... # type: ignore[empty-body] # noqa: D102
103 @_unavailable_method
104 def __getitem__(self, item: Any) -> Never: ... # type: ignore[empty-body]
106 @_unavailable_method
107 def __setitem__(self, key: Any, value: Any) -> Never: ... # type: ignore[empty-body]
109 @_unavailable_method
110 def __delitem__(self, key: Any) -> Never: ... # type: ignore[empty-body]
112 @_unavailable_method
113 def __iter__(self) -> Never: ... # type: ignore[empty-body]
115 @_unavailable_method
116 def __next__(self) -> Never: ... # type: ignore[empty-body]
118 @_unavailable_method
119 def __get__(self, instance: object, owner: type[object]) -> Never: ... # type: ignore[empty-body]
121 @_unavailable_method
122 def __set__(self, instance: object, value: object) -> Never: ... # type: ignore[empty-body]
124 @_unavailable_method
125 def __delete__(self, instance: object) -> Never: ... # type: ignore[empty-body]
127 @_unavailable_method
128 def __copy__(self) -> Never: ... # type: ignore[empty-body]
130 @_unavailable_method
131 def __deepcopy__(self, memo: dict[object, object]) -> Never: ... # type: ignore[empty-body]
133 @_unavailable_method
134 @override
135 def __hash__(self) -> Never: ... # type: ignore[empty-body]
137 @_unavailable_method
138 def __bool__(self) -> Never: ... # type: ignore[empty-body]
140 @_unavailable_method
141 def __len__(self) -> Never: ... # type: ignore[empty-body]
143 @_unavailable_method
144 def __contains__(self, item: object) -> Never: ... # type: ignore[empty-body]
146 @override
147 def __repr__(self) -> str:
148 return f"<{type(self).__name__} {object.__getattribute__(self, '_name')}>"
151@dataclass(frozen=True)
152class TokenInfo:
153 """
154 一段标记的相关信息 用于快速定位到指定标记
156 .. versionchanged:: 0.3.2
157 字段 ``current_token`` 由传入变为自动推导
159 字段 ``index`` 将始终为正数
161 变更为不可变类型
162 """
164 tokens: tuple[str, ...]
165 """
166 当前完整标记列表
167 """
168 index: int
169 """
170 current_token在tokens的下标
171 """
173 def __post_init__(self) -> None:
174 if self.index < 0:
175 object.__setattr__(self, "index", self.index + len(self.tokens))
177 @property
178 def current_token(self) -> str:
179 """当前标记"""
180 return self.tokens[self.index]
182 @property
183 def raw_string(self) -> str:
184 """标记的原始字符串"""
185 return "".join(self.tokens)
188class ConfigDataPathSyntaxWarning(SyntaxWarning):
189 """
190 配置数据检索路径语法警告
192 .. versionadded:: 0.3.2
193 """
195 def __init__(self, msg: str, token_info: TokenInfo):
196 """
197 :param msg: 警告信息
198 :type msg: str
199 :param token_info: token相关信息
200 :type token_info: TokenInfo
201 """ # noqa: D205
202 self.msg = msg
203 self.token_info = token_info
205 @override
206 def __str__(self) -> str:
207 return (
208 f"{self.msg}: "
209 f"{self.token_info.raw_string} -> {self.token_info.current_token}"
210 f" ({self.token_info.index + 1} / {len(self.token_info.tokens)})"
211 )
214class ConfigDataPathSyntaxError(Exception):
215 """
216 配置数据检索路径语法错误
218 .. versionadded:: 0.3.2
219 """
221 def __init__(self, msg: str, token_info: TokenInfo):
222 """
223 :param msg: 错误信息
224 :type msg: str
225 :param token_info: token相关信息
226 :type token_info: TokenInfo
227 """ # noqa: D205
228 self.msg = msg
229 self.token_info = token_info
231 @override
232 def __str__(self) -> str:
233 return (
234 f"{self.msg}: "
235 f"{self.token_info.raw_string} -> {self.token_info.current_token}"
236 f" ({self.token_info.index + 1} / {len(self.token_info.tokens)})"
237 )
240class ConfigOperate(Enum):
241 """对配置的操作类型"""
243 Delete = "Delete"
244 Read = "Read"
245 Write = "Write"
246 Unknown = None
249@dataclass(frozen=True)
250class KeyInfo[K: AnyKey]:
251 """
252 一段路径的相关信息 用于快速定位到指定键
254 .. versionchanged:: 0.3.2
255 字段 ``current_key`` 由传入变为自动推导
257 字段 ``index`` 将始终为正数
259 变更为不可变类型
260 """
262 path: ABCPath[K]
263 """
264 当前完整路径
265 """
267 index: int
268 """
269 current_key在path的下标
270 """
272 def __post_init__(self) -> None:
273 if self.index < 0:
274 object.__setattr__(self, "index", self.index + len(self.path))
276 @property
277 def current_key(self) -> K:
278 """当前键"""
279 return self.path[self.index]
281 @property
282 def relative_keys(self) -> Iterable[K]:
283 """从根到当前键的相对路径"""
284 return self.path[: self.index]
287class RequiredPathNotFoundError(LookupError):
288 """
289 需求的键未找到错误
291 .. versionchanged:: 0.1.5
292 现在继承自LookupError
293 """
295 def __init__(
296 self,
297 key_info: KeyInfo[Any],
298 operate: ConfigOperate = ConfigOperate.Unknown,
299 ):
300 """
301 :param key_info: 键相关信息
302 :type key_info: KeyInfo
303 :param operate: 何种操作过程中发生的该错误
304 :type operate: ConfigOperate
305 """ # noqa: D205
306 self.key_info = key_info
307 self.operate = ConfigOperate(operate)
309 @override
310 def __str__(self) -> str:
311 string = (
312 f"{self.key_info.path.unparse()} -> {self.key_info.current_key.unparse()}"
313 f" ({self.key_info.index + 1} / {len(self.key_info.path)})"
314 )
315 if self.operate.value is not ConfigOperate.Unknown:
316 string += f" Operate: {self.operate.value}"
317 return string
320class ConfigDataReadOnlyError(TypeError):
321 """
322 配置数据为只读
324 .. versionadded:: 0.1.3
325 """
327 def __init__(self, msg: str | None = None):
328 """
329 :param msg: 错误信息
330 :type msg: str | None
331 """ # noqa: D205
332 if msg is None:
333 msg = "ConfigData is read-only"
334 super().__init__(msg)
337class ConfigDataTypeError(ValueError):
338 """配置数据类型错误"""
340 def __init__(
341 self,
342 key_info: KeyInfo[Any],
343 required_type: tuple[type, ...] | type,
344 current_type: type,
345 ):
346 """
347 :param key_info: 键相关信息
348 :type key_info: KeyInfo
349 :param required_type: 该键需求的数据类型
350 :type required_type: tuple[type, ...] | type
351 :param current_type: 当前键的数据类型
352 :type current_type: type
354 .. versionchanged:: 0.1.4
355 ``required_type`` 支持传入多个需求的数据类型
357 .. versionchanged:: 0.2.0
358 重命名参数 ``now_type`` 为 ``current_type``
359 """ # noqa: D205
360 if isinstance(required_type, Sequence) and (len(required_type) == 1):
361 required_type = required_type[0]
363 self.key_info = key_info
364 self.requited_type = required_type
365 self.current_type = current_type
367 super().__init__(
368 f"{self.key_info.path.unparse()} -> {self.key_info.current_key.unparse()}"
369 f" ({self.key_info.index + 1} / {len(self.key_info.path)})"
370 f" Must be '{self.requited_type}'"
371 f", Not '{self.current_type}'"
372 )
375class CyclicReferenceError(ValueError):
376 """
377 配置数据存在循环引用错误
379 .. versionadded:: 0.2.0
380 """
382 def __init__(self, key_info: KeyInfo[Any]):
383 """
384 :param key_info: 检测到循环引用的键信息
385 :type key_info: KeyInfo[Any]
386 """ # noqa: D205
387 self.key_info = key_info
389 @override
390 def __str__(self) -> str:
391 return (
392 f"Cyclic reference detected at {self.key_info.path.unparse()} -> {self.key_info.current_key.unparse()}"
393 f" ({self.key_info.index + 1}/{len(self.key_info.path)})"
394 )
397class UnknownErrorDuringValidateError(Exception):
398 # noinspection GrazieInspection
399 """
400 在验证配置数据时发生未知错误
402 .. versionchanged:: 0.1.3
403 重命名 ``UnknownErrorDuringValidate`` 为 ``UnknownErrorDuringValidateError``
404 """
406 def __init__(self, *args: Any, **kwargs: Any):
407 """
408 :param args: 未知错误信息
409 :param kwargs: 未知错误信息
410 """ # noqa: D205
411 super().__init__(f"Args: {args}, Kwargs: {kwargs}")
414class UnsupportedConfigFormatError(Exception):
415 """
416 不支持的配置文件格式错误
418 .. note::
419 :py:attr:`format` 可以为 :py:const:`None` 这表示 `未指定配置格式` 。
420 在一些情况下 :py:const:`None` 是有效的配置格式,如表示 `默认` 。
421 此错误以 :py:const:`None` 为参数抛出时表示 `我找到了配置格式None,但是我不支持None作为配置格式`
422 """ # noqa: RUF002
424 def __init__(self, _format: str | None):
425 """
426 :param _format: 不支持的配置的文件格式
427 :type _format: str | None
429 .. versionchanged:: 0.3.0
430 重命名参数 ``format_`` 为 ``_format``
431 更改参数 ``_format`` 类型为 ``str | None``
432 """ # noqa: D205
433 self._format = _format
435 @property
436 def format(self) -> str | None:
437 """不支持的配置的文件格式"""
438 return self._format
440 @override
441 def __str__(self) -> str:
442 if self.format is None:
443 return "Unspecified config format"
444 return f"Unsupported config format: {self._format}"
446 @override
447 def __eq__(self, other: Any) -> bool:
448 return isinstance(other, type(self)) and self._format == other._format
450 @override
451 def __hash__(self) -> int:
452 """.. versionadded:: 0.3.0"""
453 return hash(self._format)
456class FailedProcessConfigFileError[E: Exception](ExceptionGroup):
457 """
458 SL处理器无法正确处理当前配置文件
460 .. versionchanged:: 0.1.4
461 现在继承自 :py:class:`BaseExceptionGroup`
463 .. versionchanged:: 0.3.0
464 现在正确的继承自 :py:class:`ExceptionGroup`
465 移除冗余属性 ``reasons``
466 """
468 @staticmethod
469 def __new__(cls, reason: E | Iterable[E] | Mapping[str, E], msg: str = "Failed to process config file") -> Self:
470 """
471 :param reason: 处理配置文件失败的原因
472 :type reason: E | Iterable[E] | Mapping[str, E]
473 :param msg: 提示信息
474 :type msg: str
476 .. versionchanged:: 0.3.0
477 更改参数 ``reason`` 类型从 :py:class:`BaseException` 改为 :py:class`Exception`
478 """ # noqa: D205
479 message: str
480 exceptions: Sequence[E]
481 if isinstance(reason, Exception):
482 reason: E # type: ignore[no-redef]
483 message = f"{msg}: {reason}"
484 exceptions = (reason,)
485 elif isinstance(reason, Mapping):
486 message = "\n".join((msg, *(f"{k}: {v}" for k, v in reason.items())))
487 exceptions = tuple(reason.values())
488 else:
489 message = "\n".join((msg, *map(str, reason)))
490 exceptions = tuple(reason)
491 return super().__new__(cls, message, exceptions)
494class ComponentMetadataException(LookupError): # noqa: N818
495 """
496 组件元数据异常
498 .. versionadded:: 0.3.0
499 """
502class ComponentMemberMismatchError(ComponentMetadataException):
503 """
504 组件成员元数据与成员不匹配错误
506 .. versionadded:: 0.3.0
507 """
509 def __init__(self, missing: set[str], redundant: set[str]):
510 """
511 :param missing: 缺少的成员
512 :type missing: set[str]
513 :param redundant: 冗余的成员
514 :type redundant: set[str]
515 """ # noqa: D205
516 self.missing = missing
517 self.redundant = redundant
519 @override
520 def __str__(self) -> str:
521 msg = "Component member metadata does not match members"
522 if self.missing:
523 msg += f", Missing members: {self.missing}"
524 if self.redundant:
525 msg += f", Redundant members: {self.redundant}"
526 return msg
529__all__ = (
530 "ComponentMemberMismatchError",
531 "ComponentMetadataException",
532 "ConfigDataPathSyntaxError",
533 "ConfigDataPathSyntaxWarning",
534 "ConfigDataReadOnlyError",
535 "ConfigDataTypeError",
536 "ConfigOperate",
537 "CyclicReferenceError",
538 "DependencyNotFoundError",
539 "FailedProcessConfigFileError",
540 "KeyInfo",
541 "RequiredPathNotFoundError",
542 "TokenInfo",
543 "UnavailableAttribute",
544 "UnknownErrorDuringValidateError",
545 "UnsupportedConfigFormatError",
546)