How to correctly redirect stdout, logging and tqdm into a PyQt widget
首先,我知道很多问题都与此相似。
但是花了很多时间之后,我现在寻求社区的帮助。
我开发并使用了一堆依赖
我希望它们可以在Jupyter内部,控制台或GUI中使用。
在Jupyter或控制台中一切正常:在日志记录/打印和tqdm进度条之间没有冲突。这是显示控制台/ Jupyter行为的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | # coding=utf-8 from tqdm.auto import tqdm import time import logging import sys import datetime __is_setup_done = False def setup_logging(log_prefix): global __is_setup_done if __is_setup_done: pass else: __log_file_name ="{}-{}_log_file.txt".format(log_prefix, datetime.datetime.utcnow().isoformat().replace(":","-")) __log_format = '%(asctime)s - %(name)-30s - %(levelname)s - %(message)s' __console_date_format = '%Y-%m-%d %H:%M:%S' __file_date_format = '%Y-%m-%d %H-%M-%S' root = logging.getLogger() root.setLevel(logging.DEBUG) console_formatter = logging.Formatter(__log_format, __console_date_format) file_formatter = logging.Formatter(__log_format, __file_date_format) file_handler = logging.FileHandler(__log_file_name, mode='a', delay=True) # file_handler = TqdmLoggingHandler2(__log_file_name, mode='a', delay=True) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) root.addHandler(file_handler) tqdm_handler = TqdmLoggingHandler() tqdm_handler.setLevel(logging.DEBUG) tqdm_handler.setFormatter(console_formatter) root.addHandler(tqdm_handler) __is_setup_done = True class TqdmLoggingHandler(logging.StreamHandler): def __init__(self, level=logging.NOTSET): logging.StreamHandler.__init__(self) def emit(self, record): msg = self.format(record) tqdm.write(msg) # from https://stackoverflow.com/questions/38543506/change-logging-print-function-to-tqdm-write-so-logging-doesnt-interfere-wit/38739634#38739634 self.flush() def example_long_procedure(): setup_logging('long_procedure') __logger = logging.getLogger('long_procedure') __logger.setLevel(logging.DEBUG) for i in tqdm(range(10), unit_scale=True, dynamic_ncols=True, file=sys.stdout): time.sleep(.1) __logger.info('foo {}'.format(i)) example_long_procedure() |
获得的输出:
1 2 3 4 5 6 7 8 9 10 11 | 2019-03-07 22:22:27 - long_procedure - INFO - foo 0 2019-03-07 22:22:27 - long_procedure - INFO - foo 1 2019-03-07 22:22:27 - long_procedure - INFO - foo 2 2019-03-07 22:22:27 - long_procedure - INFO - foo 3 2019-03-07 22:22:27 - long_procedure - INFO - foo 4 2019-03-07 22:22:28 - long_procedure - INFO - foo 5 2019-03-07 22:22:28 - long_procedure - INFO - foo 6 2019-03-07 22:22:28 - long_procedure - INFO - foo 7 2019-03-07 22:22:28 - long_procedure - INFO - foo 8 2019-03-07 22:22:28 - long_procedure - INFO - foo 9 100%||||||||||||||||||||||||||||||||| 10.0/10.0 [00:01<00:00, 9.69it/s] |
现在,我正在使用PyQt制作一个GUI,该GUI使用与上面类似的代码。由于处理过程可能很长,因此我使用了线程处理以避免在处理过程中冻结HMI。我还对Qt QWidget使用Queue()进行
我当前的用例是1个单线程,该线程具有日志和tqdm进度条以重定向到1个专用小部件。 (我不是在寻找多个线程来为窗口小部件提供多个日志和多个tqdm进度条)。
由于从辅助线程将stdout和stderr重定向到PyQt5 QTextEdit,我设法重定向了stdout。
但是,仅记录器行被重定向。 TQDM进度栏仍指向控制台输出。
这是我当前的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | # coding=utf-8 import time import logging import sys import datetime __is_setup_done = False from queue import Queue from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QThread, QMetaObject, Q_ARG, Qt from PyQt5.QtGui import QTextCursor, QFont from PyQt5.QtWidgets import QTextEdit, QPlainTextEdit, QWidget, QToolButton, QVBoxLayout, QApplication from tqdm.auto import tqdm class MainApp(QWidget): def __init__(self): super().__init__() setup_logging(self.__class__.__name__) self.__logger = logging.getLogger(self.__class__.__name__) self.__logger.setLevel(logging.DEBUG) # create console text queue self.queue_console_text = Queue() # redirect stdout to the queue output_stream = WriteStream(self.queue_console_text) sys.stdout = output_stream layout = QVBoxLayout() self.setMinimumWidth(500) # GO button self.btn_perform_actions = QToolButton(self) self.btn_perform_actions.setText('Launch long processing') self.btn_perform_actions.clicked.connect(self._btn_go_clicked) self.console_text_edit = ConsoleTextEdit(self) self.thread_initialize = QThread() self.init_procedure_object = InitializationProcedures(self) # create console text read thread + receiver object self.thread_queue_listener = QThread() self.console_text_receiver = ThreadConsoleTextQueueReceiver(self.queue_console_text) # connect receiver object to widget for text update self.console_text_receiver.queue_element_received_signal.connect(self.console_text_edit.append_text) # attach console text receiver to console text thread self.console_text_receiver.moveToThread(self.thread_queue_listener) # attach to start / stop methods self.thread_queue_listener.started.connect(self.console_text_receiver.run) self.thread_queue_listener.finished.connect(self.console_text_receiver.finished) self.thread_queue_listener.start() layout.addWidget(self.btn_perform_actions) layout.addWidget(self.console_text_edit) self.setLayout(layout) self.show() @pyqtSlot() def _btn_go_clicked(self): # prepare thread for long operation self.init_procedure_object.moveToThread(self.thread_initialize) self.thread_initialize.started.connect(self.init_procedure_object.run) self.thread_initialize.finished.connect(self.init_procedure_object.finished) # start thread self.btn_perform_actions.setEnabled(False) self.thread_initialize.start() class WriteStream(object): def __init__(self, q: Queue): self.queue = q def write(self, text): """ Redirection of stream to the given queue """ self.queue.put(text) def flush(self): """ Stream flush implementation """ pass class ThreadConsoleTextQueueReceiver(QObject): queue_element_received_signal = pyqtSignal(str) def __init__(self, q: Queue, *args, **kwargs): QObject.__init__(self, *args, **kwargs) self.queue = q @pyqtSlot() def run(self): self.queue_element_received_signal.emit('---> Console text queue reception Started <--- ') while True: text = self.queue.get() self.queue_element_received_signal.emit(text) @pyqtSlot() def finished(self): self.queue_element_received_signal.emit('---> Console text queue reception Stopped <--- ') class ConsoleTextEdit(QTextEdit):#QTextEdit): def __init__(self, parent): super(ConsoleTextEdit, self).__init__() self.setParent(parent) self.setReadOnly(True) self.setLineWidth(50) self.setMinimumWidth(1200) self.setFont(QFont('Consolas', 11)) self.flag = False @pyqtSlot(str) def append_text(self, text: str): self.moveCursor(QTextCursor.End) self.insertPlainText(text) def long_procedure(): setup_logging('long_procedure') __logger = logging.getLogger('long_procedure') __logger.setLevel(logging.DEBUG) for i in tqdm(range(10), unit_scale=True, dynamic_ncols=True): time.sleep(.1) __logger.info('foo {}'.format(i)) class InitializationProcedures(QObject): def __init__(self, main_app: MainApp): super(InitializationProcedures, self).__init__() self._main_app = main_app @pyqtSlot() def run(self): long_procedure() @pyqtSlot() def finished(self): print("Thread finished !") # might call main window to do some stuff with buttons self._main_app.btn_perform_actions.setEnabled(True) def setup_logging(log_prefix): global __is_setup_done if __is_setup_done: pass else: __log_file_name ="{}-{}_log_file.txt".format(log_prefix, datetime.datetime.utcnow().isoformat().replace(":","-")) __log_format = '%(asctime)s - %(name)-30s - %(levelname)s - %(message)s' __console_date_format = '%Y-%m-%d %H:%M:%S' __file_date_format = '%Y-%m-%d %H-%M-%S' root = logging.getLogger() root.setLevel(logging.DEBUG) console_formatter = logging.Formatter(__log_format, __console_date_format) file_formatter = logging.Formatter(__log_format, __file_date_format) file_handler = logging.FileHandler(__log_file_name, mode='a', delay=True) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) root.addHandler(file_handler) tqdm_handler = TqdmLoggingHandler() tqdm_handler.setLevel(logging.DEBUG) tqdm_handler.setFormatter(console_formatter) root.addHandler(tqdm_handler) __is_setup_done = True class TqdmLoggingHandler(logging.StreamHandler): def __init__(self, level=logging.NOTSET): logging.StreamHandler.__init__(self) def emit(self, record): msg = self.format(record) tqdm.write(msg) # from https://stackoverflow.com/questions/38543506/change-logging-print-function-to-tqdm-write-so-logging-doesnt-interfere-wit/38739634#38739634 self.flush() if __name__ == '__main__': app = QApplication(sys.argv) app.setStyle('Fusion') tqdm.ncols = 50 ex = MainApp() sys.exit(app.exec_()) |
给出:
我想获得严格要求在控制台中调用代码的确切行为。
即PyQt小部件中的预期输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 | ---> Console text queue reception Started <--- 2019-03-07 19:42:19 - long_procedure - INFO - foo 0 2019-03-07 19:42:19 - long_procedure - INFO - foo 1 2019-03-07 19:42:19 - long_procedure - INFO - foo 2 2019-03-07 19:42:19 - long_procedure - INFO - foo 3 2019-03-07 19:42:19 - long_procedure - INFO - foo 4 2019-03-07 19:42:19 - long_procedure - INFO - foo 5 2019-03-07 19:42:20 - long_procedure - INFO - foo 6 2019-03-07 19:42:20 - long_procedure - INFO - foo 7 2019-03-07 19:42:20 - long_procedure - INFO - foo 8 2019-03-07 19:42:20 - long_procedure - INFO - foo 9 100%|################################| 10.0/10.0 [00:01<00:00, 9.16it/s] |
我尝试/探索的事情没有成功。
选项1
此解决方案在QPlainTextEdit中使用tqdm的Display终端输出无法给出预期的结果。它可以很好地重定向仅包含tqdm内容的输出。
以下代码未提供预期的行为,无论是QTextEdit还是QPlainTextEdit。仅记录器行被重定向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # code from this answer # https://stackoverflow.com/questions/53381975/display-terminal-output-with-tqdm-in-qplaintextedit @pyqtSlot(str) def append_text(self, message: str): if not hasattr(self,"flag"): self.flag = False message = message.replace(' ', '').rstrip() if message: method ="replace_last_line" if self.flag else"append_text" QMetaObject.invokeMethod(self, method, Qt.QueuedConnection, Q_ARG(str, message)) self.flag = True else: self.flag = False @pyqtSlot(str) def replace_last_line(self, text): cursor = self.textCursor() cursor.movePosition(QTextCursor.End) cursor.select(QTextCursor.BlockUnderCursor) cursor.removeSelectedText() cursor.insertBlock() self.setTextCursor(cursor) self.insertPlainText(text) |
但是,以上代码+将
最后,更改所有tqdm调用使用的模块不应是首选。
因此,我发现的另一种方法是将标准流重定向到同一流/队列中的标准错误。由于默认情况下tqdm写入stderr,因此所有tqdm输出都重定向到小部件。
但是我仍然不知道能获得我想要的确切输出。
这个问题没有提供线索说明为什么QTextEdit与QPlainTextEdit之间的行为似乎有所不同
选项2
这个问题在QTextEdit小部件中重复stdout,stderr看起来与QPlainTextEdit中使用tqdm的Display终端输出非常相似,并且不能回答我上面描述的确切问题。
选项3
由于未定义flush()方法,因此使用contextlib尝试此解决方案给我一个错误。修复后,我最终仅使用tqdm行,而没有记录器行。
选项4
我还尝试拦截 r字符并实现特定行为,但未成功。
版本:
1 2 3 4 5 | tqdm 4.28.1 pyqt 5.9.2 PyQt5 5.12 PyQt5_sip 4.19.14 Python 3.7.2 |
EDIT 2019-mar-12:在我看来答案是:可能可以做到,但是需要很多努力才能记住QTextEdit的行为来自哪一行。另外,由于tdm默认情况下会写入stderr,因此最终您也将捕获所有异常跟踪。
这就是为什么我将自己的答案标记为已解决的原因:我发现达到相同的目的更为优雅:在pyqt中显示正在发生的事情。
这是获得接近预期行为的最佳方法。
它没有完全回答问题,因为我更改了GUI设计。
所以我不会投票解决。此外,这全部在一个python文件中完成。我计划进一步挑战该解决方案,以查看它是否与执行tqdm导入的真正python模块一起使用。
我以非常难看的方式修补了基本的tqdm类。主要技巧是:
-
通过将原始tqdm类存储为新名称来动态更改tqdm模块结构:
tqdm.orignal_class = tqdm.tqdm -
然后继承tqdm.original类
class TQDMPatch(tqdm.orignal_class): -
实现构造函数,以便将文件流+任何参数强制设置为所需的任意值:
super(TQDMPatch, self).__init__(... change some params ...) 。我给我的TQDM类一个自定义的WriteStream() ,它写入了Queue() -
更改GUI策略以拦截并将您的自定义tqdm流重定向到单独的Qt小部件。我的小部件假设所有接收到的打印都包含
(TQDM似乎正在这样做)。
它既可以在单个python文件中使用,也可以在多个单独的模块中使用。在后一种情况下,启动时的导入顺序至关重要。
屏幕截图:
在启动处理之前
处理期间
处理结束
这是代码
多合一档案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | # coding=utf-8 import datetime import logging import sys import time from queue import Queue from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QThread, Qt from PyQt5.QtGui import QTextCursor, QFont from PyQt5.QtWidgets import QTextEdit, QWidget, QToolButton, QVBoxLayout, QApplication, QLineEdit # DEFINITION NEEDED FIRST ... class WriteStream(object): def __init__(self, q: Queue): self.queue = q def write(self, text): self.queue.put(text) def flush(self): pass # prepare queue and streams queue_tqdm = Queue() write_stream_tqdm = WriteStream(queue_tqdm) ################## START TQDM patch procedure ################## import tqdm # save original class into module tqdm.orignal_class = tqdm.tqdm class TQDMPatch(tqdm.orignal_class): """ Derive from original class """ def __init__(self, iterable=None, desc=None, total=None, leave=True, file=None, ncols=None, mininterval=0.1, maxinterval=10.0, miniters=None, ascii=None, disable=False, unit='it', unit_scale=False, dynamic_ncols=False, smoothing=0.3, bar_format=None, initial=0, position=None, postfix=None, unit_divisor=1000, gui=False, **kwargs): super(TQDMPatch, self).__init__(iterable, desc, total, leave, write_stream_tqdm, # change any chosen file stream with our's 80, # change nb of columns (gui choice), mininterval, maxinterval, miniters, ascii, disable, unit, unit_scale, False, smoothing, bar_format, initial, position, postfix, unit_divisor, gui, **kwargs) print('TQDM Patch called') # check it works @classmethod def write(cls, s, file=None, end=" ", nolock=False): super(TQDMPatch, cls).write(s=s, file=file, end=end, nolock=nolock) # all other tqdm.orignal_class @classmethod methods may need to be redefined ! # I mainly used tqdm.auto in my modules, so use that for patch # unsure if this will work with all possible tqdm import methods # might not work for tqdm_gui ! import tqdm.auto as AUTO # change original class with the patched one, the original still exists AUTO.tqdm = TQDMPatch ################## END of TQDM patch ################## # normal MCVE code __is_setup_done = False class MainApp(QWidget): def __init__(self): super().__init__() setup_logging(self.__class__.__name__) self.__logger = logging.getLogger(self.__class__.__name__) self.__logger.setLevel(logging.DEBUG) # create stdout text queue self.queue_std_out = Queue() sys.stdout = WriteStream(self.queue_std_out) layout = QVBoxLayout() self.setMinimumWidth(500) self.btn_perform_actions = QToolButton(self) self.btn_perform_actions.setText('Launch long processing') self.btn_perform_actions.clicked.connect(self._btn_go_clicked) self.text_edit_std_out = StdOutTextEdit(self) self.text_edit_tqdm = StdTQDMTextEdit(self) self.thread_initialize = QThread() self.init_procedure_object = InitializationProcedures(self) # std out stream management # create console text read thread + receiver object self.thread_std_out_queue_listener = QThread() self.std_out_text_receiver = ThreadStdOutStreamTextQueueReceiver(self.queue_std_out) # connect receiver object to widget for text update self.std_out_text_receiver.queue_std_out_element_received_signal.connect(self.text_edit_std_out.append_text) # attach console text receiver to console text thread self.std_out_text_receiver.moveToThread(self.thread_std_out_queue_listener) # attach to start / stop methods self.thread_std_out_queue_listener.started.connect(self.std_out_text_receiver.run) self.thread_std_out_queue_listener.start() # NEW: TQDM stream management self.thread_tqdm_queue_listener = QThread() self.tqdm_text_receiver = ThreadTQDMStreamTextQueueReceiver(queue_tqdm) # connect receiver object to widget for text update self.tqdm_text_receiver.queue_tqdm_element_received_signal.connect(self.text_edit_tqdm.set_tqdm_text) # attach console text receiver to console text thread self.tqdm_text_receiver.moveToThread(self.thread_tqdm_queue_listener) # attach to start / stop methods self.thread_tqdm_queue_listener.started.connect(self.tqdm_text_receiver.run) self.thread_tqdm_queue_listener.start() layout.addWidget(self.btn_perform_actions) layout.addWidget(self.text_edit_std_out) layout.addWidget(self.text_edit_tqdm) self.setLayout(layout) self.show() @pyqtSlot() def _btn_go_clicked(self): # prepare thread for long operation self.init_procedure_object.moveToThread(self.thread_initialize) self.thread_initialize.started.connect(self.init_procedure_object.run) self.thread_initialize.finished.connect(self.init_procedure_object.finished) # start thread self.btn_perform_actions.setEnabled(False) self.thread_initialize.start() class ThreadStdOutStreamTextQueueReceiver(QObject): queue_std_out_element_received_signal = pyqtSignal(str) def __init__(self, q: Queue, *args, **kwargs): QObject.__init__(self, *args, **kwargs) self.queue = q @pyqtSlot() def run(self): self.queue_std_out_element_received_signal.emit('---> STD OUT Queue reception Started <--- ') while True: text = self.queue.get() self.queue_std_out_element_received_signal.emit(text) # NEW: dedicated receiving object for TQDM class ThreadTQDMStreamTextQueueReceiver(QObject): queue_tqdm_element_received_signal = pyqtSignal(str) def __init__(self, q: Queue, *args, **kwargs): QObject.__init__(self, *args, **kwargs) self.queue = q @pyqtSlot() def run(self): self.queue_tqdm_element_received_signal.emit(' ---> TQDM Queue reception Started <--- ') while True: text = self.queue.get() self.queue_tqdm_element_received_signal.emit(text) class StdOutTextEdit(QTextEdit): # QTextEdit): def __init__(self, parent): super(StdOutTextEdit, self).__init__() self.setParent(parent) self.setReadOnly(True) self.setLineWidth(50) self.setMinimumWidth(500) self.setFont(QFont('Consolas', 11)) @pyqtSlot(str) def append_text(self, text: str): self.moveCursor(QTextCursor.End) self.insertPlainText(text) class StdTQDMTextEdit(QLineEdit): def __init__(self, parent): super(StdTQDMTextEdit, self).__init__() self.setParent(parent) self.setReadOnly(True) self.setEnabled(True) self.setMinimumWidth(500) self.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.setClearButtonEnabled(True) self.setFont(QFont('Consolas', 11)) @pyqtSlot(str) def set_tqdm_text(self, text: str): new_text = text if new_text.find(' ') >= 0: new_text = new_text.replace(' ', '').rstrip() if new_text: self.setText(new_text) else: # we suppose that all TQDM prints have # so drop the rest pass def long_procedure(): # emulate import of modules from tqdm.auto import tqdm setup_logging('long_procedure') __logger = logging.getLogger('long_procedure') __logger.setLevel(logging.DEBUG) tqdm_obect = tqdm(range(10), unit_scale=True, dynamic_ncols=True) tqdm_obect.set_description("My progress bar description") for i in tqdm_obect: time.sleep(.1) __logger.info('foo {}'.format(i)) class InitializationProcedures(QObject): def __init__(self, main_app: MainApp): super(InitializationProcedures, self).__init__() self._main_app = main_app @pyqtSlot() def run(self): long_procedure() @pyqtSlot() def finished(self): print("Thread finished !") # might call main window to do some stuff with buttons self._main_app.btn_perform_actions.setEnabled(True) def setup_logging(log_prefix): global __is_setup_done if __is_setup_done: pass else: __log_file_name ="{}-{}_log_file.txt".format(log_prefix, datetime.datetime.utcnow().isoformat().replace(":","-")) __log_format = '%(asctime)s - %(name)-30s - %(levelname)s - %(message)s' __console_date_format = '%Y-%m-%d %H:%M:%S' __file_date_format = '%Y-%m-%d %H-%M-%S' root = logging.getLogger() root.setLevel(logging.DEBUG) console_formatter = logging.Formatter(__log_format, __console_date_format) file_formatter = logging.Formatter(__log_format, __file_date_format) file_handler = logging.FileHandler(__log_file_name, mode='a', delay=True) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) root.addHandler(file_handler) tqdm_handler = TqdmLoggingHandler() tqdm_handler.setLevel(logging.DEBUG) tqdm_handler.setFormatter(console_formatter) root.addHandler(tqdm_handler) __is_setup_done = True class TqdmLoggingHandler(logging.StreamHandler): def __init__(self): logging.StreamHandler.__init__(self) def emit(self, record): msg = self.format(record) tqdm.tqdm.write(msg) # from https://stackoverflow.com/questions/38543506/change-logging-print-function-to-tqdm-write-so-logging-doesnt-interfere-wit/38739634#38739634 self.flush() if __name__ == '__main__': app = QApplication(sys.argv) app.setStyle('Fusion') ex = MainApp() sys.exit(app.exec_()) |
带有适当的隔离模块
相同的解决方案,但实际的文件分开。
-
MyPyQtGUI.py ,程序入口点 -
output_redirection_tools.py 在执行流程期间应该完成的第一个导入。承载所有魔力。 -
config.py ,一个托管配置元素的配置模块 -
my_logging.py ,自定义日志记录配置 -
third_party_module_not_to_change.py ,我使用但不想更改的某些代码的示例版本。
MyPyQtGUI.py
重要的是要注意,项目的第一个导入应该是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | # looks like an unused import, but it actually does the TQDM class trick to intercept prints import output_redirection_tools # KEEP ME !!! import logging import sys from PyQt5.QtCore import pyqtSlot, QObject, QThread, Qt from PyQt5.QtGui import QTextCursor, QFont from PyQt5.QtWidgets import QTextEdit, QWidget, QToolButton, QVBoxLayout, QApplication, QLineEdit from config import config_dict, STDOUT_WRITE_STREAM_CONFIG, TQDM_WRITE_STREAM_CONFIG, STREAM_CONFIG_KEY_QUEUE, \ STREAM_CONFIG_KEY_QT_QUEUE_RECEIVER from my_logging import setup_logging import third_party_module_not_to_change class MainApp(QWidget): def __init__(self): super().__init__() setup_logging(self.__class__.__name__) self.__logger = logging.getLogger(self.__class__.__name__) self.__logger.setLevel(logging.DEBUG) self.queue_std_out = config_dict[STDOUT_WRITE_STREAM_CONFIG][STREAM_CONFIG_KEY_QUEUE] self.queue_tqdm = config_dict[TQDM_WRITE_STREAM_CONFIG][STREAM_CONFIG_KEY_QUEUE] layout = QVBoxLayout() self.setMinimumWidth(500) self.btn_perform_actions = QToolButton(self) self.btn_perform_actions.setText('Launch long processing') self.btn_perform_actions.clicked.connect(self._btn_go_clicked) self.text_edit_std_out = StdOutTextEdit(self) self.text_edit_tqdm = StdTQDMTextEdit(self) self.thread_initialize = QThread() self.init_procedure_object = LongProcedureWrapper(self) # std out stream management # create console text read thread + receiver object self.thread_std_out_queue_listener = QThread() self.std_out_text_receiver = config_dict[STDOUT_WRITE_STREAM_CONFIG][STREAM_CONFIG_KEY_QT_QUEUE_RECEIVER] # connect receiver object to widget for text update self.std_out_text_receiver.queue_std_out_element_received_signal.connect(self.text_edit_std_out.append_text) # attach console text receiver to console text thread self.std_out_text_receiver.moveToThread(self.thread_std_out_queue_listener) # attach to start / stop methods self.thread_std_out_queue_listener.started.connect(self.std_out_text_receiver.run) self.thread_std_out_queue_listener.start() # NEW: TQDM stream management self.thread_tqdm_queue_listener = QThread() self.tqdm_text_receiver = config_dict[TQDM_WRITE_STREAM_CONFIG][STREAM_CONFIG_KEY_QT_QUEUE_RECEIVER] # connect receiver object to widget for text update self.tqdm_text_receiver.queue_tqdm_element_received_signal.connect(self.text_edit_tqdm.set_tqdm_text) # attach console text receiver to console text thread self.tqdm_text_receiver.moveToThread(self.thread_tqdm_queue_listener) # attach to start / stop methods self.thread_tqdm_queue_listener.started.connect(self.tqdm_text_receiver.run) self.thread_tqdm_queue_listener.start() layout.addWidget(self.btn_perform_actions) layout.addWidget(self.text_edit_std_out) layout.addWidget(self.text_edit_tqdm) self.setLayout(layout) self.show() @pyqtSlot() def _btn_go_clicked(self): # prepare thread for long operation self.init_procedure_object.moveToThread(self.thread_initialize) self.thread_initialize.started.connect(self.init_procedure_object.run) # start thread self.btn_perform_actions.setEnabled(False) self.thread_initialize.start() class StdOutTextEdit(QTextEdit): def __init__(self, parent): super(StdOutTextEdit, self).__init__() self.setParent(parent) self.setReadOnly(True) self.setLineWidth(50) self.setMinimumWidth(500) self.setFont(QFont('Consolas', 11)) @pyqtSlot(str) def append_text(self, text: str): self.moveCursor(QTextCursor.End) self.insertPlainText(text) class StdTQDMTextEdit(QLineEdit): def __init__(self, parent): super(StdTQDMTextEdit, self).__init__() self.setParent(parent) self.setReadOnly(True) self.setEnabled(True) self.setMinimumWidth(500) self.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.setClearButtonEnabled(True) self.setFont(QFont('Consolas', 11)) @pyqtSlot(str) def set_tqdm_text(self, text: str): new_text = text if new_text.find(' ') >= 0: new_text = new_text.replace(' ', '').rstrip() if new_text: self.setText(new_text) else: # we suppose that all TQDM prints have , so drop the rest pass class LongProcedureWrapper(QObject): def __init__(self, main_app: MainApp): super(LongProcedureWrapper, self).__init__() self._main_app = main_app @pyqtSlot() def run(self): third_party_module_not_to_change.long_procedure() if __name__ == '__main__': app = QApplication(sys.argv) app.setStyle('Fusion') ex = MainApp() sys.exit(app.exec_()) |
my_logging.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import logging import datetime import tqdm from config import config_dict, IS_SETUP_DONE def setup_logging(log_prefix, force_debug_level=logging.DEBUG): root = logging.getLogger() root.setLevel(force_debug_level) if config_dict[IS_SETUP_DONE]: pass else: __log_file_name ="{}-{}_log_file.txt".format(log_prefix, datetime.datetime.utcnow().isoformat().replace(":","-")) __log_format = '%(asctime)s - %(name)-30s - %(levelname)s - %(message)s' __console_date_format = '%Y-%m-%d %H:%M:%S' __file_date_format = '%Y-%m-%d %H-%M-%S' console_formatter = logging.Formatter(__log_format, __console_date_format) file_formatter = logging.Formatter(__log_format, __file_date_format) file_handler = logging.FileHandler(__log_file_name, mode='a', delay=True) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) root.addHandler(file_handler) tqdm_handler = TqdmLoggingHandler() tqdm_handler.setLevel(logging.DEBUG) tqdm_handler.setFormatter(console_formatter) root.addHandler(tqdm_handler) config_dict[IS_SETUP_DONE] = True class TqdmLoggingHandler(logging.StreamHandler): def __init__(self): logging.StreamHandler.__init__(self) def emit(self, record): msg = self.format(record) tqdm.tqdm.write(msg) self.flush() |
output_redirection_tools.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | import sys from queue import Queue from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject from config import config_dict, IS_STREAMS_REDIRECTION_SETUP_DONE, TQDM_WRITE_STREAM_CONFIG, STDOUT_WRITE_STREAM_CONFIG, \ STREAM_CONFIG_KEY_QUEUE, STREAM_CONFIG_KEY_STREAM, STREAM_CONFIG_KEY_QT_QUEUE_RECEIVER class QueueWriteStream(object): def __init__(self, q: Queue): self.queue = q def write(self, text): self.queue.put(text) def flush(self): pass def perform_tqdm_default_out_stream_hack(tqdm_file_stream, tqdm_nb_columns=None): import tqdm # save original class into module tqdm.orignal_class = tqdm.tqdm class TQDMPatch(tqdm.orignal_class): """ Derive from original class """ def __init__(self, iterable=None, desc=None, total=None, leave=True, file=None, ncols=None, mininterval=0.1, maxinterval=10.0, miniters=None, ascii=None, disable=False, unit='it', unit_scale=False, dynamic_ncols=False, smoothing=0.3, bar_format=None, initial=0, position=None, postfix=None, unit_divisor=1000, gui=False, **kwargs): super(TQDMPatch, self).__init__(iterable, desc, total, leave, tqdm_file_stream, # change any chosen file stream with our's tqdm_nb_columns, # change nb of columns (gui choice), mininterval, maxinterval, miniters, ascii, disable, unit, unit_scale, False, # change param smoothing, bar_format, initial, position, postfix, unit_divisor, gui, **kwargs) print('TQDM Patch called') # check it works @classmethod def write(cls, s, file=None, end=" ", nolock=False): super(TQDMPatch, cls).write(s=s, file=file, end=end, nolock=nolock) #tqdm.orignal_class.write(s=s, file=file, end=end, nolock=nolock) # all other tqdm.orignal_class @classmethod methods may need to be redefined ! # # I mainly used tqdm.auto in my modules, so use that for patch # # unsure if this will work with all possible tqdm import methods # # might not work for tqdm_gui ! import tqdm.auto as AUTO # # # change original class with the patched one, the original still exists AUTO.tqdm = TQDMPatch #tqdm.tqdm = TQDMPatch def setup_streams_redirection(tqdm_nb_columns=None): if config_dict[IS_STREAMS_REDIRECTION_SETUP_DONE]: pass else: configure_tqdm_redirection(tqdm_nb_columns) configure_std_out_redirection() config_dict[IS_STREAMS_REDIRECTION_SETUP_DONE] = True def configure_std_out_redirection(): queue_std_out = Queue() config_dict[STDOUT_WRITE_STREAM_CONFIG] = { STREAM_CONFIG_KEY_QUEUE: queue_std_out, STREAM_CONFIG_KEY_STREAM: QueueWriteStream(queue_std_out), STREAM_CONFIG_KEY_QT_QUEUE_RECEIVER: StdOutTextQueueReceiver(q=queue_std_out) } perform_std_out_hack() def perform_std_out_hack(): sys.stdout = config_dict[STDOUT_WRITE_STREAM_CONFIG][STREAM_CONFIG_KEY_STREAM] def configure_tqdm_redirection(tqdm_nb_columns=None): queue_tqdm = Queue() config_dict[TQDM_WRITE_STREAM_CONFIG] = { STREAM_CONFIG_KEY_QUEUE: queue_tqdm, STREAM_CONFIG_KEY_STREAM: QueueWriteStream(queue_tqdm), STREAM_CONFIG_KEY_QT_QUEUE_RECEIVER: TQDMTextQueueReceiver(q=queue_tqdm) } perform_tqdm_default_out_stream_hack( tqdm_file_stream=config_dict[TQDM_WRITE_STREAM_CONFIG][STREAM_CONFIG_KEY_STREAM], tqdm_nb_columns=tqdm_nb_columns) class StdOutTextQueueReceiver(QObject): # we are forced to define 1 signal per class # see https://stackoverflow.com/questions/50294652/how-to-create-pyqtsignals-dynamically queue_std_out_element_received_signal = pyqtSignal(str) def __init__(self, q: Queue, *args, **kwargs): QObject.__init__(self, *args, **kwargs) self.queue = q @pyqtSlot() def run(self): self.queue_std_out_element_received_signal.emit('---> STD OUT Queue reception Started <--- ') while True: text = self.queue.get() self.queue_std_out_element_received_signal.emit(text) class TQDMTextQueueReceiver(QObject): # we are forced to define 1 signal per class # see https://stackoverflow.com/questions/50294652/how-to-create-pyqtsignals-dynamically queue_tqdm_element_received_signal = pyqtSignal(str) def __init__(self, q: Queue, *args, **kwargs): QObject.__init__(self, *args, **kwargs) self.queue = q @pyqtSlot() def run(self): # we assume that all TQDM outputs start with , so use that to show stream reception is started self.queue_tqdm_element_received_signal.emit(' ---> TQDM Queue reception Started <--- ') while True: text = self.queue.get() self.queue_tqdm_element_received_signal.emit(text) setup_streams_redirection() |
config.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | IS_SETUP_DONE = 'is_setup_done' TQDM_WRITE_STREAM_CONFIG = 'TQDM_WRITE_STREAM_CONFIG' STDOUT_WRITE_STREAM_CONFIG = 'STDOUT_WRITE_STREAM_CONFIG' IS_STREAMS_REDIRECTION_SETUP_DONE = 'IS_STREAMS_REDIRECTION_SETUP_DONE' STREAM_CONFIG_KEY_QUEUE = 'queue' STREAM_CONFIG_KEY_STREAM = 'write_stream' STREAM_CONFIG_KEY_QT_QUEUE_RECEIVER = 'qt_queue_receiver' default_config_dict = { IS_SETUP_DONE: False, IS_STREAMS_REDIRECTION_SETUP_DONE: False, TQDM_WRITE_STREAM_CONFIG: None, STDOUT_WRITE_STREAM_CONFIG: None, } config_dict = default_config_dict |
third_part_module_not_to_change.py
代表我使用的代码类型,并且不希望/无法更改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from tqdm.auto import tqdm import logging from my_logging import setup_logging import time def long_procedure(): setup_logging('long_procedure') __logger = logging.getLogger('long_procedure') __logger.setLevel(logging.DEBUG) tqdm_obect = tqdm(range(10), unit_scale=True, dynamic_ncols=True) tqdm_obect.set_description("My progress bar description") for i in tqdm_obect: time.sleep(.1) __logger.info('foo {}'.format(i)) |