Coverage for python/lsst/pex/config/wrap.py: 55%

115 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-17 09:34 +0000

1# This file is part of pex_config. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28__all__ = ("wrap", "makeConfigClass") 

29 

30import importlib 

31import inspect 

32import re 

33 

34from .callStack import StackFrame, getCallerFrame, getCallStack 

35from .config import Config, Field 

36from .configField import ConfigField 

37from .listField import List, ListField 

38 

39_dtypeMap = { 

40 "bool": bool, 

41 "int": int, 

42 "double": float, 

43 "float": float, 

44 "std::int64_t": int, 

45 "std::string": str, 

46} 

47"""Mapping from C++ types to Python type (`dict`) 

48 

49Tassumes we can round-trip between these using the usual pybind11 converters, 

50but doesn't require they be binary equivalent under-the-hood or anything. 

51""" 

52 

53_containerRegex = re.compile(r"(std::)?(vector|list)<\s*(?P<type>[a-z0-9_:]+)\s*>") 

54 

55 

56def makeConfigClass(ctrl, name=None, base=Config, doc=None, module=None, cls=None): 

57 """Create a `~lsst.pex.config.Config` class that matches a C++ control 

58 object class. 

59 

60 See the `wrap` decorator as a convenient interface to ``makeConfigClass``. 

61 

62 Parameters 

63 ---------- 

64 ctrl : class 

65 C++ control class to wrap. 

66 name : `str`, optional 

67 Name of the new config class; defaults to the ``__name__`` of the 

68 control class with ``'Control'`` replaced with ``'Config'``. 

69 base : `lsst.pex.config.Config`-type, optional 

70 Base class for the config class. 

71 doc : `str`, optional 

72 Docstring for the config class. 

73 module : object, `str`, `int`, or `None` optional 

74 Either a module object, a string specifying the name of the module, or 

75 an integer specifying how far back in the stack to look for the module 

76 to use: 0 is the immediate caller of `~lsst.pex.config.wrap`. This will 

77 be used to set ``__module__`` for the new config class, and the class 

78 will also be added to the module. Ignored if `None` or if ``cls`` is 

79 not `None`. Defaults to None in which case module is looked up from the 

80 module of ctrl. 

81 cls : class 

82 An existing config class to use instead of creating a new one; name, 

83 base doc, and module will be ignored if this is not `None`. 

84 

85 Notes 

86 ----- 

87 To use ``makeConfigClass``, write a control object in C++ using the 

88 ``LSST_CONTROL_FIELD`` macro in ``lsst/pex/config.h`` (note that it must 

89 have sensible default constructor): 

90 

91 .. code-block:: cpp 

92 

93 // myHeader.h 

94 

95 struct InnerControl { 

96 LSST_CONTROL_FIELD(wim, std::string, 

97 "documentation for field 'wim'"); 

98 }; 

99 

100 struct FooControl { 

101 LSST_CONTROL_FIELD(bar, int, "documentation for field 'bar'"); 

102 LSST_CONTROL_FIELD(baz, double, "documentation for field 'baz'"); 

103 LSST_NESTED_CONTROL_FIELD(zot, myWrappedLib, InnerControl, 

104 "documentation for field 'zot'"); 

105 

106 FooControl() : bar(0), baz(0.0) {} 

107 }; 

108 

109 You can use ``LSST_NESTED_CONTROL_FIELD`` to nest control objects. Wrap 

110 those control objects as you would any other C++ class, but make sure you 

111 include ``lsst/pex/config.h`` before including the header file where 

112 the control object class is defined. 

113 

114 Next, in Python: 

115 

116 .. code-block:: py 

117 

118 import lsst.pex.config 

119 import myWrappedLib 

120 

121 InnerConfig = lsst.pex.config.makeConfigClass(myWrappedLib.InnerControl) 

122 FooConfig = lsst.pex.config.makeConfigClass(myWrappedLib.FooControl) 

123 

124 This does the following things: 

125 

126 - Adds ``bar``, ``baz``, and ``zot`` fields to ``FooConfig``. 

127 - Set ``FooConfig.Control`` to ``FooControl``. 

128 - Adds ``makeControl`` and ``readControl`` methods to create a 

129 ``FooControl`` and set the ``FooConfig`` from the ``FooControl``, 

130 respectively. 

131 - If ``FooControl`` has a ``validate()`` member function, 

132 a custom ``validate()`` method will be added to ``FooConfig`` that uses 

133 it. 

134 

135 All of the above are done for ``InnerConfig`` as well. 

136 

137 Any field that would be injected that would clash with an existing 

138 attribute of the class is be silently ignored. This allows you to 

139 customize fields and inherit them from wrapped control classes. However, 

140 these names are still be processed when converting between config and 

141 control classes, so they should generally be present as base class fields 

142 or other instance attributes or descriptors. 

143 

144 While ``LSST_CONTROL_FIELD`` will work for any C++ type, automatic 

145 `~lsst.pex.config.Config` generation only supports ``bool``, ``int``, 

146 ``std::int64_t``, ``double``, and ``std::string`` fields, along with 

147 ``std::list`` and ``std::vectors`` of those types. 

148 

149 See also 

150 -------- 

151 wrap 

152 """ 

