Michael B. Currie
Michael B. Currie

Reputation: 14678

Download file with Kivy app without locking event loop

Here's a Kivy application that has a button and a progress bar. When the button is pressed, a ZIP file is downloaded from the web and unzipped. The progress bar advances to mark the download's progress.

The problem is, the downloading locks the Kivy event loop, freezing the application during the download. How can I download and unzip a file in the background?

from __future__ import division
import os
import requests
import zipfile
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout

ZIP_URL = 'https://www.python.org/ftp/python/3.5.1/python-3.5.1-embed-win32.zip'
ZIP_FILENAME = 'Python351.zip'

kv_string = """
<RootWidget>
    BoxLayout:
        orientation: "vertical"
        Button:
            id: download_button
            text: "Download content"
            on_press: self.parent.parent.download_content()
        ProgressBar:
            id: download_progress_bar
            max: 1
            value: 0.1
"""

Builder.load_string(kv_string)


class RootWidget(BoxLayout):
    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)

    def download_content(self):
        self.ids["download_button"].disabled = True

        total_size_KiB = 6023182 / 1024 # DEBUG: hardcoded
        print("Downloading file: %s" % ZIP_URL)
        with open(ZIP_FILENAME, 'wb') as handle:
            response = requests.get(ZIP_URL, stream=True)

            if not response.ok:
                print("Something went wrong.")
                return

            current_size_KiB = 0
            for block in response.iter_content(1024):
                percent_downloaded = current_size_KiB / total_size_KiB
                if not current_size_KiB % 100:
                    print("%.2f%% downloaded so far" % (percent_downloaded * 100))
                self.ids['download_progress_bar'].value = percent_downloaded

                handle.write(block)

                current_size_KiB += 1

        self.unzip_content()

    def unzip_content(self):
        print("Unzipping file")
        fh = open(ZIP_FILENAME, 'rb')
        z = zipfile.ZipFile(fh)
        ZIP_EXTRACT_FOLDER = ZIP_FILENAME + '_extracted'
        if not os.path.exists(ZIP_EXTRACT_FOLDER):
            os.makedirs(ZIP_EXTRACT_FOLDER)
        z.extractall(ZIP_EXTRACT_FOLDER)
        fh.close()
        os.remove(ZIP_FILENAME)

        print("Done")


class MyApp(App):

    def build(self):
        return RootWidget()


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

Upvotes: 3

Views: 2161

Answers (1)

Michael B. Currie
Michael B. Currie

Reputation: 14678

First, as @Nykakin suggests, to make the download asynchronous, use kivy.network.urlrequest.UrlRequest:

def download_content(self):
    self.ids["download_button"].disabled = True
    req = UrlRequest(ZIP_URL, on_progress=self.update_progress,
                     chunk_size=1024, on_success=self.unzip_content,
                     file_path=ZIP_FILENAME)

def update_progress(self, request, current_size, total_size):
    self.ids['download_progress_bar'].value = current_size / total_size

Second, as @KeyWeeUsr suggests, change the unzipping method so it doesn't block the event loop, by using the threading module:

def unzip_content(self, req, result):
    threading.Thread(target=self.unzip_thread).start()

def unzip_thread(self):
    # ... (same as unzip_content method in question)

Here's the full new version:

from __future__ import division
import os
import zipfile
import threading
import time
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.network.urlrequest import UrlRequest

ZIP_URL = 'https://www.python.org/ftp/python/3.5.1/python-3.5.1-embed-win32.zip'
ZIP_FILENAME = 'Python351.zip'

kv_string = """
<RootWidget>
    BoxLayout:
        orientation: "vertical"
        Button:
            id: download_button
            text: "Download content"
            on_press: self.parent.parent.download_content()
        ProgressBar:
            id: download_progress_bar
            max: 1
            value: 0
"""

Builder.load_string(kv_string)


class RootWidget(BoxLayout):

    stop = threading.Event()    

    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)

    def download_content(self):
        self.ids["download_button"].disabled = True
        req = UrlRequest(ZIP_URL, on_progress=self.update_progress,
                         chunk_size=1024, on_success=self.unzip_content,
                         file_path=ZIP_FILENAME)

    def update_progress(self, request, current_size, total_size):
        self.ids['download_progress_bar'].value = current_size / total_size

    def unzip_content(self, req, result):
        threading.Thread(target=self.unzip_thread).start()

    def unzip_thread(self):
        print("Unzipping file")
        fh = open(ZIP_FILENAME, 'rb')
        z = zipfile.ZipFile(fh)
        ZIP_EXTRACT_FOLDER = ZIP_FILENAME + '_extracted'
        if not os.path.exists(ZIP_EXTRACT_FOLDER):
            os.makedirs(ZIP_EXTRACT_FOLDER)
        z.extractall(ZIP_EXTRACT_FOLDER)
        fh.close()
        os.remove(ZIP_FILENAME)
        time.sleep(4) # DEBUG: stretch out the unzip method to test threading

        print("Done")


class MyApp(App):

    def on_stop(self):
        # The Kivy event loop is about to stop, set a stop signal;
        # otherwise the app window will close, but the Python process will
        # keep running until all secondary threads exit.
        self.root.stop.set()        

    def build(self):
        return RootWidget()


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

Upvotes: 5

Related Questions