summaryrefslogtreecommitdiff
path: root/lib/spack/spack/util/pattern.py
blob: 49ef583dcb67c9f37b56f4764aecf9ed8369ec7a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# Copyright 2013-2024 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 collections.abc
import functools
import inspect


class Delegate:
    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]


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)


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 (collections.abc.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, collections.abc.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:
            """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


class Bunch:
    """Carries a bunch of named attributes (from Alex Martelli bunch)"""

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


class Args(Bunch):
    """Subclass of Bunch to write argparse args more naturally."""

    def __init__(self, *flags, **kwargs):
        super().__init__(flags=tuple(flags), kwargs=kwargs)