Coverage for python/lsst/utils/wrappers.py: 11%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of utils.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12import sys
13import types
15import numpy as np
17__all__ = ("continueClass", "inClass", "TemplateMeta")
20INTRINSIC_SPECIAL_ATTRIBUTES = frozenset((
21 "__qualname__",
22 "__module__",
23 "__metaclass__",
24 "__dict__",
25 "__weakref__",
26 "__class__",
27 "__subclasshook__",
28 "__name__",
29 "__doc__",
30))
33def isAttributeSafeToTransfer(name, value):
34 """Return True if an attribute is safe to monkeypatch-transfer to another
35 class.
37 This rejects special methods that are defined automatically for all
38 classes, leaving only those explicitly defined in a class decorated by
39 `continueClass` or registered with an instance of `TemplateMeta`.
40 """
41 if name.startswith("__") and (value is getattr(object, name, None)
42 or name in INTRINSIC_SPECIAL_ATTRIBUTES):
43 return False
44 return True
47def continueClass(cls):
48 """Re-open the decorated class, adding any new definitions into the
49 original.
51 For example:
53 .. code-block:: python
55 class Foo:
56 pass
58 @continueClass
59 class Foo:
60 def run(self):
61 return None
63 is equivalent to:
65 .. code-block:: python
67 class Foo:
68 def run(self):
69 return None
71 .. warning::
73 Python's built-in `super` function does not behave properly in classes
74 decorated with `continueClass`. Base class methods must be invoked
75 directly using their explicit types instead.
77 """
78 orig = getattr(sys.modules[cls.__module__], cls.__name__)
79 for name in dir(cls):
80 # Common descriptors like classmethod and staticmethod can only be
81 # accessed without invoking their magic if we use __dict__; if we use
82 # getattr on those we'll get e.g. a bound method instance on the dummy
83 # class rather than a classmethod instance we can put on the target
84 # class.
85 attr = cls.__dict__.get(name, None) or getattr(cls, name)
86 if isAttributeSafeToTransfer(name, attr):
87 setattr(orig, name, attr)
88 return orig
91def inClass(cls, name=None):
92 """Add the decorated function to the given class as a method.
94 For example:
96 .. code-block:: python
98 class Foo:
99 pass
101 @inClass(Foo)
102 def run(self):
103 return None
105 is equivalent to:
107 .. code-block:: python
109 class Foo:
110 def run(self):
111 return None
113 Standard decorators like ``classmethod``, ``staticmethod``, and
114 ``property`` may be used *after* this decorator. Custom decorators
115 may only be used if they return an object with a ``__name__`` attribute
116 or the ``name`` optional argument is provided.
117 """
118 def decorate(func):
119 # Using 'name' instead of 'name1' breaks the closure because
120 # assignment signals a strictly local variable.
121 name1 = name
122 if name1 is None:
123 if hasattr(func, "__name__"):
124 name1 = func.__name__
125 else:
126 if hasattr(func, "__func__"):
127 # classmethod and staticmethod have __func__ but
128 # no __name__
129 name1 = func.__func__.__name__
130 elif hasattr(func, "fget"):
131 # property has fget but no __name__
132 name1 = func.fget.__name__
133 else:
134 raise ValueError(
135 "Could not guess attribute name for '{}'.".format(func)
136 )
137 setattr(cls, name1, func)
138 return func
139 return decorate
142class TemplateMeta(type):
143 """A metaclass for abstract base classes that tie together wrapped C++
144 template types.
146 C++ template classes are most easily wrapped with a separate Python class
147 for each template type, which results in an unnatural Python interface.
148 TemplateMeta provides a thin layer that connects these Python classes by
149 giving them a common base class and acting as a factory to construct them
150 in a consistent way.
152 To use, simply create a new class with the name of the template class, and
153 use ``TemplateMeta`` as its metaclass, and then call ``register`` on each
154 of its subclasses. This registers the class with a "type key" - usually a
155 Python representation of the C++ template types. The type key must be a
156 hashable object - strings, type objects, and tuples of these (for C++
157 classes with multiple template parameters) are good choices. Alternate
158 type keys for existing classes can be added by calling ``alias``, but only
159 after a subclass already been registered with a "primary" type key. For
160 example::
162 .. code-block:: python
164 import numpy as np
165 from ._image import ImageF, ImageD
167 class Image(metaclass=TemplateMeta):
168 pass
170 Image.register(np.float32, ImageF)
171 Image.register(np.float64, ImageD)
172 Image.alias("F", ImageF)
173 Image.alias("D", ImageD)
175 We have intentionally used ``numpy`` types as the primary keys for these
176 objects in this example, with strings as secondary aliases simply because
177 the primary key is added as a ``dtype`` attribute on the the registered
178 classes (so ``ImageF.dtype == numpy.float32`` in the above example).
180 This allows user code to construct objects directly using ``Image``, as
181 long as an extra ``dtype`` keyword argument is passed that matches one of
182 the type keys::
184 .. code-block:: python
186 img = Image(52, 64, dtype=np.float32)
188 This simply forwards additional positional and keyword arguments to the
189 wrapped template class's constructor.
191 The choice of "dtype" as the name of the template parameter is also
192 configurable, and in fact multiple template parameters are also supported,
193 by setting a ``TEMPLATE_PARAMS`` class attribute on the ABC to a tuple
194 containing the names of the template parameters. A ``TEMPLATE_DEFAULTS``
195 attribute can also be defined to a tuple of the same length containing
196 default values for the template parameters, allowing them to be omitted in
197 constructor calls. When the length of these attributes is more than one,
198 the type keys passed to ``register`` and ``alias`` should be tuple of the
199 same length; when the length of these attributes is one, type keys should
200 generally not be tuples.
202 As an aid for those writing the Python wrappers for C++ classes,
203 ``TemplateMeta`` also provides a way to add pure-Python methods and other
204 attributes to the wrapped template classes. To add a ``sum`` method to
205 all registered types, for example, we can just do::
207 .. code-block:: python
209 class Image(metaclass=TemplateMeta):
211 def sum(self):
212 return np.sum(self.getArray())
214 Image.register(np.float32, ImageF)
215 Image.register(np.float64, ImageD)
217 .. note::
219 ``TemplateMeta`` works by overriding the ``__instancecheck__`` and
220 ``__subclasscheck__`` special methods, and hence does not appear in
221 its registered subclasses' method resolution order or ``__bases__``
222 attributes. That means its attributes are not inherited by registered
223 subclasses. Instead, attributes added to an instance of
224 ``TemplateMeta`` are *copied* into the types registered with it. These
225 attributes will thus *replace* existing attributes in those classes
226 with the same name, and subclasses cannot delegate to base class
227 implementations of these methods.
229 Finally, abstract base classes that use ``TemplateMeta`` define a dict-
230 like interface for accessing their registered subclasses, providing
231 something like the C++ syntax for templates::
233 .. code-block:: python
235 Image[np.float32] -> ImageF
236 Image["D"] -> ImageD
238 Both primary dtypes and aliases can be used as keys in this interface,
239 which means types with aliases will be present multiple times in the dict.
240 To obtain the sequence of unique subclasses, use the ``__subclasses__``
241 method.
243 .. warning::
245 Python's built-in `super` function does not behave properly in classes
246 that have `TemplateMeta` as their metaclass (which should be rare, as
247 TemplateMeta ABCs will have base classes of their own)..
249 """
251 def __new__(cls, name, bases, attrs):
252 # __new__ is invoked when the abstract base class is defined (via a
253 # class statement). We save a dict of class attributes (including
254 # methods) that were defined in the class body so we can copy them
255 # to registered subclasses later.
256 # We also initialize an empty dict to store the registered subclasses.
257 attrs["_inherited"] = {k: v for k, v in attrs.items()
258 if isAttributeSafeToTransfer(k, v)}
259 # The special "TEMPLATE_PARAMS" class attribute, if defined, contains
260 # names of the template parameters, which we use to set those
261 # attributes on registered subclasses and intercept arguments to the
262 # constructor. This line removes it from the dict of things that
263 # should be inherited while setting a default of 'dtype' if it's not
264 # defined.
265 attrs["TEMPLATE_PARAMS"] = \
266 attrs["_inherited"].pop("TEMPLATE_PARAMS", ("dtype",))
267 attrs["TEMPLATE_DEFAULTS"] = \
268 attrs["_inherited"].pop("TEMPLATE_DEFAULTS",
269 (None,)*len(attrs["TEMPLATE_PARAMS"]))
270 attrs["_registry"] = dict()
271 self = type.__new__(cls, name, bases, attrs)
273 if len(self.TEMPLATE_PARAMS) == 0:
274 raise ValueError(
275 "TEMPLATE_PARAMS must be a tuple with at least one element."
276 )
277 if len(self.TEMPLATE_DEFAULTS) != len(self.TEMPLATE_PARAMS):
278 raise ValueError(
279 "TEMPLATE_PARAMS and TEMPLATE_DEFAULTS must have same length."
280 )
281 return self
283 def __call__(cls, *args, **kwds):
284 # __call__ is invoked when someone tries to construct an instance of
285 # the abstract base class.
286 # If the ABC defines a "TEMPLATE_PARAMS" attribute, we use those
287 # strings as the kwargs we should intercept to find the right type.
288 # Generate a type mapping key from input keywords. If the type returned
289 # from the keyword lookup is a numpy dtype object, fetch the underlying
290 # type of the dtype
291 key = []
292 for p, d in zip(cls.TEMPLATE_PARAMS, cls.TEMPLATE_DEFAULTS):
293 tempKey = kwds.pop(p, d)
294 if isinstance(tempKey, np.dtype):
295 tempKey = tempKey.type
296 key.append(tempKey)
297 key = tuple(key)
299 # indices are only tuples if there are multiple elements
300 clz = cls._registry.get(key[0] if len(key) == 1 else key, None)
301 if clz is None:
302 d = {k: v for k, v in zip(cls.TEMPLATE_PARAMS, key)}
303 raise TypeError("No registered subclass for {}.".format(d))
304 return clz(*args, **kwds)
306 def __subclasscheck__(cls, subclass):
307 # Special method hook for the issubclass built-in: we return true for
308 # any registered type or true subclass thereof.
309 if subclass in cls._registry:
310 return True
311 for v in cls._registry.values():
312 if issubclass(subclass, v):
313 return True
314 return False
316 def __instancecheck__(cls, instance):
317 # Special method hook for the isinstance built-in: we return true for
318 # an instance of any registered type or true subclass thereof.
319 if type(instance) in cls._registry:
320 return True
321 for v in cls._registry.values():
322 if isinstance(instance, v):
323 return True
324 return False
326 def __subclasses__(cls):
327 """Return a tuple of all classes that inherit from this class.
328 """
329 # This special method isn't defined as part of the Python data model,
330 # but it exists on builtins (including ABCMeta), and it provides useful
331 # functionality.
332 return tuple(set(cls._registry.values()))
334 def register(cls, key, subclass):
335 """Register a subclass of this ABC with the given key (a string,
336 number, type, or other hashable).
338 Register may only be called once for a given key or a given subclass.
339 """
340 if key is None:
341 raise ValueError("None may not be used as a key.")
342 if subclass in cls._registry.values():
343 raise ValueError(
344 "This subclass has already registered with another key; "
345 "use alias() instead."
346 )
347 if cls._registry.setdefault(key, subclass) != subclass:
348 if len(cls.TEMPLATE_PARAMS) == 1:
349 d = {cls.TEMPLATE_PARAMS[0]: key}
350 else:
351 d = {k: v for k, v in zip(cls.TEMPLATE_PARAMS, key)}
352 raise KeyError(
353 "Another subclass is already registered with {}".format(d)
354 )
355 # If the key used to register a class matches the default key,
356 # make the static methods available through the ABC
357 if cls.TEMPLATE_DEFAULTS:
358 defaults = (cls.TEMPLATE_DEFAULTS[0] if
359 len(cls.TEMPLATE_DEFAULTS) == 1 else
360 cls.TEMPLATE_DEFAULTS)
361 if key == defaults:
362 conflictStr = ("Base class has attribute {}"
363 " which is a {} method of {}."
364 " Cannot link method to base class.")
365 # In the following if statements, the explicit lookup in
366 # __dict__ must be done, as a call to getattr returns the
367 # bound method, which no longer reports as a static or class
368 # method. The static methods must be transfered to the ABC
369 # in this unbound state, so that python will still see them
370 # as static methods and not attempt to pass self. The class
371 # methods must be transfered to the ABC as a bound method
372 # so that the correct cls be called with the class method
373 for name in subclass.__dict__:
374 if name in ("__new__", "__init_subclass__"):
375 continue
376 obj = subclass.__dict__[name]
377 # copy over the static methods
378 isBuiltin = isinstance(obj, types.BuiltinFunctionType)
379 isStatic = isinstance(obj, staticmethod)
380 if isBuiltin or isStatic:
381 if hasattr(cls, name):
382 raise AttributeError(
383 conflictStr.format(name, "static", subclass))
384 setattr(cls, name, obj)
385 # copy over the class methods
386 elif isinstance(obj, classmethod):
387 if hasattr(cls, name):
388 raise AttributeError(
389 conflictStr.format(name, "class", subclass))
390 setattr(cls, name, getattr(subclass, name))
392 def setattrSafe(name, value):
393 try:
394 currentValue = getattr(subclass, name)
395 if currentValue != value:
396 msg = ("subclass already has a '{}' attribute with "
397 "value {} != {}.")
398 raise ValueError(
399 msg.format(name, currentValue, value)
400 )
401 except AttributeError:
402 setattr(subclass, name, value)
404 if len(cls.TEMPLATE_PARAMS) == 1:
405 setattrSafe(cls.TEMPLATE_PARAMS[0], key)
406 elif len(cls.TEMPLATE_PARAMS) == len(key):
407 for p, k in zip(cls.TEMPLATE_PARAMS, key):
408 setattrSafe(p, k)
409 else:
410 raise ValueError(
411 "key must have {} elements (one for each of {})".format(
412 len(cls.TEMPLATE_PARAMS), cls.TEMPLATE_PARAMS
413 )
414 )
416 for name, attr in cls._inherited.items():
417 setattr(subclass, name, attr)
419 def alias(cls, key, subclass):
420 """Add an alias that allows an existing subclass to be accessed with a
421 different key.
422 """
423 if key is None:
424 raise ValueError("None may not be used as a key.")
425 if key in cls._registry:
426 raise KeyError("Cannot multiply-register key {}".format(key))
427 primaryKey = tuple(getattr(subclass, p, None)
428 for p in cls.TEMPLATE_PARAMS)
429 if len(primaryKey) == 1:
430 # indices are only tuples if there are multiple elements
431 primaryKey = primaryKey[0]
432 if cls._registry.get(primaryKey, None) != subclass:
433 raise ValueError("Subclass is not registered with this base class.")
434 cls._registry[key] = subclass
436 # Immutable mapping interface defined below. We don't use collections
437 # mixins because we don't want their comparison operators.
439 def __getitem__(cls, key):
440 return cls._registry[key]
442 def __iter__(cls):
443 return iter(cls._registry)
445 def __len__(cls):
446 return len(cls._registry)
448 def __contains__(cls, key):
449 return key in cls._registry
451 def keys(cls):
452 """Return an iterable containing all keys (including aliases).
453 """
454 return cls._registry.keys()
456 def values(cls):
457 """Return an iterable of registered subclasses, with duplicates
458 corresponding to any aliases.
459 """
460 return cls._registry.values()
462 def items(cls):
463 """Return an iterable of (key, subclass) pairs.
464 """
465 return cls._registry.items()
467 def get(cls, key, default=None):
468 """Return the subclass associated with the given key (including
469 aliases), or ``default`` if the key is not recognized.
470 """
471 return cls._registry.get(key, default)