153 if name is None: 153 ↛ 157line 153 didn't jump to line 157, because the condition on line 153 was never false

154 if "Control" not in ctrl.__name__: 154 ↛ 155line 154 didn't jump to line 155, because the condition on line 154 was never true

155 raise ValueError("Cannot guess appropriate Config class name for %s." % ctrl) 

156 name = ctrl.__name__.replace("Control", "Config") 

157 if cls is None: 157 ↛ 179line 157 didn't jump to line 179, because the condition on line 157 was never false

158 cls = type(name, (base,), {"__doc__": doc}) 

159 if module is not None: 159 ↛ 176line 159 didn't jump to line 176, because the condition on line 159 was never false

160 # Not only does setting __module__ make Python pretty-printers 

161 # more useful, it's also necessary if we want to pickle Config 

162 # objects. 

163 if isinstance(module, int): 163 ↛ 164line 163 didn't jump to line 164, because the condition on line 163 was never true

164 frame = getCallerFrame(module) 

165 moduleObj = inspect.getmodule(frame) 

166 moduleName = moduleObj.__name__ 

167 elif isinstance(module, str): 167 ↛ 168line 167 didn't jump to line 168, because the condition on line 167 was never true

168 moduleName = module 

169 moduleObj = __import__(moduleName) 

170 else: 

171 moduleObj = module 

172 moduleName = moduleObj.__name__ 

173 cls.__module__ = moduleName 

174 setattr(moduleObj, name, cls) 

175 else: 

176 cls.__module__ = ctrl.__module__ 

177 moduleName = ctrl.__module__ 

178 else: 

179 moduleName = cls.__module__ 

180 if doc is None: 180 ↛ 182line 180 didn't jump to line 182, because the condition on line 180 was never false

181 doc = ctrl.__doc__ 

182 fields = {} 

183 # loop over all class attributes, looking for the special static methods 

184 # that indicate a field defined by one of the macros in pex/config.h. 

185 for attr in dir(ctrl): 

186 if attr.startswith("_type_"): 

187 k = attr[len("_type_") :] 

188 getDoc = "_doc_" + k 

189 getModule = "_module_" + k 

190 getType = attr 

191 if hasattr(ctrl, k) and hasattr(ctrl, getDoc): 191 ↛ 185line 191 didn't jump to line 185, because the condition on line 191 was never false

192 doc = getattr(ctrl, getDoc)() 

193 ctype = getattr(ctrl, getType)() 

194 if hasattr(ctrl, getModule): # if this is present, it's a nested control object 

195 nestedModuleName = getattr(ctrl, getModule)() 

196 if nestedModuleName == moduleName: 196 ↛ 199line 196 didn't jump to line 199, because the condition on line 196 was never false

197 nestedModuleObj = moduleObj 

198 else: 

199 nestedModuleObj = importlib.import_module(nestedModuleName) 

200 try: 

201 dtype = getattr(nestedModuleObj, ctype).ConfigClass 

202 except AttributeError: 

203 raise AttributeError("'%s.%s.ConfigClass' does not exist" % (moduleName, ctype)) 

204 fields[k] = ConfigField(doc=doc, dtype=dtype) 

205 else: 

206 try: 

207 dtype = _dtypeMap[ctype] 

208 FieldCls = Field 

209 except KeyError: 

210 dtype = None 

211 m = _containerRegex.match(ctype) 

212 if m: 212 ↛ 215line 212 didn't jump to line 215, because the condition on line 212 was never false

