lsst.pex.config  16.0-5-gd0f1235+8
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 # Mapping from C++ types to Python type: assumes we can round-trip between these using
34 # the usual pybind11 converters, but doesn't require they be binary equivalent under-the-hood
35 # or anything.
36 _dtypeMap = {
37  "bool": bool,
38  "int": int,
39  "double": float,
40  "float": float,
41  "std::int64_t": int,
42  "std::string": str
43 }
44 
45 _containerRegex = re.compile(r"(std::)?(vector|list)<\s*(?P<type>[a-z0-9_:]+)\s*>")
46 
47 
48 def makeConfigClass(ctrl, name=None, base=Config, doc=None, module=0, cls=None):
49  """A function that creates a Python config class that matches a C++ control object class.
50 
51  @param ctrl C++ control class to wrap.
52  @param name Name of the new config class; defaults to the __name__ of the control
53  class with 'Control' replaced with 'Config'.
54  @param base Base class for the config class.
55  @param doc Docstring for the config class.
56  @param module Either a module object, a string specifying the name of the module, or an
57  integer specifying how far back in the stack to look for the module to use:
58  0 is the immediate caller of pex.config.wrap. This will be used to
59  set __module__ for the new config class, and the class will also be added
60  to the module. Ignored if None or if cls is not None, but note that the default
61  is to use the callers' module.
62  @param cls An existing config class to use instead of creating a new one; name, base
63  doc, and module will be ignored if this is not None.
64 
65  See the 'wrap' decorator as a way to use makeConfigClass that may be more convenient.
66 
67  To use makeConfigClass, in C++, write a control object, using the LSST_CONTROL_FIELD macro in
68  lsst/pex/config.h (note that it must have sensible default constructor):
69 
70  @code
71  // myHeader.h
72 
73  struct InnerControl {
74  LSST_CONTROL_FIELD(wim, std::string, "documentation for field 'wim'");
75  };
76 
77  struct FooControl {
78  LSST_CONTROL_FIELD(bar, int, "documentation for field 'bar'");
79  LSST_CONTROL_FIELD(baz, double, "documentation for field 'baz'");
80  LSST_NESTED_CONTROL_FIELD(zot, myWrappedLib, InnerControl, "documentation for field 'zot'");
81 
82  FooControl() : bar(0), baz(0.0) {}
83  };
84  @endcode
85 
86 
87  You can use LSST_NESTED_CONTROL_FIELD to nest control objects. Now, wrap those control objects as
88  you would any other C++ class, but make sure you include lsst/pex/config.h before including the header
89  file where the control object class is defined:
90 
91  Now, in Python, do this:
92 
93  @code
94  import myWrappedLib
95  import lsst.pex.config
96  InnerConfig = lsst.pex.config.makeConfigClass(myWrappedLib.InnerControl)
97  FooConfig = lsst.pex.config.makeConfigClass(myWrappedLib.FooControl)
98  @endcode
99 
100  This will add fully-fledged "bar", "baz", and "zot" fields to FooConfig, set
101  FooConfig.Control = FooControl, and inject makeControl and readControl
102  methods to create a FooControl and set the FooConfig from the FooControl,
103  respectively. In addition, if FooControl has a validate() member function,
104  a custom validate() method will be added to FooConfig that uses it. And,
105  of course, all of the above will be done for InnerControl/InnerConfig too.
106 
107  Any field that would be injected that would clash with an existing attribute of the
108  class will be silently ignored; this allows the user to customize fields and
109  inherit them from wrapped control classes. However, these names will still be
110  processed when converting between config and control classes, so they should generally
111  be present as base class fields or other instance attributes or descriptors.
112 
113  While LSST_CONTROL_FIELD will work for any C++ type, automatic Config generation
114  only supports bool, int, std::int64_t, double, and std::string fields, along
115  with std::list and std::vectors of those types.
116  """
117  if name is None:
118  if "Control" not in ctrl.__name__:
119  raise ValueError("Cannot guess appropriate Config class name for %s." % ctrl)
120  name = ctrl.__name__.replace("Control", "Config")
121  if cls is None:
122  cls = type(name, (base,), {"__doc__": doc})
123  if module is not None:
124  # Not only does setting __module__ make Python pretty-printers more useful,
125  # it's also necessary if we want to pickle Config objects.
126  if isinstance(module, int):
127  frame = getCallerFrame(module)
128  moduleObj = inspect.getmodule(frame)
129  moduleName = moduleObj.__name__
130  elif isinstance(module, str):
131  moduleName = module
132  moduleObj = __import__(moduleName)
133  else:
134  moduleObj = module
135  moduleName = moduleObj.__name__
136  cls.__module__ = moduleName
137  setattr(moduleObj, name, cls)
138  if doc is None:
139  doc = ctrl.__doc__
140  fields = {}
141  # loop over all class attributes, looking for the special static methods that indicate a field
142  # defined by one of the macros in pex/config.h.
143  for attr in dir(ctrl):
144  if attr.startswith("_type_"):
145  k = attr[len("_type_"):]
146  getDoc = "_doc_" + k
147  getModule = "_module_" + k
148  getType = attr
149  if hasattr(ctrl, k) and hasattr(ctrl, getDoc):
150  doc = getattr(ctrl, getDoc)()
151  ctype = getattr(ctrl, getType)()
152  if hasattr(ctrl, getModule): # if this is present, it's a nested control object
153  nestedModuleName = getattr(ctrl, getModule)()
154  if nestedModuleName == moduleName:
155  nestedModuleObj = moduleObj
156  else:
157  nestedModuleObj = importlib.import_module(nestedModuleName)
158  try:
159  dtype = getattr(nestedModuleObj, ctype).ConfigClass
160  except AttributeError:
161  raise AttributeError("'%s.%s.ConfigClass' does not exist" % (moduleName, ctype))
162  fields[k] = ConfigField(doc=doc, dtype=dtype)
163  else:
164  try:
165  dtype = _dtypeMap[ctype]
166  FieldCls = Field
167  except KeyError:
168  dtype = None
169  m = _containerRegex.match(ctype)
170  if m:
171  dtype = _dtypeMap.get(m.group("type"), None)
172  FieldCls = ListField
173  if dtype is None:
174  raise TypeError("Could not parse field type '%s'." % ctype)
175  fields[k] = FieldCls(doc=doc, dtype=dtype, optional=True)
176 
177  # Define a number of methods to put in the new Config class. Note that these are "closures";
178  # they have access to local variables defined in the makeConfigClass function (like the fields dict).
179  def makeControl(self):
180  """Construct a C++ Control object from this Config object.
181 
182  Fields set to None will be ignored, and left at the values defined by the
183  Control object's default constructor.
184  """
185  r = self.Control()
186  for k, f in fields.items():
187  value = getattr(self, k)
188  if isinstance(f, ConfigField):
189  value = value.makeControl()
190  if value is not None:
191  if isinstance(value, List):
192  setattr(r, k, value._list)
193  else:
194  setattr(r, k, value)
195  return r
196 
197  def readControl(self, control, __at=None, __label="readControl", __reset=False):
198  """Read values from a C++ Control object and assign them to self's fields.
199 
200  The __at, __label, and __reset arguments are for internal use only; they are used to
201  remove internal calls from the history.
202  """
203  if __at is None:
204  __at = getCallStack()
205  values = {}
206  for k, f in fields.items():
207  if isinstance(f, ConfigField):
208  getattr(self, k).readControl(getattr(control, k),
209  __at=__at, __label=__label, __reset=__reset)
210  else:
211  values[k] = getattr(control, k)
212  if __reset:
213  self._history = {}
214  self.update(__at=__at, __label=__label, **values)
215 
216  def validate(self):
217  """Validate the config object by constructing a control object and using
218  a C++ validate() implementation."""
219  super(cls, self).validate()
220  r = self.makeControl()
221  r.validate()
222 
223  def setDefaults(self):
224  """Initialize the config object, using the Control objects default ctor
225  to provide defaults."""
226  super(cls, self).setDefaults()
227  try:
228  r = self.Control()
229  # Indicate in the history that these values came from C++, even if we can't say which line
230  self.readControl(r, __at=[(ctrl.__name__ + " C++", 0, "setDefaults", "")], __label="defaults",
231  __reset=True)
232  except Exception:
233  pass # if we can't instantiate the Control, don't set defaults
234 
235  ctrl.ConfigClass = cls
236  cls.Control = ctrl
237  cls.makeControl = makeControl
238  cls.readControl = readControl
239  cls.setDefaults = setDefaults
240  if hasattr(ctrl, "validate"):
241  cls.validate = validate
242  for k, field in fields.items():
243  if not hasattr(cls, k):
244  setattr(cls, k, field)
245  return cls
246 
247 
248 def wrap(ctrl):
249  """A decorator that adds fields from a C++ control class to a Python config class.
250 
251  Used like this:
252 
253  @wrap(MyControlClass)
254  class MyConfigClass(Config):
255  pass
256 
257  See makeConfigClass for more information; this is equivalent to calling makeConfigClass
258  with the decorated class as the 'cls' argument.
259  """
260  def decorate(cls):
261  return makeConfigClass(ctrl, cls=cls)
262  return decorate
def wrap(ctrl)
Definition: wrap.py:248
def getCallStack(skip=0)
Definition: callStack.py:153
def getCallerFrame(relative=0)
Definition: callStack.py:29
def makeConfigClass(ctrl, name=None, base=Config, doc=None, module=0, cls=None)
Definition: wrap.py:48