Coverage for src / c41811 / config / basic / mapping.py: 100%
113 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-06 06:04 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-06 06:04 +0000
1# cython: language_level = 3 # noqa: ERA001
4"""
5映射类型配置数据实现
7.. versionadded:: 0.2.0
8"""
10import operator
11from collections import OrderedDict
12from collections.abc import Generator
13from collections.abc import ItemsView
14from collections.abc import KeysView
15from collections.abc import Mapping
16from collections.abc import MutableMapping
17from collections.abc import ValuesView
18from copy import deepcopy
19from typing import Any
20from typing import Self
21from typing import cast
22from typing import override
24from ._generate_operators import generate
25from ._generate_operators import operate
26from .core import BasicIndexedConfigData
27from .utils import check_read_only
28from .utils import fmt_path
29from ..abc import PathLike
30from ..errors import CyclicReferenceError
31from ..errors import KeyInfo
32from ..errors import RequiredPathNotFoundError
33from ..path import AttrKey
34from ..path import Path
35from ..utils import Unset
38def _keys_recursive(
39 data: Mapping[Any, Any],
40 seen: set[int] | None = None,
41 *,
42 strict: bool,
43 end_point_only: bool,
44) -> Generator[str, None, None]:
45 """
46 递归获取配置的键
48 :param data: 配置数据
49 :type data: Mapping
50 :param seen: 已访问的配置数据的id
51 :type seen: set[int] | None
52 :param strict: 是否严格模式,如果为 True,则当遇到循环引用时,会抛出异常
53 :type strict: bool
54 :param end_point_only: 是否只返回叶子节点的键
55 :type end_point_only: bool
57 :return: 获取的生成器
58 :rtype: Generator[str, None, None]
60 :raises CyclicReferenceError: 当遇到循环引用时,如果 strict 为 True,则抛出此异常
61 :raises TypeError: 递归获取时键不为str时抛出
63 .. versionadded:: 0.2.0
64 """ # noqa: RUF002
65 if seen is None:
66 seen = set()
68 if id(data) in seen:
69 if strict:
70 # noinspection PyTypeChecker
71 raise CyclicReferenceError(key_info=KeyInfo(Path([]), None, -1))
72 return
73 seen.add(id(data))
75 for k, v in data.items():
76 if not isinstance(k, str):
77 msg = f"key must be str, not {type(k).__name__}"
78 raise TypeError(msg)
79 k = k.replace("\\", "\\\\") # noqa: PLW2901
80 if isinstance(v, Mapping):
81 try:
82 yield from (
83 f"{k}\\.{x}" for x in _keys_recursive(v, seen, strict=strict, end_point_only=end_point_only)
84 )
85 except CyclicReferenceError as err:
86 key_info = err.key_info
87 key = AttrKey(k)
89 key_info.path = Path((key, *key_info.path))
90 key_info.current_key = key if key_info.current_key is None else key_info.current_key
91 key_info.index += 1
92 raise
93 if end_point_only:
94 continue
95 yield k
96 seen.remove(id(data))
99@generate
100class MappingConfigData[D: Mapping[Any, Any]](BasicIndexedConfigData[D], MutableMapping[Any, Any]):
101 """
102 映射配置数据
104 .. versionadded:: 0.1.5
105 """
107 _data: D
108 data: D
110 def __init__(self, data: D | None = None):
111 """
112 :param data: 映射数据
113 :type data: D | None
114 """ # noqa: D205
115 if data is None:
116 data = {} # type: ignore[assignment]
117 super().__init__(cast(D, data))
119 @property
120 @override
121 def data_read_only(self) -> bool:
122 return not isinstance(self._data, MutableMapping)
124 @override
125 def keys(self, *, recursive: bool = False, strict: bool = True, end_point_only: bool = False) -> KeysView[Any]:
126 # noinspection GrazieInspection
127 r"""
128 获取所有键
130 不为 :py:class:`~collections.abc.Mapping` 默认行为时键必须为 :py:class:`str` 且返回值会被转换为
131 :ref:`配置数据路径字符串 <term-config-data-path-syntax>`
133 :param recursive: 是否递归获取
134 :type recursive: bool
135 :param strict: 是否严格检查循环引用数据,为真时提前抛出错误,否则静默忽略
136 :type strict: bool
137 :param end_point_only: 是否只获取叶子节点
138 :type end_point_only: bool
140 :return: 所有键
141 :rtype: KeysView[str]
143 :raise TypeError: 递归获取时键不为str时抛出
144 :raise CyclicReferenceError: 严格检查循环引用数据时发现循环引用抛出
146 例子
147 ----
149 >>> from c41811.config import MappingConfigData
150 >>> data = MappingConfigData({"foo": {"bar": {"baz": "value"}, "bar1": "value1"}, "foo1": "value2"})
152 不带参数行为与普通字典一样
154 >>> data.keys()
155 dict_keys(['foo', 'foo1'])
157 参数 ``end_point_only`` 会滤掉非 ``叶子节点`` 的键
159 >>> data.keys(end_point_only=True) # 内部计算为保留顺序采用了OrderedDict所以返回值是odict_keys
160 odict_keys(['foo1'])
162 参数 ``recursive`` 用于获取所有的 ``路径``
164 >>> data.keys(recursive=True)
165 odict_keys(['foo\\.bar\\.baz', 'foo\\.bar', 'foo\\.bar1', 'foo', 'foo1'])
167 同时提供 ``recursive`` 和 ``end_point_only`` 会产出所有 ``叶子节点`` 的路径
169 >>> data.keys(recursive=True, end_point_only=True)
170 odict_keys(['foo\\.bar\\.baz', 'foo\\.bar1', 'foo1'])
172 为严格模式时会检查循环引用并提前引发错误
174 >>> cyclic: dict[str, Any] = {"cyclic": None, "key": "value"}
175 >>> cyclic["cyclic"] = cyclic
176 >>> cyclic: MappingConfigData[dict[str, Any]] = MappingConfigData(cyclic)
178 >>> cyclic.keys(recursive=True) # 默认为严格模式
179 Traceback (most recent call last):
180 ...
181 c41811.config.errors.CyclicReferenceError: Cyclic reference detected at \.cyclic -> \.cyclic (1/1)
183 否则静默跳过循环引用
185 >>> cyclic.keys(recursive=True, strict=False)
186 odict_keys(['cyclic', 'key'])
188 >>> cyclic.keys(recursive=True, strict=False, end_point_only=True)
189 odict_keys(['key'])
191 .. versionchanged:: 0.2.0
192 添加参数 ``strict``
193 """ # noqa: RUF002
194 if recursive:
195 return OrderedDict.fromkeys(
196 x for x in _keys_recursive(self._data, strict=strict, end_point_only=end_point_only)
197 ).keys()
199 if end_point_only:
200 return OrderedDict.fromkeys(
201 k.replace("\\", "\\\\") for k, v in self._data.items() if not isinstance(v, Mapping)
202 ).keys()
204 return self._data.keys()
206 @override
207 def values(self, return_raw_value: bool = False) -> ValuesView[Any]:
208 """
209 获取所有值
211 :param return_raw_value: 是否获取原始数据
212 :type return_raw_value: bool
214 :return: 所有键值对
215 :rtype: ValuesView[Any]
217 .. versionchanged:: 0.2.0
218 重命名参数 ``get_raw`` 为 ``return_raw_value``
219 """
220 if return_raw_value:
221 return self._data.values()
223 return OrderedDict(
224 (k, self.from_data(v) if isinstance(v, Mapping) else deepcopy(v)) for k, v in self._data.items()
225 ).values()
227 @override
228 def items(self, *, return_raw_value: bool = False) -> ItemsView[str, Any]:
229 """
230 获取所有键值对
232 :param return_raw_value: 是否获取原始数据
233 :type return_raw_value: bool
235 :return: 所有键值对
236 :rtype: ItemsView[str, Any]
238 .. versionchanged:: 0.2.0
239 重命名参数 ``get_raw`` 为 ``return_raw_value``
240 """
241 if return_raw_value:
242 return self._data.items()
243 return OrderedDict(
244 (deepcopy(k), self.from_data(v) if isinstance(v, Mapping) else deepcopy(v)) for k, v in self._data.items()
245 ).items()
247 @override
248 @check_read_only
249 def clear(self) -> None:
250 self._data.clear() # type: ignore[attr-defined]
252 @override
253 @check_read_only
254 def pop(self, path: PathLike, /, default: Any = Unset) -> Any:
255 path = fmt_path(path)
256 try:
257 result = self.retrieve(path)
258 self.delete(path)
259 except RequiredPathNotFoundError:
260 if default is not Unset:
261 return default
262 raise
263 return result
265 @override
266 @check_read_only
267 def popitem(self) -> Any:
268 return self._data.popitem() # type: ignore[attr-defined]
270 @override
271 @check_read_only
272 def update(self, m: Any = None, /, **kwargs: Any) -> None:
273 if m is not None:
274 self._data.update(m) # type: ignore[attr-defined]
275 return
276 self._data.update(**kwargs) # type: ignore[attr-defined]
278 def __getattr__(self, item: Any) -> Self | Any:
279 try:
280 return self[item]
281 except KeyError:
282 msg = f"'{self.__class__.__name__}' object has no attribute '{item}'"
283 raise AttributeError(msg) from None
285 @operate(operator.or_, operator.ior)
286 def __or__(self, other: Any) -> Self: # type: ignore[empty-body]
287 ...
289 def __ror__(self, other: Any) -> Self: # type: ignore[empty-body]
290 ...
293__all__ = ("MappingConfigData",)