0

I working on software, that includes parts working with the WHO ICD11 API. When I run the code:

import json
import re
import threading
import time
from typing import Dict, List

import qdarkgraystyle as qdarkgraystyle
import requests
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtWidgets import QTreeView
from threading import Lock
from gui import Ui_MainWindow
import auth

printlock = Lock()
p = print


def print(*a, **b):
    with printlock:
        p(*a, **b)


class g:
    max = 1
    progress = 0
    end_workers = False
    loaded_dict = None


class ApplicationWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(ApplicationWindow, self).__init__()
        
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)


def linearization_url():
    return f"https://id.who.int/icd/release/{ui.dropRevision.currentText().split('/')[0]}/{ui.dropRelease.currentText()}{('/' + ui.dropRevision.currentText().split('/')[1]) if len(ui.dropRevision.currentText().split('/')) > 1 else ''}"


def download():
    loader = Loader(linearization_url())
    loader.statusUpdateHook.connect(updatehook)
    loader.statusFinished.connect(finishedLoader)
    loader.start()


def updatehook():
    ui.progress.setTextVisible(True)
    ui.progress.setMaximum(gl.max)
    ui.progress.setValue(gl.progress)


def finishedLoader():
    json.dump(gl.loaded_dict, open("dict.json"), indent=4)


def split_link(url: str) -> Dict[str, str]:
    return re.search(
            "https?://id.who.int/icd/release/(?P<revision>[0-9]{2})/(?P<release>[^/]*)(/(?P<linearization>.*))?",
            url).groupdict()


def worker(loader):
    print("Worker booting...")
    _token = gl.token
    over = True
    while not gl.end_workers:
        url = ""
        with loader.index_lock:
            try:
                url = loader.working_list[loader.index]
                loader.index += 1
            except IndexError:
                over = False
        if over:
            json = request_json(url, _token)
            with loader.finished_count_lock:
                loader.working_dict[url] = json
                if "child" in json:
                    for child in json["child"]:
                        loader.working_list.append(child)
                loader.finished_count += 1
        else:
            over = True


def loadReleases():
    token = getToken(auth.id, auth.secret)
    ui.dropRelease.clear()
    ui.dropRelease.repaint()
    for release in request_json("https://id.who.int/icd/release/" + ui.dropRevision.currentText(), token)[
        "release"]:
        ui.dropRelease.addItem(split_link(release)["release"])


def getToken(clientID, clientSecret) -> str:
    return requests.post('https://icdaccessmanagement.who.int/connect/token',
                         data={'client_id':  clientID, 'client_secret': clientSecret, 'scope': 'icdapi_access',
                               'grant_type': 'client_credentials'}).json()['access_token']


def request_json(link_: str, token_: str):
    headers_ = {
        'Authorization':   'Bearer ' + token_,
        'Accept':          'application/json',
        'Accept-Language': 'en',
        'API-Version':     'v2'
    }
    return requests.get(link_, headers=headers_).json()


class Loader(QtCore.QThread):
    statusFinished = QtCore.pyqtSignal()
    statusUpdateHook = QtCore.pyqtSignal()
    index = 0
    finished_count = 0
    working_list = []
    working_dict = {}
    index_lock = Lock()
    finished_count_lock = Lock()
    workers = []
    
    def __init__(self, lurl: str):
        super().__init__()
        self.working_list.append(lurl)
    
    def progressUpdate(self):
        gl.max = len(self.working_list)
        gl.progress = self.finished_count
        self.statusUpdateHook.emit()
    
    def run(self):
        for i in range(0, 20):
            self.workers.append(threading.Thread(target=worker, args=(self,)))
            self.workers[i].start()
        while self.finished_count < len(self.working_list):
            with self.index_lock:
                with self.finished_count_lock:
                    self.progressUpdate()
            time.sleep(5)
        for work in self.workers:
            if work.isAlive():
                gl.end_workers = True
        gl.loaded_dict = self.working_dict
        self.statusFinished.emit()


if __name__ == "__main__":
    import sys
    
    gl = g()
    gl.token = getToken(auth.id, auth.secret)
    tabs: List[QTreeView] = []
    app = QtWidgets.QApplication(sys.argv)
    application = ApplicationWindow()
    application.setStyleSheet(qdarkgraystyle.load_stylesheet())
    ui = application.ui
    ui.buttonDownload.clicked.connect(download)
    ui.dropRevision.addItems(["10", "11/mms"])
    ui.dropRevision.currentIndexChanged.connect(loadReleases)
    loadReleases()
    application.show()
    sys.exit(app.exec_())

in Pycharms debug mode, it does, what I want it to. It works fine as long as it is in debug mode, while when in normal mode, when the buttonDownload.clicked event is triggered, the whole program crashes with the only output being:

QThread: Destroyed while thread is still running

Has anyone any idea on how to fix that?

(For reproducing purposes: You need API keys to access the API. They are imported from auth as auth.id and auth.secret. ID and secret can be obtained from an account over the WHO ICD11 API site)

bad_coder
  • 11,289
  • 20
  • 44
  • 72
CodeSpoof
  • 55
  • 4

1 Answers1

2

Loader inherits QThread, in download function QThread object is bound to local variable loader, on exiting function this variable got garbage collected and bound object gets destroyed. You need to make sure loader variable outlive function, for example make it global variable or return it from function and store somewhere.

mugiseyebrows
  • 4,138
  • 1
  • 14
  • 15