~lazypower/charms/trusty/idlerpg/trunk

« back to all changes in this revision

Viewing changes to lib/charms/reactive/decorators.py

  • Committer: Charles Butler
  • Date: 2015-10-21 03:37:56 UTC
  • Revision ID: chuck@dasroot.net-20151021033756-abwxkds2746q9qog
Initial Commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
5
# charm-helpers is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU Lesser General Public License version 3 as
 
7
# published by the Free Software Foundation.
 
8
#
 
9
# charm-helpers is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public License
 
15
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
16
 
 
17
from six.moves import filter, map
 
18
from functools import wraps, partial
 
19
 
 
20
from charmhelpers.core import hookenv
 
21
from charms.reactive.bus import Handler
 
22
from charms.reactive.bus import get_states
 
23
from charms.reactive.bus import _action_id
 
24
from charms.reactive.relations import RelationBase
 
25
from charms.reactive.helpers import _hook
 
26
from charms.reactive.helpers import _when
 
27
from charms.reactive.helpers import any_file_changed
 
28
from charms.reactive.helpers import was_invoked
 
29
from charms.reactive.helpers import mark_invoked
 
30
 
 
31
 
 
32
def hook(*hook_patterns):
 
33
    """
 
34
    Register the decorated function to run when the current hook matches any of
 
35
    the ``hook_patterns``.
 
36
 
 
37
    The hook patterns can use the ``{interface:...}`` and ``{A,B,...}`` syntax
 
38
    supported by :func:`~charms.reactive.bus.any_hook`.
 
39
 
 
40
    If the hook is a relation hook, an instance of that relation class will be
 
41
    passed in to the decorated function.
 
42
 
 
43
    For example, to match any joined or changed hook for the relation providing
 
44
    the ``mysql`` interface::
 
45
 
 
46
        class MySQLRelation(RelationBase):
 
47
            @hook('{provides:mysql}-relation-{joined,changed}')
 
48
            def joined_or_changed(self):
 
49
                pass
 
50
 
 
51
    Note that hook decorators **cannot** be combined with :func:`when` or
 
52
    :func:`when_not` decorators.
 
53
    """
 
54
    def _register(action):
 
55
        def arg_gen():
 
56
            # use a generator to defer calling of hookenv.relation_type, for tests
 
57
            rel = RelationBase.from_name(hookenv.relation_type())
 
58
            if rel:
 
59
                yield rel
 
60
 
 
61
        handler = Handler.get(action)
 
62
        handler.add_predicate(partial(_hook, hook_patterns))
 
63
        handler.add_args(arg_gen())
 
64
        return action
 
65
    return _register
 
66
 
 
67
 
 
68
def when(*desired_states):
 
69
    """
 
70
    Register the decorated function to run when all ``desired_states`` are active.
 
71
 
 
72
    This decorator will pass zero or more relation instances to the handler, if
 
73
    any of the states are associated with relations.  If so, they will be passed
 
74
    in in the same order that the states are given to the decorator.
 
75
 
 
76
    Note that handlers whose conditions match are triggered at least once per
 
77
    hook invocation.
 
78
    """
 
79
    def _register(action):
 
80
        handler = Handler.get(action)
 
81
        handler.add_predicate(partial(_when, desired_states, False))
 
82
        handler.add_args(filter(None, map(RelationBase.from_state, desired_states)))
 
83
        handler.register_states(desired_states)
 
84
        return action
 
85
    return _register
 
86
 
 
87
 
 
88
def when_not(*desired_states):
 
89
    """
 
90
    Register the decorated function to run when **not** all desired_states are active.
 
91
 
 
92
    This decorator will never cause arguments to be passed to the handler.
 
93
 
 
94
    Note that handlers whose conditions match are triggered at least once per
 
95
    hook invocation.
 
96
    """
 
97
    def _register(action):
 
98
        handler = Handler.get(action)
 
99
        handler.add_predicate(partial(_when, desired_states, True))
 
100
        handler.register_states(desired_states)
 
101
        return action
 
102
    return _register
 
103
 
 
104
 
 
105
def when_file_changed(*filenames, **kwargs):
 
106
    """
 
107
    Register the decorated function to run when one or more files have changed.
 
108
 
 
109
    :param list filenames: The names of one or more files to check for changes.
 
110
    :param str hash_type: The type of hash to use for determining if a file has
 
111
        changed.  Defaults to 'md5'.  Must be given as a kwarg.
 
112
    """
 
113
    def _register(action):
 
114
        handler = Handler.get(action)
 
115
        handler.add_predicate(partial(any_file_changed, filenames, **kwargs))
 
116
        return action
 
117
    return _register
 
118
 
 
119
 
 
120
def not_unless(*desired_states):
 
121
    """
 
122
    Assert that the decorated function can only be called if the desired_states
 
123
    are active.
 
124
 
 
125
    Note that, unlike :func:`when`, this does **not** trigger the decorated
 
126
    function if the states match.  It **only** raises an exception if the
 
127
    function is called when the states do not match.
 
128
 
 
129
    This is primarily for informational purposes and as a guard clause.
 
130
    """
 
131
    def _decorator(func):
 
132
        @wraps(func)
 
133
        def _wrapped(*args, **kwargs):
 
134
            active_states = get_states()
 
135
            missing_states = [state for state in desired_states if state not in active_states]
 
136
            if missing_states:
 
137
                func_id = "%s:%s:%s" % (func.__code__.co_filename,
 
138
                                        func.__code__.co_firstlineno,
 
139
                                        func.__code__.co_name)
 
140
                hookenv.log('%s called before state%s: %s' % (
 
141
                    func_id,
 
142
                    's' if len(missing_states) > 1 else '',
 
143
                    ', '.join(missing_states)), hookenv.WARNING)
 
144
            return func(*args, **kwargs)
 
145
        return _wrapped
 
146
    return _decorator
 
147
 
 
148
 
 
149
def only_once(action):
 
150
    """
 
151
    Ensure that the decorated function is only executed the first time it is called.
 
152
 
 
153
    This can be used on reactive handlers to ensure that they are only triggered
 
154
    once, even if their conditions continue to match on subsequent calls, even
 
155
    across hook invocations.
 
156
    """
 
157
    @wraps(action)
 
158
    def wrapper(*args, **kwargs):
 
159
        action_id = _action_id(action)
 
160
        if not was_invoked(action_id):
 
161
            action(*args, **kwargs)
 
162
            mark_invoked(action_id)
 
163
    return wrapper