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

115 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-26 08:53 +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__ = ("makeConfigClass", "wrap") 

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 

49It assumes 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 : `type` 

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 : `type` [`lsst.pex.config.Config`] 

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( 

122 myWrappedLib.InnerControl 

123 ) 

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

125 

126 This does the following things: 

127 

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

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

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

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

132 respectively. 

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

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

135 it. 

136 

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

138 

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

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

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

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

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

144 or other instance attributes or descriptors. 

145 

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

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

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

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

150 

151 See Also 

152 -------- 

153 wrap : Add fields from C++ object. 

154 """ 

155 if name is None: 155 ↛ 159line 155 didn't jump to line 159 because the condition on line 155 was always true

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

157 raise ValueError(f"Cannot guess appropriate Config class name for {ctrl}.") 

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

159 if cls is None: 159 ↛ 181line 159 didn't jump to line 181 because the condition on line 159 was always true

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

161 if module is not None: 161 ↛ 178line 161 didn't jump to line 178 because the condition on line 161 was always true

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

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

164 # objects. 

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

166 frame = getCallerFrame(module) 

167 moduleObj = inspect.getmodule(frame) 

168 moduleName = moduleObj.__name__ 

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

170 moduleName = module 

171 moduleObj = __import__(moduleName) 

172 else: 

173 moduleObj = module 

174 moduleName = moduleObj.__name__ 

175 cls.__module__ = moduleName 

176 setattr(moduleObj, name, cls) 

177 else: 

178 cls.__module__ = ctrl.__module__ 

179 moduleName = ctrl.__module__ 

180 else: 

181 moduleName = cls.__module__ 

182 if doc is None: 182 ↛ 184line 182 didn't jump to line 184 because the condition on line 182 was always true

183 doc = ctrl.__doc__ 

184 fields = {} 

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

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

187 for attr in dir(ctrl): 

188 if attr.startswith("_type_"): 

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

190 getDoc = "_doc_" + k 

191 getModule = "_module_" + k 

192 getType = attr 

193 if hasattr(ctrl, k) and hasattr(ctrl, getDoc): 193 ↛ 187line 193 didn't jump to line 187 because the condition on line 193 was always true

194 doc = getattr(ctrl, getDoc)() 

195 ctype = getattr(ctrl, getType)() 

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

197 nestedModuleName = getattr(ctrl, getModule)() 

198 if nestedModuleName == moduleName: 198 ↛ 201line 198 didn't jump to line 201 because the condition on line 198 was always true

199 nestedModuleObj = moduleObj 

200 else: 

201 nestedModuleObj = importlib.import_module(nestedModuleName) 

202 try: 

203 dtype = getattr(nestedModuleObj, ctype).ConfigClass 

204 except AttributeError as e: 

205 raise AttributeError(f"'{moduleName}.{ctype}.ConfigClass' does not exist") from e 

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

207 else: 

208 try: 

209 dtype = _dtypeMap[ctype] 

210 FieldCls = Field 

211 except KeyError: 

212 dtype = None 

213 m = _containerRegex.match(ctype) 

214 if m: 214 ↛ 217line 214 didn't jump to line 217 because the condition on line 214 was always true

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

216 FieldCls = ListField 

217 if dtype is None: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 raise TypeError(f"Could not parse field type '{ctype}'.") 

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

220 

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

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

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

224 def makeControl(self): 

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

226 

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

228 by the Control object's default constructor. 

229 """ 

230 r = self.Control() 

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

232 value = getattr(self, k) 

233 if isinstance(f, ConfigField): 

234 value = value.makeControl() 

235 if value is not None: 

236 if isinstance(value, List): 

237 setattr(r, k, value._list) 

238 else: 

239 setattr(r, k, value) 

240 return r 

241 

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

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

244 fields. 

245 

246 Parameters 

247 ---------- 

248 control : `type` 

249 C++ Control object. 

250 __at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\ 

251 optional 

252 Internal use only. 

253 __label : `str`, optional 

254 Internal use only. 

255 __reset : `bool`, optional 

256 Internal use only. 

257 

258 Notes 

259 ----- 

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

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

262 """ 

263 if __at is None: 

264 __at = getCallStack() 

265 values = {} 

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

267 if isinstance(f, ConfigField): 

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

269 else: 

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

271 if __reset: 

272 self._history = {} 

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

274 

275 def validate(self): 

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

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

278 """ 

279 super(cls, self).validate() 

280 r = self.makeControl() 

281 r.validate() 

282 

283 def setDefaults(self): 

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

285 to provide defaults. 

286 """ 

287 super(cls, self).setDefaults() 

288 try: 

289 r = self.Control() 

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

291 # if we can't say which line 

292 self.readControl( 

293 r, 

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

295 __label="defaults", 

296 __reset=True, 

297 ) 

298 except Exception: 

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

300 

301 ctrl.ConfigClass = cls 

302 cls.Control = ctrl 

303 cls.makeControl = makeControl 

304 cls.readControl = readControl 

305 cls.setDefaults = setDefaults 

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

307 cls.validate = validate 

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

309 if not hasattr(cls, k): 309 ↛ 308line 309 didn't jump to line 308 because the condition on line 309 was always true

310 setattr(cls, k, field) 

311 return cls 

312 

313 

314def wrap(ctrl): 

315 """Add fields from a C++ control class to a `lsst.pex.config.Config` class. 

316 

317 Parameters 

318 ---------- 

319 ctrl : `object` 

320 The C++ control class. 

321 

322 Notes 

323 ----- 

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

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

326 ``cls`` argument. 

327 

328 Examples 

329 -------- 

330 Use `wrap` like this:: 

331 

332 @wrap(MyControlClass) 

333 class MyConfigClass(Config): 

334 pass 

335 

336 See Also 

337 -------- 

338 makeConfigClass : Make a config class. 

339 """ 

340 

341 def decorate(cls): 

342 return makeConfigClass(ctrl, cls=cls) 

343 

344 return decorate