When the download button is pressed it is supposed to download a set of files from a specified url. After each download I update the progress bar accordingly, but the bar only updates after all the files have been downloaded. The UI is being blocked until all downloads are complete. I am using Beeware's Toga for the UI. I have tried using async and await but the download function is synchronous so it doesn't work. Below is the code:
"""
Downloader
"""
import pathlib, os
import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW, LEFT, CENTER, TOP
from downloader.my_libs.downloader import downloader
WHITE = '#ffffff'
PRIMARY_COLOR = '#000000'
SECONDARY_COLOR = '#fbfbfb'
ACCENT_COLOR = '#80ff80'
BACKGROUND_COLOR = '#ffffff'
class Downloader(toga.App):
def __init__(self, formal_name=None, app_id=None, app_name=None, id=None, icon=None, author=None, version=None, home_page=None, description=None, startup=None, windows=None, on_exit=None, factory=None):
super().__init__(formal_name, app_id, app_name, id, icon, author, version, home_page, description, startup, windows, on_exit, factory)
self.resource_folder = pathlib.Path(__file__).joinpath('../resources').resolve()
self.url_filename = self.resource_folder.joinpath('url.txt')
self.output_path = pathlib.Path(__file__).joinpath('../../../../files').resolve()
self.downloader = Downloader(url='', output_path=self.output_path)
self.number_of_items_downloaded = 0
self.number_of_items_to_download = 0
def startup(self):
main_box = toga.Box(style=Pack(background_color=BACKGROUND_COLOR))
box = toga.Box(style=Pack(background_color=BACKGROUND_COLOR))
url_box = toga.Box()
title_label = toga.Label(text='Downloader', style=Pack(text_align=CENTER, font_size=20, flex=1, background_color=BACKGROUND_COLOR, color=PRIMARY_COLOR))
self.url_input = toga.TextInput(style=Pack(flex=4, font_size=12, background_color=SECONDARY_COLOR, color=PRIMARY_COLOR))
self.download_button = toga.Button('Download', on_press=self.on_download, style=Pack(flex=1, font_size=12, padding_top=5, background_color=ACCENT_COLOR, color=PRIMARY_COLOR))
self.info_box = toga.MultilineTextInput(readonly=True, style=Pack(flex=1, padding_top=10, font_size=12, background_color=SECONDARY_COLOR, color=PRIMARY_COLOR))
self.progress_bar = toga.ProgressBar(max=100, value=0, style=Pack(padding_top=10, background_color=SECONDARY_COLOR, color=ACCENT_COLOR))
box.add(title_label)
url_box.add(self.url_input)
box.add(url_box)
box.add(self.download_button)
box.add(self.info_box)
box.add(self.progress_bar)
main_box.add(box)
box.style.update(direction=COLUMN, padding=50, flex=1)
url_box.style.update(direction=ROW, flex=1, padding_top=5)
self.main_window = toga.MainWindow(title=self.formal_name)
self.main_window.content = main_box
self.main_window.show()
def on_download(self, widget):
self.progress_bar.start()
self.download_files()
self.progress_bar.stop()
def download_files(self):
self.number_of_items_to_download = len(self.downloader.urls)
for url in self.downloader.urls:
try:
self.download_file(url)
self.update_progress()
except Exception as exception:
print('Could not download from:', url)
print(exception, '\n\n')
self.info_box.value = self.info_box.value + f'\nCould not download from: {url}' + f'exception\n\n'
def update_progress(self):
self.progress_bar.value = (self.number_of_items_downloaded / self.number_of_items_to_download) * 100
def download_file(self, url):
title, filepath = self.downloader.download(url)
self.info_box.value = self.info_box.value + f'Downloaded {title} \nTo {filepath}\n\n'
self.number_of_items_downloaded += 1
def main():
return Downloader()
The main principles to remember in most UI frameworks are:
Here's a solution that follows both of those rules. I haven't tested it, but it should be pretty close:
def on_download(self, widget):
# As of Toga 0.4, self.loop is pre-set by Toga itself,
# so the next line should be removed.
self.loop = asyncio.get_event_loop()
threading.Thread(target=self.download_files).start()
def download_files(self):
self.number_of_items_to_download = len(self.downloader.urls)
for url in self.downloader.urls:
try:
self.download_file(url)
except Exception as exception:
print('Could not download from:', url)
print(exception, '\n\n')
# TODO: use call_soon_threadsafe to update the UI
# self.info_box.value = self.info_box.value + f'\nCould not download from: {url}' + f'exception\n\n'
def update_progress(self, title, filepath):
self.info_box.value = self.info_box.value + f'Downloaded {title} \nTo {filepath}\n\n'
self.progress_bar.value = (self.number_of_items_downloaded / self.number_of_items_to_download) * 100
def download_file(self, url):
title, filepath = self.downloader.download(url)
self.number_of_items_downloaded += 1
self.loop.call_soon_threadsafe(self.update_progress, title, filepath)
There's no need to join
the thread. If you want to update the UI when the download is complete, just do that with another call to call_soon_threadsafe
.
Even better, you could use a higher-level API like run_in_executor
:
# Must add `async`!
async def on_download(self, widget):
loop = asyncio.get_event_loop()
self.number_of_items_to_download = len(self.downloader.urls)
for url in self.downloader.urls:
try:
title, filepath = await loop.run_in_executor(
None, self.downloader.download, url
)
self.info_box.value = self.info_box.value + f'Downloaded {title} \nTo {filepath}\n\n'
self.number_of_items_downloaded += 1
self.progress_bar.value = (self.number_of_items_downloaded / self.number_of_items_to_download) * 100
except Exception as exception:
print('Could not download from:', url)
print(exception, '\n\n')
self.info_box.value = self.info_box.value + f'\nCould not download from: {url}' + f'exception\n\n'