Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Copyright (c) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET 

2# 

3""" 

4Facilities providing classes for handling configuration for communication protocols 

5and devices. 

6""" 

7 

8import dataclasses 

9import json 

10from abc import ABC, abstractmethod 

11from importlib import import_module 

12from typing import Dict, Sequence 

13 

14from .utils.typing import is_generic_type_hint, check_generic_type 

15 

16 

17def _has_default_value(f: dataclasses.Field): 

18 return not isinstance(f.default, dataclasses._MISSING_TYPE) 

19 

20 

21# Hooks of configdataclass 

22def _clean_values(self): 

23 """ 

24 Cleans and enforces configuration values. Does nothing by default, but may be 

25 overridden to add custom configuration value checks. 

26 """ 

27 

28 

29_configclass_hooks = { 

30 "clean_values": _clean_values, 

31} 

32 

33 

34# Methods of configdataclass 

35def ___post_init__(self): 

36 self._check_types() 

37 self.clean_values() 

38 

39 

40def _force_value(self, fieldname, value): 

41 """ 

42 Forces a value to a dataclass field despite the class being frozen. 

43 

44 NOTE: you can define `post_force_value` method with same signature as this method 

45 to do extra processing after `value` has been forced on `fieldname`. 

46 

47 :param fieldname: name of the field 

48 :param value: value to assign 

49 """ 

50 object.__setattr__(self, fieldname, value) 

51 if hasattr(self, "post_force_value"): 

52 self.post_force_value(fieldname, value) 

53 

54 

55@classmethod # type: ignore 

56def _keys(cls) -> Sequence[str]: 

57 """ 

58 Returns a list of all configdataclass fields key-names. 

59 

60 :return: a list of strings containing all keys. 

61 """ 

62 return [f.name for f in dataclasses.fields(cls)] 

63 

64 

65@classmethod # type: ignore 

66def _required_keys(cls) -> Sequence[str]: 

67 """ 

68 Returns a list of all configdataclass fields, that have no default value assigned 

69 and need to be specified on instantiation. 

70 

71 :return: a list of strings containing all required keys. 

72 """ 

73 return [f.name for f in dataclasses.fields(cls) if not _has_default_value(f)] 

74 

75 

76@classmethod # type: ignore 

77def _optional_defaults(cls) -> Dict[str, object]: 

78 """ 

79 Returns a list of all configdataclass fields, that have a default value assigned 

80 and may be optionally specified on instantiation. 

81 

82 :return: a list of strings containing all optional keys. 

83 """ 

84 return {f.name: f.default for f in dataclasses.fields(cls) if _has_default_value(f)} 

85 

86 

87def __check_types(self): 

88 mod = import_module(self.__module__) 

89 for field in dataclasses.fields(self): 

90 name = field.name 

91 value = getattr(self, name) 

92 type_ = field.type 

93 if isinstance(type_, str): # `from __future__ import annotations` in use 

94 try: 

95 # built-in types 

96 type_ = eval(type_) 

97 except NameError: 

98 # module-level defined type 

99 type_ = getattr(mod, type_) 

100 if is_generic_type_hint(type_): 

101 check_generic_type(value, type_, name=name) 

102 elif not isinstance(value, type_): 

103 raise TypeError( 

104 "Type of field `{}` is `{}` and does not match `{}`.".format( 

105 name, type(value), type_ 

106 ) 

107 ) 

108 

109 

110_configclass_methods = { 

111 "__post_init__": ___post_init__, 

112 "force_value": _force_value, 

113 "keys": _keys, 

114 "required_keys": _required_keys, 

115 "optional_defaults": _optional_defaults, 

116 "_check_types": __check_types, 

117} 

118 

119 

120def configdataclass(direct_decoration=None, frozen=True): 

121 """ 

122 Decorator to make a class a configdataclass. Types in these dataclasses are 

123 enforced. Implement a function clean_values(self) to do additional checking on 

124 value ranges etc. 

125 

126 It is possible to inherit from a configdataclass and re-decorate it with 

127 @configdataclass. In a subclass, default values can be added to existing fields. 

128 Note: adding additional non-default fields is prone to errors, since the order 

129 has to be respected through the whole chain (first non-default fields, only then 

130 default-fields). 

131 

132 :param frozen: defaults to True. False allows to later change configuration values. 

133 Attention: if configdataclass is not frozen and a value is changed, typing is 

134 not enforced anymore! 

135 """ 

