Reputation: 14678
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
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