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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

# 

# LSST Data Management System 

# Copyright 2008, 2009, 2010 LSST Corporation. 

# 

# This product includes software developed by the 

# LSST Project (http://www.lsst.org/). 

# 

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

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

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

# (at your option) any later version. 

# 

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

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

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

# GNU General Public License for more details. 

# 

# You should have received a copy of the LSST License Statement and 

# the GNU General Public License along with this program. If not, 

# see <http://www.lsstcorp.org/LegalNotices/>. 

# 

__all__ = ("wrap", "makeConfigClass") 

 

import inspect 

import re 

import importlib 

 

from .config import Config, Field 

from .listField import ListField, List 

from .configField import ConfigField 

from .callStack import getCallerFrame, getCallStack 

 

# Mapping from C++ types to Python type: assumes we can round-trip between these using 

# the usual pybind11 converters, but doesn't require they be binary equivalent under-the-hood 

# or anything. 

_dtypeMap = { 

"bool": bool, 

"int": int, 

"double": float, 

"float": float, 

"std::int64_t": int, 

"std::string": str 

} 

 

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

 

 

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

"""A function that creates a Python config class that matches a C++ control object class. 

 

@param ctrl C++ control class to wrap. 

@param name Name of the new config class; defaults to the __name__ of the control 

class with 'Control' replaced with 'Config'. 

@param base Base class for the config class. 

@param doc Docstring for the config class. 

@param module Either a module object, a string specifying the name of the module, or an 

integer specifying how far back in the stack to look for the module to use: 

0 is the immediate caller of pex.config.wrap. This will be used to 

set __module__ for the new config class, and the class will also be added 

to the module. Ignored if None or if cls is not None, but note that the default 

is to use the callers' module. 

@param cls An existing config class to use instead of creating a new one; name, base 

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

 

See the 'wrap' decorator as a way to use makeConfigClass that may be more convenient. 

 

To use makeConfigClass, in C++, write a control object, using the LSST_CONTROL_FIELD macro in 

lsst/pex/config.h (note that it must have sensible default constructor): 

 

@code 

// myHeader.h 

 

struct InnerControl { 

LSST_CONTROL_FIELD(wim, std::string, "documentation for field 'wim'"); 

}; 

 

struct FooControl { 

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

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

LSST_NESTED_CONTROL_FIELD(zot, myWrappedLib, InnerControl, "documentation for field 'zot'"); 

 

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

}; 

@endcode 

 

 

You can use LSST_NESTED_CONTROL_FIELD to nest control objects. Now, wrap those control objects as 

you would any other C++ class, but make sure you include lsst/pex/config.h before including the header 

file where the control object class is defined: 

 

Now, in Python, do this: 

 

@code 

import myWrappedLib 

import lsst.pex.config 

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

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

@endcode 

 

This will add fully-fledged "bar", "baz", and "zot" fields to FooConfig, set 

FooConfig.Control = FooControl, and inject makeControl and readControl 

methods to create a FooControl and set the FooConfig from the FooControl, 

respectively. In addition, if FooControl has a validate() member function, 

a custom validate() method will be added to FooConfig that uses it. And, 

of course, all of the above will be done for InnerControl/InnerConfig too. 

 

Any field that would be injected that would clash with an existing attribute of the 

class will be silently ignored; this allows the user to customize fields and 

inherit them from wrapped control classes. However, these names will still be 

processed when converting between config and control classes, so they should generally 

be present as base class fields or other instance attributes or descriptors. 

 

While LSST_CONTROL_FIELD will work for any C++ type, automatic Config generation 

only supports bool, int, std::int64_t, double, and std::string fields, along 

with std::list and std::vectors of those types. 

""" 

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

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

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

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

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

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

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

# Not only does setting __module__ make Python pretty-printers more useful, 

# it's also necessary if we want to pickle Config objects. 

126 ↛ 130line 126 didn't jump to line 130, because the condition on line 126 was never false if isinstance(module, int): 

frame = getCallerFrame(module) 

moduleObj = inspect.getmodule(frame) 

moduleName = moduleObj.__name__ 

elif isinstance(module, str): 

moduleName = module 

moduleObj = __import__(moduleName) 

else: 

moduleObj = module 

moduleName = moduleObj.__name__ 

cls.__module__ = moduleName 

setattr(moduleObj, name, cls) 

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

doc = ctrl.__doc__ 

fields = {} 

# loop over all class attributes, looking for the special static methods that indicate a field 

# defined by one of the macros in pex/config.h. 

for attr in dir(ctrl): 

if attr.startswith("_type_"): 

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

getDoc = "_doc_" + k 

getModule = "_module_" + k 

getType = attr 

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

doc = getattr(ctrl, getDoc)() 

ctype = getattr(ctrl, getType)() 

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

nestedModuleName = getattr(ctrl, getModule)() 

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

nestedModuleObj = moduleObj 

else: 

nestedModuleObj = importlib.import_module(nestedModuleName) 

try: 

dtype = getattr(nestedModuleObj, ctype).ConfigClass 

except AttributeError: 

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

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

else: 

try: 

dtype = _dtypeMap[ctype] 

FieldCls = Field 

except KeyError: 

dtype = None 

m = _containerRegex.match(ctype) 

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

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

FieldCls = ListField 

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

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

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

 

# Define a number of methods to put in the new Config class. Note that these are "closures"; 

# they have access to local variables defined in the makeConfigClass function (like the fields dict). 

def makeControl(self): 

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

 

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

Control object's default constructor. 

""" 

r = self.Control() 

for k, f in fields.items(): 

value = getattr(self, k) 

if isinstance(f, ConfigField): 

value = value.makeControl() 

if value is not None: 

if isinstance(value, List): 

setattr(r, k, value._list) 

else: 

setattr(r, k, value) 

return r 

 

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

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

 

The __at, __label, and __reset arguments are for internal use only; they are used to 

remove internal calls from the history. 

""" 

if __at is None: 

__at = getCallStack() 

values = {} 

for k, f in fields.items(): 

if isinstance(f, ConfigField): 

getattr(self, k).readControl(getattr(control, k), 

__at=__at, __label=__label, __reset=__reset) 

else: 

values[k] = getattr(control, k) 

if __reset: 

self._history = {} 

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

 

def validate(self): 

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

a C++ validate() implementation.""" 

super(cls, self).validate() 

r = self.makeControl() 

r.validate() 

 

def setDefaults(self): 

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

to provide defaults.""" 

super(cls, self).setDefaults() 

try: 

r = self.Control() 

# Indicate in the history that these values came from C++, even if we can't say which line 

self.readControl(r, __at=[(ctrl.__name__ + " C++", 0, "setDefaults", "")], __label="defaults", 

__reset=True) 

except Exception: 

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

 

ctrl.ConfigClass = cls 

cls.Control = ctrl 

cls.makeControl = makeControl 

cls.readControl = readControl 

cls.setDefaults = setDefaults 

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

cls.validate = validate 

for k, field in fields.items(): 

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

setattr(cls, k, field) 

return cls 

 

 

def wrap(ctrl): 

"""A decorator that adds fields from a C++ control class to a Python config class. 

 

Used like this: 

 

@wrap(MyControlClass) 

class MyConfigClass(Config): 

pass 

 

See makeConfigClass for more information; this is equivalent to calling makeConfigClass 

with the decorated class as the 'cls' argument. 

""" 

def decorate(cls): 

return makeConfigClass(ctrl, cls=cls) 

return decorate