Loading [MathJax]/extensions/tex2jax.js
LSST Applications g01e2988da0+8c3210761e,g0fba68d861+c1d6bbbe03,g1fd858c14a+7a7b9dd5ed,g2c84ff76c0+5cb23283cf,g30358e5240+f0e04ebe90,g35bb328faa+fcb1d3bbc8,g436fd98eb5+bdc6fcdd04,g4af146b050+742274f7cd,g4d2262a081+ea0311752b,g4e0f332c67+cb09b8a5b6,g53246c7159+fcb1d3bbc8,g5a012ec0e7+477f9c599b,g60b5630c4e+bdc6fcdd04,g67b6fd64d1+2218407a0c,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g7b71ed6315+fcb1d3bbc8,g87b7deb4dc+777438113c,g8852436030+ebf28f0d95,g89139ef638+2218407a0c,g9125e01d80+fcb1d3bbc8,g989de1cb63+2218407a0c,g9f33ca652e+42fb53f4c8,g9f7030ddb1+11b9b6f027,ga2b97cdc51+bdc6fcdd04,gab72ac2889+bdc6fcdd04,gabe3b4be73+1e0a283bba,gabf8522325+3210f02652,gb1101e3267+9c79701da9,gb58c049af0+f03b321e39,gb89ab40317+2218407a0c,gcf25f946ba+ebf28f0d95,gd6cbbdb0b4+e8f9c9c900,gd9a9a58781+fcb1d3bbc8,gde0f65d7ad+1f9613449c,ge278dab8ac+3ef3db156b,ge410e46f29+2218407a0c,gf67bdafdda+2218407a0c,v29.0.0.rc3
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
wrap.py
Go to the documentation of this file.
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 : 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(
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:
156 if "Control" not in ctrl.__name__:
157 raise ValueError(f"Cannot guess appropriate Config class name for {ctrl}.")
158 name = ctrl.__name__.replace("Control", "Config")
159 if cls is None:
160 cls = type(name, (base,), {"__doc__": doc})
161 if module is not None:
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):
166 frame = getCallerFrame(module)
167 moduleObj = inspect.getmodule(frame)
168 moduleName = moduleObj.__name__
169 elif isinstance(module, str):
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:
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):
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:
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:
215 dtype = _dtypeMap.get(m.group("type"), None)
216 FieldCls = ListField
217 if dtype is None:
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"):
307 cls.validate = validate
308 for k, field in fields.items():
309 if not hasattr(cls, k):
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
makeConfigClass(ctrl, name=None, base=Config, doc=None, module=None, cls=None)
Definition wrap.py:56