Coverage for python/lsst/pex/config/wrap.py: 55%
115 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-20 11:16 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-20 11:16 +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/>.
28__all__ = ("wrap", "makeConfigClass")
30import importlib
31import inspect
32import re
34from .callStack import StackFrame, getCallerFrame, getCallStack
35from .config import Config, Field
36from .configField import ConfigField
37from .listField import List, ListField
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`)
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"""
53_containerRegex = re.compile(r"(std::)?(vector|list)<\s*(?P<type>[a-z0-9_:]+)\s*>")
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.
60 See the `wrap` decorator as a convenient interface to ``makeConfigClass``.
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`.
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):
91 .. code-block:: cpp
93 // myHeader.h
95 struct InnerControl {
96 LSST_CONTROL_FIELD(wim, std::string,
97 "documentation for field 'wim'");
98 };
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'");
106 FooControl() : bar(0), baz(0.0) {}
107 };
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.
114 Next, in Python:
116 .. code-block:: py
118 import lsst.pex.config
119 import myWrappedLib
121 InnerConfig = lsst.pex.config.makeConfigClass(myWrappedLib.InnerControl)
122 FooConfig = lsst.pex.config.makeConfigClass(myWrappedLib.FooControl)
124 This does the following things:
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.
135 All of the above are done for ``InnerConfig`` as well.
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.
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.
149 See Also
150 --------
151 wrap : Add fields from C++ object.
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(f"'{moduleName}.{ctype}.ConfigClass' does not exist")
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)
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.
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
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.
244 Parameters
245 ----------
246 control : `type`
247 C++ Control object.
248 __at : `list` of `~lsst.pex.config.callStack.StackFrame` or `None`,\
249 optional
250 Internal use only.
251 __label : `str`, optional
252 Internal use only.
253 __reset : `bool`, optional
254 Internal use only.
256 Notes
257 -----
258 The ``__at``, ``__label``, and ``__reset`` arguments are for internal
259 use only; they are used to remove internal calls from the history.
260 """
261 if __at is None:
262 __at = getCallStack()
263 values = {}
264 for k, f in fields.items():
265 if isinstance(f, ConfigField):
266 getattr(self, k).readControl(getattr(control, k), __at=__at, __label=__label, __reset=__reset)
267 else:
268 values[k] = getattr(control, k)
269 if __reset:
270 self._history = {}
271 self.update(__at=__at, __label=__label, **values)
273 def validate(self):
274 """Validate the config object by constructing a control object and
275 using a C++ ``validate()`` implementation.
276 """
277 super(cls, self).validate()
278 r = self.makeControl()
279 r.validate()
281 def setDefaults(self):
282 """Initialize the config object, using the Control objects default ctor
283 to provide defaults.
284 """
285 super(cls, self).setDefaults()
286 try:
287 r = self.Control()
288 # Indicate in the history that these values came from C++, even
289 # if we can't say which line
290 self.readControl(
291 r,
292 __at=[StackFrame(ctrl.__name__ + " C++", 0, "setDefaults", "")],
293 __label="defaults",
294 __reset=True,
295 )
296 except Exception:
297 pass # if we can't instantiate the Control, don't set defaults
299 ctrl.ConfigClass = cls
300 cls.Control = ctrl
301 cls.makeControl = makeControl
302 cls.readControl = readControl
303 cls.setDefaults = setDefaults
304 if hasattr(ctrl, "validate"): 304 ↛ 305line 304 didn't jump to line 305, because the condition on line 304 was never true
305 cls.validate = validate
306 for k, field in fields.items():
307 if not hasattr(cls, k): 307 ↛ 306line 307 didn't jump to line 306, because the condition on line 307 was never false
308 setattr(cls, k, field)
309 return cls
312def wrap(ctrl):
313 """Add fields from a C++ control class to a `lsst.pex.config.Config` class.
315 Parameters
316 ----------
317 ctrl : object
318 The C++ control class.
320 Notes
321 -----
322 See `makeConfigClass` for more information. This `wrap` decorator is
323 equivalent to calling `makeConfigClass` with the decorated class as the
324 ``cls`` argument.
326 Examples
327 --------
328 Use `wrap` like this::
330 @wrap(MyControlClass)
331 class MyConfigClass(Config):
332 pass
334 See Also
335 --------
336 makeConfigClass : Make a config class.
337 """
339 def decorate(cls):
340 return makeConfigClass(ctrl, cls=cls)
342 return decorate