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

1# cython: language_level = 3 # noqa: ERA001 

2 

3 

4"""错误类""" 

5 

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 

18 

19from .abc import ABCPath 

20from .abc import AnyKey 

21 

22 

23class DependencyNotFoundError(ImportError): 

24 """ 

25 依赖缺失 

26 

27 .. versionadded:: 0.3.0 

28 """ 

29 

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)) 

46 

47 

48# noinspection PyNewStyleGenericSyntax 

49def _unavailable_method[M: Callable[..., Never]](method: M) -> M: 

50 """ 

51 被装饰的方法在调用时会抛出实例 :py:attr:`~UnavailableAttribute._exception` 上的异常 

52 

53 :param method: 被装饰的方法 

54 :type method: M 

55 :return: 被装饰的方法 

56 :rtype: M 

57 

58 .. versionadded:: 0.3.0 

59 """ 

60 

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") 

65 

66 return cast(M, wrapper) 

67 

68 

69class UnavailableAttribute: 

70 """ 

71 占位代理对象,在任意访问时抛出异常 

72 

73 .. versionadded:: 0.3.0 

74 """ # noqa: RUF002 

75 

76 __slots__ = ("_name", "_reason") 

77 

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) 

87 

88 @_unavailable_method 

89 @override 

90 def __getattribute__(self, name: str) -> Never: ... # type: ignore[empty-body] 

91 

92 @_unavailable_method 

93 @override 

94 def __setattr__(self, name: str, value: Any) -> Never: ... # type: ignore[empty-body] 

95 

96 @_unavailable_method 

97 @override 

98 def __delattr__(self, name: str) -> Never: ... # type: ignore[empty-body] 

99 

100 @_unavailable_method 

101 def __call__(self, *args: Any, **kwargs: Any) -> Never: ... # type: ignore[empty-body] # noqa: D102 

102 

103 @_unavailable_method 

104 def __getitem__(self, item: Any) -> Never: ... # type: ignore[empty-body] 

105 

106 @_unavailable_method 

107 def __setitem__(self, key: Any, value: Any) -> Never: ... # type: ignore[empty-body] 

108 

109 @_unavailable_method 

110 def __delitem__(self, key: Any) -> Never: ... # type: ignore[empty-body] 

111 

112 @_unavailable_method 

113 def __iter__(self) -> Never: ... # type: ignore[empty-body] 

114 

115 @_unavailable_method 

116 def __next__(self) -> Never: ... # type: ignore[empty-body] 

117 

118 @_unavailable_method 

119 def __get__(self, instance: object, owner: type[object]) -> Never: ... # type: ignore[empty-body] 

120 

121 @_unavailable_method 

122 def __set__(self, instance: object, value: object) -> Never: ... # type: ignore[empty-body] 

123 

124 @_unavailable_method 

125 def __delete__(self, instance: object) -> Never: ... # type: ignore[empty-body] 

126 

127 @_unavailable_method 

128 def __copy__(self) -> Never: ... # type: ignore[empty-body] 

129 

130 @_unavailable_method 

131 def __deepcopy__(self, memo: dict[object, object]) -> Never: ... # type: ignore[empty-body] 

132 

133 @_unavailable_method 

134 @override 

135 def __hash__(self) -> Never: ... # type: ignore[empty-body] 

136 

137 @_unavailable_method 

138 def __bool__(self) -> Never: ... # type: ignore[empty-body] 

139 

140 @_unavailable_method 

141 def __len__(self) -> Never: ... # type: ignore[empty-body] 

142 

143 @_unavailable_method 

144 def __contains__(self, item: object) -> Never: ... # type: ignore[empty-body] 

145 

146 @override 

147 def __repr__(self) -> str: 

148 return f"<{type(self).__name__} {object.__getattribute__(self, '_name')}>" 

149 

150 

151@dataclass(frozen=True) 

152class TokenInfo: 

153 """ 

154 一段标记的相关信息 用于快速定位到指定标记 

155 

156 .. versionchanged:: 0.3.2 

157 字段 ``current_token`` 由传入变为自动推导 

158 

159 字段 ``index`` 将始终为正数 

160 

161 变更为不可变类型 

162 """ 

163 

164 tokens: tuple[str, ...] 

165 """ 

166 当前完整标记列表 

167 """ 

168 index: int 

169 """ 

170 current_token在tokens的下标 

171 """ 

172 

173 def __post_init__(self) -> None: 

174 if self.index < 0: 

175 object.__setattr__(self, "index", self.index + len(self.tokens)) 

176 

177 @property 

178 def current_token(self) -> str: 

179 """当前标记""" 

180 return self.tokens[self.index] 

181 

182 @property 

183 def raw_string(self) -> str: 

184 """标记的原始字符串""" 

185 return "".join(self.tokens) 

186 

187 

188class ConfigDataPathSyntaxWarning(SyntaxWarning): 

189 """ 

190 配置数据检索路径语法警告 

191 

192 .. versionadded:: 0.3.2 

193 """ 

194 

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 

204 

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 ) 

212 

213 

214class ConfigDataPathSyntaxError(Exception): 