136 

137 def decorator(cls): 

138 for name, method in _configclass_methods.items(): 

139 if name in cls.__dict__: 

140 raise AttributeError( 

141 "configdataclass {!r} cannot define {!r} method".format( 

142 cls.__name__, 

143 name, 

144 ) 

145 ) 

146 setattr(cls, name, method) 

147 for name, hook in _configclass_hooks.items(): 

148 if not hasattr(cls, name): 

149 setattr(cls, name, hook) 

150 if not hasattr(cls, "is_configdataclass"): 

151 setattr(cls, "is_configdataclass", True) 

152 

153 return dataclasses.dataclass(cls, frozen=frozen) 

154 

155 if direct_decoration: 

156 return decorator(direct_decoration) 

157 

158 return decorator 

159 

160 

161class ConfigurationMixin(ABC): 

162 """ 

163 Mixin providing configuration to a class. 

164 """ 

165 

166 # omitting type hint of `configuration` on purpose, because type hinting 

167 # configdataclass is not possible. Union[Dict[str, object], object] resolves to 

168 # object. 

169 def __init__(self, configuration) -> None: 

170 """ 

171 Constructor for the configuration mixin. 

172 

173 :param configuration: is the configuration provided either as: 

174 * a dict with string keys and values, then the default config dataclass 

175 will be used 

176 * a configdataclass object 

177 * None, then the config_cls() with no parameters is instantiated 

178 """ 

179 

180 if not configuration: 

181 configuration = {} 

182 

183 if hasattr(configuration, "is_configdataclass"): 

184 self._configuration = configuration 

185 elif isinstance(configuration, Dict): 

186 default_configdataclass = self.config_cls() 

187 if not hasattr(default_configdataclass, "is_configdataclass"): 

188 raise TypeError( 

189 "Default configdataclass is not a configdataclass. Is" 

190 "the decorator `@configdataclass` applied?" 

191 ) 

192 self._configuration = default_configdataclass(**configuration) 

193 else: 

194 raise TypeError("configuration is not a dictionary or configdataclass.") 

195 

196 @staticmethod 

197 @abstractmethod 

198 def config_cls(): 

199 """ 

200 Return the default configdataclass class. 

201 

202 :return: a reference to the default configdataclass class 

203 """ 

204 

205 @property 

206 def config(self): 

207 """ 

208 ConfigDataclass property. 

209 

210 :return: the configuration 

211 """ 

212 

213 return self._configuration 

214 

215 @classmethod 

216 def from_json(cls, filename: str): 

217 """ 

218 Instantiate communication protocol using configuration from a JSON file. 

219 

220 :param filename: Path and filename to the JSON configuration 

221 """ 

222 

223 configuration = cls._configuration_load_json(filename) 

224 return cls(configuration) 

225 

226 def configuration_save_json(self, path: str) -> None: 

227 """ 

228 Save current configuration as JSON file. 

229 

230 :param path: path to the JSON file. 

231 """ 

232 

233 self._configuration_save_json(dataclasses.asdict(self._configuration), path) 

234 

235 @staticmethod 

236 def _configuration_load_json(path: str) -> Dict[str, object]: 

237 """ 

238 Load configuration from JSON file and return Dict. This method is only used 

239 during construction, if not directly a configuration is given but rather a 

240 path to a JSON config file. 

241 

242 :param path: Path to the JSON configuration file. 

243 :return: Dictionary containing the parameters read from the JSON file. 

244 """ 

245 

246 with open(path, "r") as fp: 

247 return json.load(fp) 

248 

249 @staticmethod 

250 def _configuration_save_json(configuration: Dict[str, object], path: str) -> None: 

251 """ 

252 Store a configuration dict to a JSON file. 

253 

254 :param configuration: configuration dictionary 

255 :param path: path to the JSON file. 

256 """ 

257 

258 with open(path, "w") as fp: 

259 json.dump(configuration, fp, indent=4) 

260 

261 

262@configdataclass 

263class EmptyConfig: 

264 """ 

265 Empty configuration dataclass. 

266 """