Repository: kivy/kivy

Issue:
Callback sometimes still called when unscheduling in same frame
When running the following code you'll notice that if we unschedule the callback in the same frame, the callback will still be called::

```
from kivy.app import runTouchApp
from kivy.uix.button import Button
from kivy.clock import Clock


class Button2(Button):

    lambda_a = None
    lambda_b = None

    def __init__(self, **kw):
        super(Button2, self).__init__(**kw)
        self.lambda_a = lambda dt: self.func_a()
        self.lambda_b = lambda dt: self.func_b()

    def func_b(self):
        print('func_b')

    def func_a(self):
        print('Unscheduling b')
        Clock.unschedule(self.lambda_b)

    def on_press(self, *l):
        Clock.schedule_once(self.lambda_a)
        Clock.schedule_once(self.lambda_b)


runTouchApp(Button2())
```

The output is:

```
Unscheduling b
func_b
```

The reason for this is because when the scheduled object is hashed, lambda in this case, the name is used for the hash (https://github.com/kivy/kivy/blob/master/kivy/clock.py#L218). In this case, all the lambdas get hashed to `<lambda>`. 

Therefore, when the events are iterated through, for each hash, we copy the list of events (https://github.com/kivy/kivy/blob/master/kivy/clock.py#L509) and dispatch them. So all the events that share a hash won't be unscheduled because we're working from the copied list which won't be updated when we unschedule.

This can be fixed, by assigning a unique name to the lambda. However, this is a general issue. I don't know whether this should be considered a bug and maybe use a smarter hash, or if we should just post a big warning about this on the docs?



Discussion:
In particular, I'll add the warning to the docs if it is decided that that is just how it works.


Resolving patch:
diff --git a/kivy/clock.py b/kivy/clock.py
--- a/kivy/clock.py
+++ b/kivy/clock.py
@@ -268,9 +268,12 @@ def is_triggered(self):
 
     def cancel(self):
         if self._is_triggered:
-            events = self.clock._events
+            clock = self.clock
+            events = clock._events
             cid = self.cid
             if cid in events and self in events[cid]:
+                if clock._current_event is self:
+                    clock._current_event = None
                 events[cid].remove(self)
         self._is_triggered = False
 
@@ -330,7 +333,7 @@ class ClockBase(_ClockBase):
     __slots__ = ('_dt', '_last_fps_tick', '_last_tick', '_fps', '_rfps',
                  '_start_tick', '_fps_counter', '_rfps_counter', '_events',
                  '_frames', '_frames_displayed',
-                 '_max_fps', 'max_iteration')
+                 '_max_fps', 'max_iteration', '_current_event')
 
     MIN_SLEEP = 0.005
     SLEEP_UNDERSHOOT = MIN_SLEEP - 0.001
@@ -348,6 +351,7 @@ def __init__(self):
         self._frames_displayed = 0
         self._events = {}
         self._max_fps = float(Config.getint('graphics', 'maxfps'))
+        self._current_event = None
 
         #: .. versionadded:: 1.0.5
         #:     When a schedule_once is used with -1, you can add a limit on
@@ -508,12 +512,16 @@ def unschedule(self, callback):
             if cid in events:
                 for event in events[cid][:]:
                     if event is callback:
+                        if self._current_event is event:
+                            self._current_event = None
                         events[cid].remove(event)
         else:
             cid = _hash(callback)
             if cid in events:
                 for event in events[cid][:]:
                     if event.get_callback() == callback:
+                        if self._current_event is event:
+                            self._current_event = None
                         events[cid].remove(event)
 
     def _release_references(self):
@@ -534,10 +542,14 @@ def _process_events(self):
         events = self._events
         for cid in list(events.keys())[:]:
             for event in events[cid][:]:
-                if event.tick(self._last_tick) is False:
-                    # event may be already removed by the callback
-                    if event in events[cid]:
-                        events[cid].remove(event)
+                # event may be already removed from original list
+                if event in events[cid]:
+                    self._current_event = event
+                    if event.tick(self._last_tick) is False:
+                        # if event removed by callback, _current_event is None
+                        if self._current_event is event:
+                            events[cid].remove(event)
+        self._current_event = None
 
     def _process_events_before_frame(self):
         found = True


Test patch: