LSSTApplications  20.0.0
LSSTDataManagementBasePackage
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__ = ("wrap", "makeConfigClass")
29 
30 import inspect
31 import re
32 import importlib
33 
34 from .config import Config, Field
35 from .listField import ListField, List
36 from .configField import ConfigField
37 from .callStack import getCallerFrame, getCallStack
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 
49 Tassumes we can round-trip between these using the usual pybind11 converters,
50 but 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 
56 def 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:
154  if "Control" not in ctrl.__name__:
155  raise ValueError("Cannot guess appropriate Config class name for %s." % ctrl)
156  name = ctrl.__name__.replace("Control", "Config")
157  if cls is None:
158  cls = type(name, (base,), {"__doc__": doc})
159  if module is not None:
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):
164  frame = getCallerFrame(module)
165  moduleObj = inspect.getmodule(frame)
166  moduleName = moduleObj.__name__
167  elif isinstance(module, str):
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:
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):
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:
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:
213  dtype = _dtypeMap.get(m.group("type"), None)
214  FieldCls = ListField
215  if dtype is None:
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),
260  __at=__at, __label=__label, __reset=__reset)
261  else:
262  values[k] = getattr(control, k)
263  if __reset:
264  self._history = {}
265  self.update(__at=__at, __label=__label, **values)
266 
267  def validate(self):
268  """Validate the config object by constructing a control object and
269  using a C++ ``validate()`` implementation.
270  """
271  super(cls, self).validate()
272  r = self.makeControl()
273  r.validate()
274 
275  def setDefaults(self):
276  """Initialize the config object, using the Control objects default ctor
277  to provide defaults.
278  """
279  super(cls, self).setDefaults()
280  try:
281  r = self.Control()
282  # Indicate in the history that these values came from C++, even
283  # if we can't say which line
284  self.readControl(r, __at=[(ctrl.__name__ + " C++", 0, "setDefaults", "")], __label="defaults",
285  __reset=True)
286  except Exception:
287  pass # if we can't instantiate the Control, don't set defaults
288 
289  ctrl.ConfigClass = cls
290  cls.Control = ctrl
291  cls.makeControl = makeControl
292  cls.readControl = readControl
293  cls.setDefaults = setDefaults
294  if hasattr(ctrl, "validate"):
295  cls.validate = validate
296  for k, field in fields.items():
297  if not hasattr(cls, k):
298  setattr(cls, k, field)
299  return cls
300 
301 
302 def wrap(ctrl):
303  """Decorator that adds fields from a C++ control class to a
304  `lsst.pex.config.Config` class.
305 
306  Parameters
307  ----------
308  ctrl : object
309  The C++ control class.
310 
311  Notes
312  -----
313  See `makeConfigClass` for more information. This `wrap` decorator is
314  equivalent to calling `makeConfigClass` with the decorated class as the
315  ``cls`` argument.
316 
317  Examples
318  --------
319  Use `wrap` like this::
320 
321  @wrap(MyControlClass)
322  class MyConfigClass(Config):
323  pass
324 
325  See also
326  --------
327  makeConfigClass
328  """
329  def decorate(cls):
330  return makeConfigClass(ctrl, cls=cls)
331  return decorate
pex.config.wrap.makeControl
makeControl
Definition: wrap.py:291
pex.config.wrap.makeConfigClass
def makeConfigClass(ctrl, name=None, base=Config, doc=None, module=None, cls=None)
Definition: wrap.py:56
pex.config.wrap.readControl
readControl
Definition: wrap.py:292
pex.config.callStack.getCallStack
def getCallStack(skip=0)
Definition: callStack.py:175
pex.config.configField.ConfigField
Definition: configField.py:35
pex.config.callStack.getCallerFrame
def getCallerFrame(relative=0)
Definition: callStack.py:34
pex.config.wrap.setDefaults
setDefaults
Definition: wrap.py:293
type
table::Key< int > type
Definition: Detector.cc:163
pex.config.wrap.wrap
def wrap(ctrl)
Definition: wrap.py:302
pex.config.wrap.validate
validate
Definition: wrap.py:295