Repository: juju-solutions/charms.reactive

Issue:
only_once help unclear
_From @jacekn on January 27, 2016 17:29_

I was trying to execute certain function only once in my code.

This does not work:
```
@only_once()
def basenode():
    print("in basenode")
```
```
TypeError: only_once() missing 1 required positional argument: 'action'
```

I tried like this but it also did not work:
```
@only_once("basenode")
def basenode():
    print("in basenode")
```
```
AttributeError: 'str' object has no attribute '__code__'
```
Can documentation be clarified to show correct use of this decorator?

_Copied from original issue: juju/charm-tools#94_

Discussion:

Patch:
diff --git a/charms/reactive/bus.py b/charms/reactive/bus.py
index 885e498..853571a 100644
--- a/charms/reactive/bus.py
+++ b/charms/reactive/bus.py
@@ -229,6 +229,7 @@ class Handler(object):
         self._action = action
         self._args = []
         self._predicates = []
+        self._post_callbacks = []
         self._states = set()
 
     def id(self):
@@ -255,6 +256,12 @@ class Handler(object):
             hookenv.log('  Adding predicate for %s: %s' % (self.id(), _predicate), level=hookenv.DEBUG)
         self._predicates.append(predicate)
 
+    def add_post_callback(self, callback):
+        """
+        Add a callback to be run after the action is invoked.
+        """
+        self._post_callbacks.append(callback)
+
     def test(self):
         """
         Check the predicate(s) and return True if this handler should be invoked.
@@ -278,6 +285,8 @@ class Handler(object):
         """
         args = self._get_args()
         self._action(*args)
+        for callback in self._post_callbacks:
+            callback()
 
     def register_states(self, states):
         """
diff --git a/charms/reactive/decorators.py b/charms/reactive/decorators.py
index 7918106..e89332f 100644
--- a/charms/reactive/decorators.py
+++ b/charms/reactive/decorators.py
@@ -205,18 +205,18 @@ def not_unless(*desired_states):
     return _decorator
 
 
-def only_once(action):
+def only_once(action=None):
     """
-    Ensure that the decorated function is only executed the first time it is called.
+    Register the decorated function to be run once, and only once.
 
-    This can be used on reactive handlers to ensure that they are only triggered
-    once, even if their conditions continue to match on subsequent calls, even
-    across hook invocations.
+    This decorator will never cause arguments to be passed to the handler.
     """
-    @wraps(action)
-    def wrapper(*args, **kwargs):
-        action_id = _action_id(action)
-        if not was_invoked(action_id):
-            action(*args, **kwargs)
-            mark_invoked(action_id)
-    return wrapper
+    if action is None:
+        # allow to be used as @only_once or @only_once()
+        return only_once
+
+    action_id = _action_id(action)
+    handler = Handler.get(action)
+    handler.add_predicate(lambda: not was_invoked(action_id))
+    handler.add_post_callback(partial(mark_invoked, action_id))
+    return action


Test patch:
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index 4691a30..2599b53 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -241,11 +241,28 @@ class TestReactiveDecorators(unittest.TestCase):
         calls = []
 
         @reactive.decorators.only_once
-        def test(num):
-            calls.append(num)
+        def test():
+            calls.append(len(calls)+1)
+
+        handler = reactive.bus.Handler.get(test)
 
-        test(1)
-        test(2)
+        assert handler.test()
+        handler.invoke()
+        assert not handler.test()
+        self.assertEquals(calls, [1])
+
+    def test_only_once_parens(self):
+        calls = []
+
+        @reactive.decorators.only_once()
+        def test():
+            calls.append(len(calls)+1)
+
+        handler = reactive.bus.Handler.get(test)
+
+        assert handler.test()
+        handler.invoke()
+        assert not handler.test()
         self.assertEquals(calls, [1])
 
     def test_multi(self):