DJay
DJay

Reputation: 1282

Using kivy clock to wait before execution

I have a list which I have fetched from a server and here is what I want to do

result = [1,7,0,0,2,4....]

def got_json(req, result):
    for i in result:
        if i is odd:
            wait for 5 seconds and call my_function
            now continue
        else:
            call my_function (now)

So basically I'm looking for more like a time.sleep() but using time.sleep just freezes the app, I just want to pause the execution of my for loop in got_json method and not all other stuff which I suppose time.sleep does.

I tried using Clock.schedule_once using this code

class MyWidget(BoxLayout):

    def onbuttonclick(self):
        my_list = range(10)
        for i in my_list:
            if (i%2 == 0) :
                Clock.schedule_once(self.my_callback, 5)
                continue

            print("Whatever")

    def my_callback(self,dt):
        print("called")

The output seems like it is indeed scheduling the function but its not stopping the execution of the for loop, which is what I want Output of above code

Whatever
Whatever
Whatever
Whatever
Whatever
called
called
called
called
called

Output I want to have

Whatever
**5 seconds**

called
Whatever
**5 seconds**

called 

and so on...
How can I use the Clock object to do what I want? Thanks

Upvotes: 2

Views: 3942

Answers (3)

mishaeel
mishaeel

Reputation: 68

An alternative answer that may be easier to understand for beginners like myself (all respect to @Mikhail Gerasimov's answer which is over my head):

In Kivy, the Clock.schedule_once function when inside a loop schedules all the callbacks at nearly the same time. So in the code above if dt (the number of seconds to delay your callback function) is 5, all the callbacks from the loop schedule at nearly the same time in the loop, and then all start at the nearly the same time 5 seconds later.

What worked for me to get around this was to declare dt as 5 seconds before the loop, and then at the end of each iteration of the loop add an additional 5 seconds to dt. This way when all the callbacks are scheduled at the end of the loop, the first callback starts at 5 seconds, the next at 10 seconds, the next at 15, and so on.

Here is the code edited using this method (explanation of the 'partial' addition can be found in the Kivy docs--I'm using Kivy 2.2.0):

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
from functools import partial

class MyWidget(BoxLayout):

    def onbuttonclick(self):
        my_list = range(10)
        dt = 5 #declare dt here
        for i in my_list:
            if i == 0:
                print('whatever')
            if (i%2 == 0) :
                Clock.schedule_once(partial(self.my_callback, self, 'whatever'), dt) 
                # partial added above to allow Clock.schedule_once to accept arguments in addtion to dt
                dt = dt + 5 # add 5 seconds to dt to make the next callback start 5 seconds after the last
                continue

    def my_callback(self, whatever, dt):
        print("called")
        print(whatever)

Upvotes: 0

Mikhail Gerasimov
Mikhail Gerasimov

Reputation: 39546

This is interesting question. Using thread - is most universal solution for tasks like this in general. However, if we talking about this concrete case, you can use generator and it's yield points to return yourself control flow and resume execution later using Clock.schedule_once:

from functools import wraps

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock


def yield_to_sleep(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        gen = func()
        def next_step(*_):
            try:
                t = next(gen)  # this executes 'func' before next yield and returns control to you
            except StopIteration:
                pass
            else:
                Clock.schedule_once(next_step, t)  # having control you can resume func execution after some time
        next_step()
    return wrapper


@yield_to_sleep  # use this decorator to cast 'yield' to non-blocking sleep
def test_function():
    for i in range(10):
        if (i % 2 == 0):
            yield 5  # use yield to "sleep"
            print('Called')
        else:
            print('Whatever')


class TestApp(App):
    def build(self):
        test_function()
        return BoxLayout()


if __name__ == '__main__':
    TestApp().run()

Upd:

def func():
    yield 'STEP 1'
    yield 'STEP 2'


gen = func()
print('Result of calling generator-function is generator object:', gen, '\n')


res1 = next(gen)
print('''Calling next() on generator object executes original 
function to the moment of yield point and freeze it\'s state:\n''', res1, '\n')


print('''Look at line of code that prints this mesage:
It\'s not located inside func(), 
but we were able to call it "in the middle" of executing func():
We see msg "STEP 1", but we don't see "STEP 2" yet.
This is what makes generators so cool: we can execute part of it,
do anything we want and resume execution later.
In "yield_to_sleep" this resuming delegated to Clock.schedule_once
making generator being executed after some time and without freezing anything.''', '\n')


res2 = next(gen)
print('Another next() and we on next yield point inside func():\n', res2, '\n')


try:
    next(gen)
except StopIteration:
    print('''If no yield points left, another next() call will finish func()
and raise StopIteration exception. It\'s not error it\'s just way
to say to outer code that generator is done.''', '\n')

Upvotes: 2

Yohst
Yohst

Reputation: 1902

Let on_json() start a thread with the code you have now in on_json be the body of the thread. Just use time.sleep() in the thread. So your new thread blocks when it needs to but your main app does not.

Upvotes: 1

Related Questions