lsst.pex.config  13.0-4-gbcd7061+9
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 
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 __all__ = ("wrap", "makeConfigClass")
34 
35 # Mapping from C++ types to Python type: assumes we can round-trip between these using
36 # the usual pybind11 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": basestring
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=0, 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 the immediate caller of pex.config.wrap. 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, myWrappedLib, 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, wrap 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 
93  Now, in Python, do this:
94 
95  @code
96  import myWrappedLib
97  import lsst.pex.config
98  InnerConfig = lsst.pex.config.makeConfigClass(myWrappedLib.InnerControl)
99  FooConfig = lsst.pex.config.makeConfigClass(myWrappedLib.FooControl)
100  @endcode
101 
102  This will add fully-fledged "bar", "baz", and "zot" fields to FooConfig, set
103  FooConfig.Control = FooControl, and inject makeControl and readControl
104  methods to create a FooControl and set the FooConfig from the FooControl,
105  respectively. In addition, if FooControl has a validate() member function,
106  a custom validate() method will be added to FooConfig that uses it. And,
107  of course, all of the above will be done for InnerControl/InnerConfig too.
108 
109  Any field that would be injected that would clash with an existing attribute of the
110  class will be silently ignored; this allows the user to customize fields and
111  inherit them from wrapped control classes. However, these names will still be
112  processed when converting between config and control classes, so they should generally
113  be present as base class fields or other instance attributes or descriptors.
114 
115  While LSST_CONTROL_FIELD will work for any C++ type, automatic Config generation
116  only supports bool, int, std::int64_t, double, and std::string fields, along
117  with std::list and std::vectors of those types.
118  """
119  if name is None:
120  if "Control" not in ctrl.__name__:
121  raise ValueError("Cannot guess appropriate Config class name for %s." % ctrl)
122  name = ctrl.__name__.replace("Control", "Config")
123  if cls is None:
124  cls = type(name, (base,), {"__doc__": doc})
125  if module is not None:
126  # Not only does setting __module__ make Python pretty-printers more useful,
127  # it's also necessary if we want to pickle Config objects.
128  if isinstance(module, int):
129  frame = getCallerFrame(module)
130  moduleObj = inspect.getmodule(frame)
131  moduleName = moduleObj.__name__
132  elif isinstance(module, basestring):
133  moduleName = module
134  moduleObj = __import__(moduleName)
135  else:
136  moduleObj = module
137  moduleName = moduleObj.__name__
138  cls.__module__ = moduleName
139  setattr(moduleObj, name, cls)
140  if doc is None:
141  doc = ctrl.__doc__
142  fields = {}
143  # loop over all class attributes, looking for the special static methods that indicate a field
144  # defined by one of the macros in pex/config.h.
145  for attr in dir(ctrl):
146  if attr.startswith("_type_"):
147  k = attr[len("_type_"):]
148  getDoc = "_doc_" + k
149  getModule = "_module_" + k
150  getType = attr
151  if hasattr(ctrl, k) and hasattr(ctrl, getDoc):
152  doc = getattr(ctrl, getDoc)()
153  ctype = getattr(ctrl, getType)()
154  if hasattr(ctrl, getModule): # if this is present, it's a nested control object
155  nestedModuleName = getattr(ctrl, getModule)()
156  if nestedModuleName == moduleName:
157  nestedModuleObj = moduleObj
158  else:
159  nestedModuleObj = importlib.import_module(nestedModuleName)
160  try:
161  dtype = getattr(nestedModuleObj, ctype).ConfigClass
162  except AttributeError:
163  raise AttributeError("'%s.%s.ConfigClass' does not exist" % (moduleName, ctype))
164  fields[k] = ConfigField(doc=doc, dtype=dtype)
165  else:
166  try:
167  dtype = _dtypeMap[ctype]
168  FieldCls = Field
169  except KeyError:
170  dtype = None
171  m = _containerRegex.match(ctype)
172  if m:
173  dtype = _dtypeMap.get(m.group("type"), None)
174  FieldCls = ListField
175  if dtype is None:
176  raise TypeError("Could not parse field type '%s'." % ctype)
177  fields[k] = FieldCls(doc=doc, dtype=dtype, optional=True)
178 
179  # Define a number of methods to put in the new Config class. Note that these are "closures";
180  # they have access to local variables defined in the makeConfigClass function (like the fields dict).
181  def makeControl(self):
182  """Construct a C++ Control object from this Config object.
183 
184  Fields set to None will be ignored, and left at the values defined by the
185  Control object's default constructor.
186  """
187  r = self.Control()
188  for k, f in fields.items():
189  value = getattr(self, k)
190  if isinstance(f, ConfigField):
191  value = value.makeControl()
192  if value is not None:
193  if isinstance(value, List):
194  setattr(r, k, value._list)
195  else:
196  setattr(r, k, value)
197  return r
198 
199  def readControl(self, control, __at=None, __label="readControl", __reset=False):
200  """Read values from a C++ Control object and assign them to self's fields.
201 
202  The __at, __label, and __reset arguments are for internal use only; they are used to
203  remove internal calls from the history.
204  """
205  if __at is None:
206  __at = getCallStack()
207  values = {}
208  for k, f in fields.items():
209  if isinstance(f, ConfigField):
210  getattr(self, k).readControl(getattr(control, k),
211  __at=__at, __label=__label, __reset=__reset)
212  else:
213  values[k] = getattr(control, k)
214  if __reset:
215  self._history = {}
216  self.update(__at=__at, __label=__label, **values)
217 
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 
225  def setDefaults(self):
226  """Initialize the config object, using the Control objects default ctor
227  to provide defaults."""
228  super(cls, self).setDefaults()
229  try:
230  r = self.Control()
231  # Indicate in the history that these values came from C++, even if we can't say which line
232  self.readControl(r, __at=[(ctrl.__name__ + " C++", 0, "setDefaults", "")], __label="defaults",
233  __reset=True)
234  except:
235  pass # if we can't instantiate the Control, don't set defaults
236 
237  ctrl.ConfigClass = cls
238  cls.Control = ctrl
239  cls.makeControl = makeControl
240  cls.readControl = readControl
241  cls.setDefaults = setDefaults
242  if hasattr(ctrl, "validate"):
243  cls.validate = validate
244  for k, field in fields.items():
245  if not hasattr(cls, k):
246  setattr(cls, k, field)
247  return cls
248 
249 
250 def wrap(ctrl):
251  """A decorator that adds fields from a C++ control class to a Python config class.
252 
253  Used like this:
254 
255  @wrap(MyControlClass)
256  class MyConfigClass(Config):
257  pass
258 
259  See makeConfigClass for more information; this is equivalent to calling makeConfigClass
260  with the decorated class as the 'cls' argument.
261  """
262  def decorate(cls):
263  return makeConfigClass(ctrl, cls=cls)
264  return decorate
def wrap(ctrl)
Definition: wrap.py:250
def getCallStack(skip=0)
Definition: callStack.py:157
def getCallerFrame(relative=0)
Definition: callStack.py:33
def makeConfigClass(ctrl, name=None, base=Config, doc=None, module=0, cls=None)
Definition: wrap.py:50