LSSTApplications  10.0-2-g4f67435,11.0.rc2+1,11.0.rc2+12,11.0.rc2+3,11.0.rc2+4,11.0.rc2+5,11.0.rc2+6,11.0.rc2+7,11.0.rc2+8
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 
23 import inspect
24 import re
25 import importlib
26 import traceback
27 
28 from .config import Config, Field
29 from .listField import ListField
30 from .configField import ConfigField
31 
32 __all__ = ("wrap", "makeConfigClass")
33 
34 # Mapping from C++ types to Python type: assumes we can round-trip between these using
35 # the usual Swig converters, but doesn't require they be binary equivalent under-the-hood
36 # or anything.
37 _dtypeMap = {
38  "bool": bool,
39  "int": int,
40  "double": float,
41  "float": float,
42  "boost::int64_t": int,
43  "std::string": str
44 }
45 
46 _containerRegex = re.compile(r"(std::)?(vector|list)<\s*(?P<type>[a-z0-9_:]+)\s*>")
47 
48 def makeConfigClass(ctrl, name=None, base=Config, doc=None, module=1, 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 pex.config.wrap, 1 is the immediate caller, etc. 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, mySwigLib, 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, Swig 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  @code
91  // mySwigLib.i
92  %{
93  #include "myHeader.h"
94  %}
95 
96  %include "lsst/pex/config.h"
97  %include "myHeader.h"
98  @endcode
99 
100  Now, in Python, do this:
101 
102  @code
103  import mySwigLib
104  import lsst.pex.config
105  InnerConfig = lsst.pex.config.makeConfigClass(mySwigLib.InnerControl)
106  FooConfig = lsst.pex.config.makeConfigClass(mySwigLib.FooControl)
107  @endcode
108 
109  This will add fully-fledged "bar", "baz", and "zot" fields to FooConfig, set
110  FooConfig.Control = FooControl, and inject makeControl and readControl
111  methods to create a FooControl and set the FooConfig from the FooControl,
112  respectively. In addition, if FooControl has a validate() member function,
113  a custom validate() method will be added to FooConfig that uses it. And,
114  of course, all of the above will be done for InnerControl/InnerConfig too.
115 
116  Any field that would be injected that would clash with an existing attribute of the
117  class will be silently ignored; this allows the user to customize fields and
118  inherit them from wrapped control classes. However, these names will still be
119  processed when converting between config and control classes, so they should generally
120  be present as base class fields or other instance attributes or descriptors.
121 
122  While LSST_CONTROL_FIELD will work for any C++ type, automatic Config generation
123  only supports bool, int, boost::int64_t, double, and std::string fields, along
124  with std::list and std::vectors of those types.
125  """
126  if name is None:
127  if "Control" not in ctrl.__name__:
128  raise ValueError("Cannot guess appropriate Config class name for %s." % ctrl)
129  name = ctrl.__name__.replace("Control", "Config")
130  if cls is None:
131  cls = type(name, (base,), {"__doc__":doc})
132  if module is not None:
133  # Not only does setting __module__ make Python pretty-printers more useful,
134  # it's also necessary if we want to pickle Config objects.
135  if isinstance(module, int):
136  frame = inspect.stack()[module]
137  moduleObj = inspect.getmodule(frame[0])
138  moduleName = moduleObj.__name__
139  elif isinstance(module, basestring):
140  moduleName = module
141  moduleObj = __import__(moduleName)
142  else:
143  moduleObj = module
144  moduleName = moduleObj.__name__
145  cls.__module__ = moduleName
146  setattr(moduleObj, name, cls)
147  if doc is None:
148  doc = ctrl.__doc__
149  fields = {}
150  # loop over all class attributes, looking for the special static methods that indicate a field
151  # defined by one of the macros in pex/config.h.
152  for attr in dir(ctrl):
153  if attr.startswith("_type_"):
154  k = attr[len("_type_"):]
155  getDoc = "_doc_" + k
156  getModule = "_module_" + k
157  getType = attr
158  if hasattr(ctrl, k) and hasattr(ctrl, getDoc):
159  doc = getattr(ctrl, getDoc)()
160  ctype = getattr(ctrl, getType)()
161  if hasattr(ctrl, getModule): # if this is present, it's a nested control object
162  nestedModuleName = getattr(ctrl, getModule)()
163  if nestedModuleName == moduleName:
164  nestedModuleObj = moduleObj
165  else:
166  nestedModuleObj = importlib.import_module(nestedModuleName)
167  try:
168  dtype = getattr(nestedModuleObj, ctype).ConfigClass
169  except AttributeError:
170  raise AttributeError("'%s.%s.ConfigClass' does not exist" % (moduleName, ctype))
171  fields[k] = ConfigField(doc=doc, dtype=dtype)
172  else:
173  try:
174  dtype = _dtypeMap[ctype]
175  FieldCls = Field
176  except KeyError:
177  dtype = None
178  m = _containerRegex.match(ctype)
179  if m:
180  dtype = _dtypeMap.get(m.group("type"), None)
181  FieldCls = ListField
182  if dtype is None:
183  raise TypeError("Could not parse field type '%s'." % ctype)
184  fields[k] = FieldCls(doc=doc, dtype=dtype, optional=True)
185  # Define a number of methods to put in the new Config class. Note that these are "closures";
186  # they have access to local variables defined in the makeConfigClass function (like the fields dict).
187  def makeControl(self):
188  """Construct a C++ Control object from this Config object.
189 
190  Fields set to None will be ignored, and left at the values defined by the
191  Control object's default constructor.
192  """
193  r = self.Control()
194  for k, f in fields.iteritems():
195  value = getattr(self, k)
196  if isinstance(f, ConfigField):
197  value = value.makeControl()
198  if value is not None:
199  setattr(r, k, value)
200  return r
201  def readControl(self, control, __at=None, __label="readControl", __reset=False):
202  """Read values from a C++ Control object and assign them to self's fields.
203 
204  The __at, __label, and __reset arguments are for internal use only; they are used to
205  remove internal calls from the history.
206  """
207  if __at is None: __at = traceback.extract_stack()[:-1]
208  values = {}
209  for k, f in fields.iteritems():
210  if isinstance(f, ConfigField):
211  getattr(self, k).readControl(getattr(control, k),
212  __at=__at, __label=__label, __reset=__reset)
213  else:
214  values[k] = getattr(control, k)
215  if __reset:
216  self._history = {}
217  self.update(__at=__at, __label=__label, **values)
218  def validate(self):
219  """Validate the config object by constructing a control object and using
220  a C++ validate() implementation."""
221  super(cls, self).validate()
222  r = self.makeControl()
223  r.validate()
224  def setDefaults(self):
225  """Initialize the config object, using the Control objects default ctor
226  to provide defaults."""
227  super(cls, self).setDefaults()
228  try:
229  r = self.Control()
230  # Indicate in the history that these values came from C++, even if we can't say which line
231  self.readControl(r, __at=[(ctrl.__name__ + " C++", 0, "setDefaults", "")], __label="defaults",
232  __reset=True)
233  except:
234  pass # if we can't instantiate the Control, don't set defaults
235 
236  ctrl.ConfigClass = cls
237  cls.Control = ctrl
238  cls.makeControl = makeControl
239  cls.readControl = readControl
240  cls.setDefaults = setDefaults
241  if hasattr(ctrl, "validate"):
242  cls.validate = validate
243  for k, field in fields.iteritems():
244  if not hasattr(cls, k):
245  setattr(cls, k, field)
246  return cls
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