213 dtype = _dtypeMap.get(m.group("type"), None) 

214 FieldCls = ListField 

215 if dtype is None: 215 ↛ 216line 215 didn't jump to line 216, because the condition on line 215 was never true

216 raise TypeError("Could not parse field type '%s'." % ctype) 

217 fields[k] = FieldCls(doc=doc, dtype=dtype, optional=True) 

218 

219 # Define a number of methods to put in the new Config class. Note that 

220 # these are "closures"; they have access to local variables defined in 

221 # the makeConfigClass function (like the fields dict). 

222 def makeControl(self): 

223 """Construct a C++ Control object from this Config object. 

224 

225 Fields set to `None` will be ignored, and left at the values defined 

226 by the Control object's default constructor. 

227 """ 

228 r = self.Control() 

229 for k, f in fields.items(): 

230 value = getattr(self, k) 

231 if isinstance(f, ConfigField): 

232 value = value.makeControl() 

233 if value is not None: 

234 if isinstance(value, List): 

235 setattr(r, k, value._list) 

236 else: 

237 setattr(r, k, value) 

238 return r 

239 

240 def readControl(self, control, __at=None, __label="readControl", __reset=False): 

241 """Read values from a C++ Control object and assign them to self's 

242 fields. 

243 

244 Parameters 

245 ---------- 

246 control 

247 C++ Control object. 

248 

249 Notes 

250 ----- 

251 The ``__at``, ``__label``, and ``__reset`` arguments are for internal 

252 use only; they are used to remove internal calls from the history. 

253 """ 

254 if __at is None: 

255 __at = getCallStack() 

256 values = {} 

257 for k, f in fields.items(): 

258 if isinstance(f, ConfigField): 

259 getattr(self, k).readControl(getattr(control, k), __at=__at, __label=__label, __reset=__reset) 

260 else: 

261 values[k] = getattr(control, k) 

262 if __reset: 

263 self._history = {} 

264 self.update(__at=__at, __label=__label, **values) 

265 

266 def validate(self): 

267 """Validate the config object by constructing a control object and 

268 using a C++ ``validate()`` implementation. 

269 """ 

270 super(cls, self).validate() 

271 r = self.makeControl() 

272 r.validate() 

273 

274 def setDefaults(self): 

275 """Initialize the config object, using the Control objects default ctor 

276 to provide defaults. 

277 """ 

278 super(cls, self).setDefaults() 

279 try: 

280 r = self.Control() 

281 # Indicate in the history that these values came from C++, even 

282 # if we can't say which line 

283 self.readControl( 

284 r, 

285 __at=[StackFrame(ctrl.__name__ + " C++", 0, "setDefaults", "")], 

286 __label="defaults", 

287 __reset=True, 

288 ) 

289 except Exception: 

290 pass # if we can't instantiate the Control, don't set defaults 

291 

292 ctrl.ConfigClass = cls 

293 cls.Control = ctrl 

294 cls.makeControl = makeControl 

295 cls.readControl = readControl 

296 cls.setDefaults = setDefaults 

297 if hasattr(ctrl, "validate"): 297 ↛ 298line 297 didn't jump to line 298, because the condition on line 297 was never true

298 cls.validate = validate 

299 for k, field in fields.items(): 

300 if not hasattr(cls, k): 300 ↛ 299line 300 didn't jump to line 299, because the condition on line 300 was never false

301 setattr(cls, k, field) 

302 return cls 

303 

304 

305def wrap(ctrl): 

306 """Decorator that adds fields from a C++ control class to a 

307 `lsst.pex.config.Config` class. 

308 

309 Parameters 

310 ---------- 

311 ctrl : object 

312 The C++ control class. 

313 

314 Notes 

315 ----- 

316 See `makeConfigClass` for more information. This `wrap` decorator is 

317 equivalent to calling `makeConfigClass` with the decorated class as the 

318 ``cls`` argument. 

319 

320 Examples 

321 -------- 

322 Use `wrap` like this:: 

323 

324 @wrap(MyControlClass) 

325 class MyConfigClass(Config): 

326 pass 

327 

328 See also 

329 -------- 

330 makeConfigClass 

331 """ 

332 

333 def decorate(cls): 

334 return makeConfigClass(ctrl, cls=cls) 

335 

336 return decorate