215 """ 

216 配置数据检索路径语法错误 

217 

218 .. versionadded:: 0.3.2 

219 """ 

220 

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 

230 

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 ) 

238 

239 

240class ConfigOperate(Enum): 

241 """对配置的操作类型""" 

242 

243 Delete = "Delete" 

244 Read = "Read" 

245 Write = "Write" 

246 Unknown = None 

247 

248 

249@dataclass(frozen=True) 

250class KeyInfo[K: AnyKey]: 

251 """ 

252 一段路径的相关信息 用于快速定位到指定键 

253 

254 .. versionchanged:: 0.3.2 

255 字段 ``current_key`` 由传入变为自动推导 

256 

257 字段 ``index`` 将始终为正数 

258 

259 变更为不可变类型 

260 """ 

261 

262 path: ABCPath[K] 

263 """ 

264 当前完整路径 

265 """ 

266 

267 index: int 

268 """ 

269 current_key在path的下标 

270 """ 

271 

272 def __post_init__(self) -> None: 

273 if self.index < 0: 

274 object.__setattr__(self, "index", self.index + len(self.path)) 

275 

276 @property 

277 def current_key(self) -> K: 

278 """当前键""" 

279 return self.path[self.index] 

280 

281 @property 

282 def relative_keys(self) -> Iterable[K]: 

283 """从根到当前键的相对路径""" 

284 return self.path[: self.index] 

285 

286 

287class RequiredPathNotFoundError(LookupError): 

288 """ 

289 需求的键未找到错误 

290 

291 .. versionchanged:: 0.1.5 

292 现在继承自LookupError 

293 """ 

294 

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) 

308 

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 

318 

319 

320class ConfigDataReadOnlyError(TypeError): 

321 """ 

322 配置数据为只读 

323 

324 .. versionadded:: 0.1.3 

325 """ 

326 

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) 

335 

336 

337class ConfigDataTypeError(ValueError): 

338 """配置数据类型错误""" 

339 

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 

353 

354 .. versionchanged:: 0.1.4 

355 ``required_type`` 支持传入多个需求的数据类型 

356 

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] 

362 

363 self.key_info = key_info 

364 self.requited_type = required_type 

365 self.current_type = current_type 

366 

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 ) 

373 

374 

375class CyclicReferenceError(ValueError): 

376 """ 

377 配置数据存在循环引用错误 

378 

379 .. versionadded:: 0.2.0 

380 """ 

381 

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 

388 

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 ) 

395 

396 

397class UnknownErrorDuringValidateError(Exception): 

398 # noinspection GrazieInspection 

399 """ 

400 在验证配置数据时发生未知错误 

401 

402 .. versionchanged:: 0.1.3 

403 重命名 ``UnknownErrorDuringValidate`` 为 ``UnknownErrorDuringValidateError`` 

404 """ 

405 

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}") 

412 

413 

414class UnsupportedConfigFormatError(Exception): 

415 """ 

416 不支持的配置文件格式错误 

417 

418 .. note:: 

419 :py:attr:`format` 可以为 :py:const:`None` 这表示 `未指定配置格式` 。 

420 在一些情况下 :py:const:`None` 是有效的配置格式,如表示 `默认` 。 

421 此错误以 :py:const:`None` 为参数抛出时表示 `我找到了配置格式None,但是我不支持None作为配置格式` 

422 """ # noqa: RUF002 

423 

424 def __init__(self, _format: str | None): 

425 """ 

426 :param _format: 不支持的配置的文件格式 

427 :type _format: str | None 

428 

429 .. versionchanged:: 0.3.0 

430 重命名参数 ``format_`` 为 ``_format`` 

431 更改参数 ``_format`` 类型为 ``str | None`` 

432 """ # noqa: D205 

433 self._format = _format 

434 

435 @property 

436 def format(self) -> str | None: 

437 """不支持的配置的文件格式""" 

438 return self._format 

439 

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}" 

445 

446 @override 

447 def __eq__(self, other: Any) -> bool: 

448 return isinstance(other, type(self)) and self._format == other._format 

449 

450 @override 

451 def __hash__(self) -> int: 

452 """.. versionadded:: 0.3.0""" 

453 return hash(self._format) 

454 

455 

456class FailedProcessConfigFileError[E: Exception](ExceptionGroup): 

457 """ 

458 SL处理器无法正确处理当前配置文件 

459 

460 .. versionchanged:: 0.1.4 

461 现在继承自 :py:class:`BaseExceptionGroup` 

462 

463 .. versionchanged:: 0.3.0 

464 现在正确的继承自 :py:class:`ExceptionGroup` 

465 移除冗余属性 ``reasons`` 

466 """ 

467 

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 

475 

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) 

492 

493 

494class ComponentMetadataException(LookupError): # noqa: N818 

495 """ 

496 组件元数据异常 

497 

498 .. versionadded:: 0.3.0 

499 """ 

500 

501 

502class ComponentMemberMismatchError(ComponentMetadataException): 

503 """ 

504 组件成员元数据与成员不匹配错误 

505 

506 .. versionadded:: 0.3.0 

507 """ 

508 

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 

518 

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 

527 

528 

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)