summaryrefslogtreecommitdiff
path: root/lib/spack/spack/multi_function.py
blob: abceb156c0abf12f36210cb7548f30419eceb3dc (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
135
136
137
138
139
140
141
142
143
144
145
146
147
"""This module contains utilities for using multi-functions in spack.
You can think of multi-functions like overloaded functions -- they're
functions with the same name, and we need to select a version of the
function based on some criteria.  e.g., for overloaded functions, you
would select a version of the function to call based on the types of
its arguments.

For spack, we might want to select a version of the function based on
the platform we want to build a package for, or based on the versions
of the dependencies of the package.
"""
import sys
import functools

import arch
import spack.error as serr

class NoSuchVersionError(serr.SpackError):
    """Raised when we can't find a version of a function for a platform."""
    def __init__(self, fun_name, sys_type):
        super(NoSuchVersionError, self).__init__(
            "No version of %s found for %s!" % (fun_name, sys_type))


class PlatformMultiFunction(object):
    """This is a callable type for storing a collection of versions
       of an instance method.  The platform decorator (see docs below)
       creates PlatformMultiFunctions and registers function versions
       with them.

       To register a function, you can do something like this:
           pmf = PlatformMultiFunction()
           pmf.regsiter("chaos_5_x86_64_ib", some_function)

       When the pmf is actually called, it selects a version of
       the function to call based on the sys_type of the object
       it is called on.

       See the docs for the platform decorator for more details.
    """
    def __init__(self, default=None):
        self.function_map = {}
        self.default = default
        if default:
            self.__name__ = default.__name__

    def register(self, platform, function):
        """Register a version of a function for a particular sys_type."""
        self.function_map[platform] = function
        if not hasattr(self, '__name__'):
            self.__name__ = function.__name__
        else:
            assert(self.__name__ == function.__name__)

    def __get__(self, obj, objtype):
        """This makes __call__ support instance methods."""
        return functools.partial(self.__call__, obj)

    def __call__(self, package_self, *args, **kwargs):
        """Try to find a function that matches package_self.sys_type.
           If none is found, call the default function that this was
           initialized with.  If there is no default, raise an error.
        """
        # TODO: make this work with specs.
        sys_type = package_self.sys_type
        function = self.function_map.get(sys_type, self.default)
        if function:
            function(package_self, *args, **kwargs)
        else:
            raise NoSuchVersionError(self.__name__, sys_type)

    def __str__(self):
        return "<%s, %s>" % (self.default, self.function_map)


class platform(object):
    """This annotation lets packages declare platform-specific versions
       of functions like install().  For example:

       class SomePackage(Package):
           ...

           def install(self, prefix):
               # Do default install

           @platform('chaos_5_x86_64_ib')
           def install(self, prefix):
               # This will be executed instead of the default install if
               # the package's sys_type() is chaos_5_x86_64_ib.

           @platform('bgqos_0")
           def install(self, prefix):
               # This will be executed if the package's sys_type is bgqos_0

       This allows each package to have a default version of install() AND
       specialized versions for particular platforms.  The version that is
       called depends on the sys_type of SomePackage.

       Note that this works for functions other than install, as well.  So,
       if you only have part of the install that is platform specific, you
       could do this:

       class SomePackage(Package):
           ...

           def setup(self):
               # do nothing in the default case
               pass

           @platform('chaos_5_x86_64_ib')
           def setup(self):
               # do something for x86_64

           def install(self, prefix):
               # Do common install stuff
               self.setup()
               # Do more common install stuff

       If there is no specialized version for the package's sys_type, the
       default (un-decorated) version will be called.  If there is no default
       version and no specialized version, the call raises a
       NoSuchVersionError.

       Note that the default version of install() must *always* come first.
       Otherwise it will override all of the platform-specific versions.
       There's not much we can do to get around this because of the way
       decorators work.
    """
class platform(object):
    def __init__(self, sys_type):
        self.sys_type = sys_type

    def __call__(self, fun):
        # Record the sys_type as an attribute on this function
        fun.sys_type = self.sys_type

        # Get the first definition of the function in the calling scope
        calling_frame = sys._getframe(1).f_locals
        original_fun = calling_frame.get(fun.__name__)

        # Create a multifunction out of the original function if it
        # isn't one already.
        if not type(original_fun) == PlatformMultiFunction:
            original_fun = PlatformMultiFunction(original_fun)

        original_fun.register(self.sys_type, fun)
        return original_fun