Coverage for src / c41811 / config / basic / environment.py: 100%

108 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-06 06:04 +0000

1# cython: language_level = 3 # noqa: ERA001 

2 

3 

4""" 

5环境变量配置数据实现 

6 

7.. versionadded:: 0.2.0 

8""" 

9 

10from collections.abc import Callable 

11from collections.abc import Iterable 

12from collections.abc import MutableMapping 

13from dataclasses import dataclass 

14from dataclasses import field 

15from functools import update_wrapper 

16from typing import Any 

17from typing import Self 

18from typing import cast 

19from typing import override 

20 

21import wrapt 

22 

23from .mapping import MappingConfigData 

24from ..abc import PathLike 

25from ..utils import Unset 

26 

27 

28@dataclass 

29class Difference: 

30 """与初始化数据的差异""" 

31 

32 updated: set[str] = field(default_factory=set) 

33 """ 

34 修改/新增的键 

35 

36 .. note:: 

37 实现的不是很完美,如果一个键更改为了另一个值再改回来仍然会被认为是被修改过的键 

38 """ # noqa: RUF001 

39 removed: set[str] = field(default_factory=set) 

40 """ 

41 删除的键 

42 """ 

43 

44 def clear(self) -> None: 

45 """清空差异""" 

46 self.updated.clear() 

47 self.removed.clear() 

48 

49 def __iadd__(self, other: Any) -> Self: 

50 if not isinstance(other, Iterable): 

51 return NotImplemented 

52 other = set(other) 

53 self.updated |= other 

54 self.removed -= other 

55 return self 

56 

57 def __isub__(self, other: Any) -> Self: 

58 if not isinstance(other, Iterable): 

59 return NotImplemented 

60 other = set(other) 

61 self.updated -= other 

62 self.removed |= other 

63 return self 

64 

65 def __bool__(self) -> bool: 

66 return bool(self.updated and self.removed) 

67 

68 

69# noinspection PyNewStyleGenericSyntax 

70def diff_keys[F: Callable[..., Any]](func: F) -> F: 

71 """ 

72 计算执行方法前后配置数据的键差异 

73 

74 :param func: 方法 

75 :type func: F 

76 

77 :return: 方法 

78 :rtype: F 

79 """ 

80 

81 @wrapt.decorator # type: ignore[arg-type] 

82 def wrapper(wrapped: F, instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: 

83 if not isinstance(instance, EnvironmentConfigData): 

84 msg = f"instance must be {EnvironmentConfigData.__name__} but got {type(instance).__name__}" 

85 raise TypeError(msg) 

86 

87 before = set(instance.keys()) 

88 before_never_changed = before - instance.difference.updated - instance.difference.removed 

89 may_change = {k: instance[k] for k in before_never_changed} 

90 

91 result = wrapped(*args, **kwargs) 

92 after = set(instance.keys()) 

93 

94 added = after - before 

95 deleted = before - after 

96 

97 instance.difference += added 

98 instance.difference -= deleted 

99 

100 current_never_changed = before_never_changed - added - deleted 

101 for may_changed in current_never_changed: 

102 if may_change[may_changed] != instance[may_changed]: 

103 instance.difference += {may_changed} 

104 

105 return result 

106 

107 return cast(F, update_wrapper(wrapper(func), func)) 

108 

109 

110class EnvironmentConfigData(MappingConfigData[MutableMapping[str, str]]): 

111 """ 

112 环境变量配置数据 

113 

114 内部维护了与初始化参数的键差异 

115 

116 .. note:: 

117 :py:class:`~config.processor.OSEnv.OSEnvSL` 在保存时会重置差异数据 

118 

119 .. warning:: 

120 当前实现 `不会验证值的类型` ,当值不是字符串类型时,在实际写入 :py:data:`os.environ` 时会抛出错误, 

121 请确保传入的值是字符串类型 

122 """ # noqa: RUF002 

123 

124 def __init__(self, data: MutableMapping[str, str] | None = None): 

125 """ 

126 :param data: 环境变量数据 

127 :type data: MutableMapping[str, str] | None 

128 """ # noqa: D205 

129 super().__init__(data) 

130 self.difference = Difference() 

131 

132 @diff_keys 

133 @override 

134 def modify(self, path: PathLike, value: str, *, allow_create: bool = True) -> Self: 

135 return super().modify(path, value, allow_create=allow_create) 

136 

137 @diff_keys 

138 @override 

139 def delete(self, path: PathLike) -> Self: 

140 return super().delete(path) 

141 

142 @diff_keys 

143 @override 

144 def unset(self, path: PathLike) -> Self: 

145 return super().unset(path) 

146 

147 @diff_keys 

148 @override 

149 def setdefault[V](self, path: PathLike, default: V | None = None, *, return_raw_value: bool = False) -> V | Any: 

150 return super().setdefault(path, default, return_raw_value=return_raw_value) 

151 

152 @diff_keys 

153 @override 

154 def clear(self) -> None: 

155 super().clear() 

156 

157 @diff_keys 

158 @override 

159 def pop(self, path: PathLike, /, default: Any = Unset) -> Any: 

160 return super().pop(path, default) 

161 

162 @diff_keys 

163 @override 

164 def popitem(self) -> Any: 

165 return super().popitem() 

166 

167 @diff_keys 

168 @override 

169 def update(self, m: Any | None = None, /, **kwargs: str) -> None: 

170 super().update(m, **kwargs) 

171 

172 @diff_keys 

173 @override 

174 def __setitem__(self, index: str, value: str) -> None: 

175 super().__setitem__(index, value) 

176 

177 @diff_keys 

178 @override 

179 def __delitem__(self, index: str) -> None: 

180 super().__delitem__(index) 

181 

182 @diff_keys 

183 def __ior__(self, other: MutableMapping[str, str]) -> Self: 

184 return super().__ior__(other) # type: ignore[misc, no-any-return] 

185 

186 

187__all__ = ( 

188 "Difference", 

189 "EnvironmentConfigData", 

190)