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