# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import functools
import inspect
from llnl.util.compat import MutableSequence
[docs]class Delegate(object):
def __init__(self, name, container):
self.name = name
self.container = container
def __call__(self, *args, **kwargs):
return [getattr(item, self.name)(*args, **kwargs)
for item in self.container]
[docs]class Composite(list):
def __init__(self, fns_to_delegate):
self.fns_to_delegate = fns_to_delegate
def __getattr__(self, name):
if name != 'fns_to_delegate' and name in self.fns_to_delegate:
return Delegate(name, self)
else:
return self.__getattribute__(name)
[docs]def composite(interface=None, method_list=None, container=list):
"""Decorator implementing the GoF composite pattern.
Args:
interface (type): class exposing the interface to which the
composite object must conform. Only non-private and
non-special methods will be taken into account
method_list (list): names of methods that should be part
of the composite
container (MutableSequence): container for the composite object
(default = list). Must fulfill the MutableSequence
contract. The composite class will expose the container API
to manage object composition
Returns:
a class decorator that patches a class adding all the methods
it needs to be a composite for a given interface.
"""
# Check if container fulfills the MutableSequence contract and raise an
# exception if it doesn't. The patched class returned by the decorator will
# inherit from the container class to expose the interface needed to manage
# objects composition
if not issubclass(container, MutableSequence):
raise TypeError("Container must fulfill the MutableSequence contract")
# Check if at least one of the 'interface' or the 'method_list' arguments
# are defined
if interface is None and method_list is None:
raise TypeError(
"Either 'interface' or 'method_list' must be defined on a call "
"to composite")
def cls_decorator(cls):
# Retrieve the base class of the composite. Inspect its methods and
# decide which ones will be overridden
def no_special_no_private(x):
return callable(x) and not x.__name__.startswith('_')
# Patch the behavior of each of the methods in the previous list.
# This is done associating an instance of the descriptor below to
# any method that needs to be patched.
class IterateOver(object):
"""Decorator used to patch methods in a composite.
It iterates over all the items in the instance containing the
associated attribute and calls for each of them an attribute
with the same name
"""
def __init__(self, name, func=None):
self.name = name
self.func = func
def __get__(self, instance, owner):
def getter(*args, **kwargs):
for item in instance:
getattr(item, self.name)(*args, **kwargs)
# If we are using this descriptor to wrap a method from an
# interface, then we must conditionally use the
# `functools.wraps` decorator to set the appropriate fields
if self.func is not None:
getter = functools.wraps(self.func)(getter)
return getter
dictionary_for_type_call = {}
# Construct a dictionary with the methods explicitly passed as name
if method_list is not None:
dictionary_for_type_call.update(
(name, IterateOver(name)) for name in method_list)
# Construct a dictionary with the methods inspected from the interface
if interface is not None:
dictionary_for_type_call.update(
(name, IterateOver(name, method))
for name, method in inspect.getmembers(
interface, predicate=no_special_no_private))
# Get the methods that are defined in the scope of the composite
# class and override any previous definition
dictionary_for_type_call.update(
(name, method) for name, method in inspect.getmembers(
cls, predicate=inspect.ismethod))
# Generate the new class on the fly and return it
# FIXME : inherit from interface if we start to use ABC classes?
wrapper_class = type(cls.__name__, (cls, container),
dictionary_for_type_call)
return wrapper_class
return cls_decorator
[docs]class Bunch(object):
"""Carries a bunch of named attributes (from Alex Martelli bunch)"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
[docs]class Args(Bunch):
"""Subclass of Bunch to write argparse args more naturally."""
def __init__(self, *flags, **kwargs):
super(Args, self).__init__(flags=tuple(flags), kwargs=kwargs)