LSSTApplications  18.1.0
LSSTDataManagementBasePackage
wrap.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 __all__ = ("wrap", "makeConfigClass")
23 
24 import inspect
25 import re
26 import importlib
27 
28 from .config import Config, Field
29 from .listField import ListField, List
30 from .configField import ConfigField
31 from .callStack import getCallerFrame, getCallStack
32 
33 _dtypeMap = {
34  "bool": bool,
35  "int": int,
36  "double": float,
37  "float": float,
38  "std::int64_t": int,
39  "std::string": str
40 }
41 """Mapping from C++ types to Python type (`dict`)
42 
43 Tassumes we can round-trip between these using the usual pybind11 converters,
44 but doesn't require they be binary equivalent under-the-hood or anything.
45 """
46 
47 _containerRegex = re.compile(r"(std::)?(vector|list)<\s*(?P<type>[a-z0-9_:]+)\s*>")
48 
49 
50 def makeConfigClass(ctrl, name=None, base=Config, doc=None, module=None, cls=None):
51  """Create a `~lsst.pex.config.Config` class that matches a C++ control
52  object class.
53 
54  See the `wrap` decorator as a convenient interface to ``makeConfigClass``.
55 
56  Parameters
57  ----------
58  ctrl : class
59  C++ control class to wrap.
60  name : `str`, optional
61  Name of the new config class; defaults to the ``__name__`` of the
62  control class with ``'Control'`` replaced with ``'Config'``.
63  base : `lsst.pex.config.Config`-type, optional
64  Base class for the config class.
65  doc : `str`, optional
66  Docstring for the config class.
67  module : object, `str`, `int`, or `None` optional
68  Either a module object, a string specifying the name of the module, or
69  an integer specifying how far back in the stack to look for the module
70  to use: 0 is the immediate caller of `~lsst.pex.config.wrap`. This will
71  be used to set ``__module__`` for the new config class, and the class
72  will also be added to the module. Ignored if `None` or if ``cls`` is
73  not `None`. Defaults to None in which case module is looked up from the
74  module of ctrl.
75  cls : class
76  An existing config class to use instead of creating a new one; name,
77  base doc, and module will be ignored if this is not `None`.
78 
79  Notes
80  -----
81  To use ``makeConfigClass``, write a control object in C++ using the
82  ``LSST_CONTROL_FIELD`` macro in ``lsst/pex/config.h`` (note that it must
83  have sensible default constructor):
84 
85  .. code-block:: cpp
86 
87  // myHeader.h
88 
89  struct InnerControl {
90  LSST_CONTROL_FIELD(wim, std::string, "documentation for field 'wim'");
91  };
92 
93  struct FooControl {
94  LSST_CONTROL_FIELD(bar, int, "documentation for field 'bar'");
95  LSST_CONTROL_FIELD(baz, double, "documentation for field 'baz'");
96  LSST_NESTED_CONTROL_FIELD(zot, myWrappedLib, InnerControl, "documentation for field 'zot'");
97 
98  FooControl() : bar(0), baz(0.0) {}
99  };
100 
101  You can use ``LSST_NESTED_CONTROL_FIELD`` to nest control objects. Wrap
102  those control objects as you would any other C++ class, but make sure you
103  include ``lsst/pex/config.h`` before including the header file where
104  the control object class is defined.
105 
106  Next, in Python:
107 
108  .. code-block:: py
109 
110  import lsst.pex.config
111  import myWrappedLib
112 
113  InnerConfig = lsst.pex.config.makeConfigClass(myWrappedLib.InnerControl)
114  FooConfig = lsst.pex.config.makeConfigClass(myWrappedLib.FooControl)
115 
116  This does the following things:
117 
118  - Adds ``bar``, ``baz``, and ``zot`` fields to ``FooConfig``.
119  - Set ``FooConfig.Control`` to ``FooControl``.
120  - Adds ``makeControl`` and ``readControl`` methods to create a
121  ``FooControl`` and set the ``FooConfig`` from the ``FooControl``,
122  respectively.
123  - If ``FooControl`` has a ``validate()`` member function,
124  a custom ``validate()`` method will be added to ``FooConfig`` that uses
125  it.
126 
127  All of the above are done for ``InnerConfig`` as well.
128 
129  Any field that would be injected that would clash with an existing
130  attribute of the class is be silently ignored. This allows you to
131  customize fields and inherit them from wrapped control classes. However,
132  these names are still be processed when converting between config and
133  control classes, so they should generally be present as base class fields
134  or other instance attributes or descriptors.
135 
136  While ``LSST_CONTROL_FIELD`` will work for any C++ type, automatic
137  `~lsst.pex.config.Config` generation only supports ``bool``, ``int``,
138  ``std::int64_t``, ``double``, and ``std::string`` fields, along with
139  ``std::list`` and ``std::vectors`` of those types.
140 
141  See also
142  --------
143  wrap
144  """
145  if name is None:
146  if "Control" not in ctrl.__name__:
147  raise ValueError("Cannot guess appropriate Config class name for %s." % ctrl)
148  name = ctrl.__name__.replace("Control", "Config")
149  if cls is None:
150  cls = type(name, (base,), {"__doc__": doc})
151  if module is not None:
152  # Not only does setting __module__ make Python pretty-printers more useful,
153  # it's also necessary if we want to pickle Config objects.
154  if isinstance(module, int):
155  frame = getCallerFrame(module)
156  moduleObj = inspect.getmodule(frame)
157  moduleName = moduleObj.__name__
158  elif isinstance(module, str):
159  moduleName = module
160  moduleObj = __import__(moduleName)
161  else:
162  moduleObj = module
163  moduleName = moduleObj.__name__
164  cls.__module__ = moduleName
165  setattr(moduleObj, name, cls)
166  else:
167  cls.__module__ = ctrl.__module__
168  moduleName = ctrl.__module__
169  else:
170  moduleName = cls.__module__
171  if doc is None:
172  doc = ctrl.__doc__
173  fields = {}
174  # loop over all class attributes, looking for the special static methods that indicate a field
175  # defined by one of the macros in pex/config.h.
176  for attr in dir(ctrl):
177  if attr.startswith("_type_"):
178  k = attr[len("_type_"):]
179  getDoc = "_doc_" + k
180  getModule = "_module_" + k
181  getType = attr
182  if hasattr(ctrl, k) and hasattr(ctrl, getDoc):
183  doc = getattr(ctrl, getDoc)()
184  ctype = getattr(ctrl, getType)()
185  if hasattr(ctrl, getModule): # if this is present, it's a nested control object
186  nestedModuleName = getattr(ctrl, getModule)()
187  if nestedModuleName == moduleName:
188  nestedModuleObj = moduleObj
189  else:
190  nestedModuleObj = importlib.import_module(nestedModuleName)
191  try:
192  dtype = getattr(nestedModuleObj, ctype).ConfigClass
193  except AttributeError:
194  raise AttributeError("'%s.%s.ConfigClass' does not exist" % (moduleName, ctype))
195  fields[k] = ConfigField(doc=doc, dtype=dtype)
196  else:
197  try:
198  dtype = _dtypeMap[ctype]
199  FieldCls = Field
200  except KeyError:
201  dtype = None
202  m = _containerRegex.match(ctype)
203  if m:
204  dtype = _dtypeMap.get(m.group("type"), None)
205  FieldCls = ListField
206  if dtype is None:
207  raise TypeError("Could not parse field type '%s'." % ctype)
208  fields[k] = FieldCls(doc=doc, dtype=dtype, optional=True)
209 
210  # Define a number of methods to put in the new Config class. Note that these are "closures";
211  # they have access to local variables defined in the makeConfigClass function (like the fields dict).
212  def makeControl(self):
213  """Construct a C++ Control object from this Config object.
214 
215  Fields set to `None` will be ignored, and left at the values defined
216  by the Control object's default constructor.
217  """
218  r = self.Control()
219  for k, f in fields.items():
220  value = getattr(self, k)
221  if isinstance(f, ConfigField):
222  value = value.makeControl()
223  if value is not None:
224  if isinstance(value, List):
225  setattr(r, k, value._list)
226  else:
227  setattr(r, k, value)
228  return r
229 
230  def readControl(self, control, __at=None, __label="readControl", __reset=False):
231  """Read values from a C++ Control object and assign them to self's
232  fields.
233 
234  Parameters
235  ----------
236  control
237  C++ Control object.
238 
239  Notes
240  -----
241  The ``__at``, ``__label``, and ``__reset`` arguments are for internal
242  use only; they are used to remove internal calls from the history.
243  """
244  if __at is None:
245  __at = getCallStack()
246  values = {}
247  for k, f in fields.items():
248  if isinstance(f, ConfigField):
249  getattr(self, k).readControl(getattr(control, k),
250  __at=__at, __label=__label, __reset=__reset)
251  else:
252  values[k] = getattr(control, k)
253  if __reset:
254  self._history = {}
255  self.update(__at=__at, __label=__label, **values)
256 
257  def validate(self):
258  """Validate the config object by constructing a control object and
259  using a C++ ``validate()`` implementation.
260  """
261  super(cls, self).validate()
262  r = self.makeControl()
263  r.validate()
264 
265  def setDefaults(self):
266  """Initialize the config object, using the Control objects default ctor
267  to provide defaults.
268  """
269  super(cls, self).setDefaults()
270  try:
271  r = self.Control()
272  # Indicate in the history that these values came from C++, even if we can't say which line
273  self.readControl(r, __at=[(ctrl.__name__ + " C++", 0, "setDefaults", "")], __label="defaults",
274  __reset=True)
275  except Exception:
276  pass # if we can't instantiate the Control, don't set defaults
277 
278  ctrl.ConfigClass = cls
279  cls.Control = ctrl
280  cls.makeControl = makeControl
281  cls.readControl = readControl
282  cls.setDefaults = setDefaults
283  if hasattr(ctrl, "validate"):
284  cls.validate = validate
285  for k, field in fields.items():
286  if not hasattr(cls, k):
287  setattr(cls, k, field)
288  return cls
289 
290 
291 def wrap(ctrl):
292  """Decorator that adds fields from a C++ control class to a
293  `lsst.pex.config.Config` class.
294 
295  Parameters
296  ----------
297  ctrl : object
298  The C++ control class.
299 
300  Notes
301  -----
302  See `makeConfigClass` for more information. This `wrap` decorator is
303  equivalent to calling `makeConfigClass` with the decorated class as the
304  ``cls`` argument.
305 
306  Examples
307  --------
308  Use `wrap` like this::
309 
310  @wrap(MyControlClass)
311  class MyConfigClass(Config):
312  pass
313 
314  See also
315  --------
316  makeConfigClass
317  """
318  def decorate(cls):
319  return makeConfigClass(ctrl, cls=cls)
320  return decorate
def wrap(ctrl)
Definition: wrap.py:291
def getCallStack(skip=0)
Definition: callStack.py:169
def makeConfigClass(ctrl, name=None, base=Config, doc=None, module=None, cls=None)
Definition: wrap.py:50
table::Key< int > type
Definition: Detector.cc:167
def getCallerFrame(relative=0)
Definition: callStack.py:29