This article is based on the theme of Qiita Summer Festival 2020 "If you want to make a △△ (app) using only 〇〇 (language)" The contents are in line with it.
On July 5, 2020, my work "VMD sizing ver5.00 (exe | code)" Has been released. The theme of this tool is to "regenerate VMD (MMD motion data) with the appropriate head and body for the specified model", well, it sounds to most of you who are reading this article. It's a hobby app.
** Nga! !! !! ** **
Most of the people who enjoy MMD (MikuMikuDance) are very ordinary people who have nothing to do with Python or programs. If anyone responds with "Snake ?!", I think it's rather valuable. How can such people use their own apps? This article is about the theme of how to make a ** "App that adjusts the UI to something nice after a while" **, which has come to the fore after such anguish, trial and error, and a lot of trouble. , It is a summary of my own answers. By the way, "VMD sizing" has been used to the extent that the cumulative total exceeds DL8500. (Approximately 9600 DL when combined with the 32-bit version)
Python + pyinstaller = exe
is not so rare, but it requires some ingenuity to bring it down to a level that can withstand actual operation.
<"Isn't it okay to make it in C?" ** I like Python. ** (Because I don't know C ...)
First, let's prepare the development environment.
<"It's not a reason for machine learning, isn't it okay to leave it raw?" **No way! !! ** **
A common problem in pyinstaller articles is that "extra libraries are included and the exe file becomes large". It's common to try out new libraries during development. However, if you create an exe as it is, there is a high possibility that unnecessary libraries will be included in the exe. ** Let's separate the development environment and the release environment exactly. ** **
Download the installer from Anaconda Official. If it is made from now on, it would be better to use 3 series.
After DL, please follow the steps to install.
First, let's build a virtual environment for development.
conda create -n pytest_env pip python=3.7
Once you have a development environment, let's ʻactivate`. By the way, let's also create a source code management directory.
Similarly, create a release environment,
conda create -n pytest_release pip python=3.7
Once you have the management directory, move to it and install the required libraries. Here is one trick.
** pyinstaller
installs only in release environment **
By installing pyinstaller
only in the release environment, you can prevent accidental release in the development environment.
Since it's a big deal, let's introduce numpy.
Installation command for development environment
pip install numpy wxPython
Installation command for release environment
pip install numpy wxPython pypiwin32 pyinstaller
pypiwin32
seems to be the library needed to run pyinstaller
on Windows.
The GUI is easy to create using WxFormBuilder. However, there are some things that are difficult to understand the automatic naming convention, that parts cannot be reused, and that there are some things that are not enough to create an actual operation application, so I output it when it is in a certain form, and after that I have to do it myself. I recommend it.
Reference: GUI (WxFormBuilder) in Python (mm_sys) https://qiita.com/mm_sys/items/716cb159ea8c9e634300
After this, we will proceed in a reverse lookup format. Please see the section you are interested in.
I think most of the people who are interested in this article are interested in it. I would love to know if there is a correct answer. So I skipped various things and brought it to the beginning.
--Start a logic thread that runs for a long time while keeping the GUI thread as it is --Multiprocess can also be executed in the logic thread --Logic threads do not increase processes --When the GUI thread is terminated, the logic thread is also terminated. --End the logic thread with the suspend button --The button for logic thread is valid only for single click (double click is disabled)
The code that meets the above requirements is below.
executor.py
# -*- coding: utf-8 -*-
#
import wx
import sys
import argparse
import numpy as np
import multiprocessing
from pathlib import Path
from form.MainFrame import MainFrame
from utils.MLogger import MLogger
VERSION_NAME = "ver1.00"
#No exponential notation, omitted if the number of effective decimal places exceeds 6 or 30, and the number of characters in one line is 200.
np.set_printoptions(suppress=True, precision=6, threshold=30, linewidth=200)
#Windows multi-process measures
multiprocessing.freeze_support()
if __name__ == '__main__':
#Argument interpretation
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", default=20, type=int)
args = parser.parse_args()
#Logger initialization
MLogger.initialize(level=args.verbose, is_file=False)
#GUI startup
app = wx.App(False)
frame = MainFrame(None, VERSION_NAME, args.verbose)
frame.Show(True)
app.MainLoop()
First, the caller ʻexecutor.py`. Launch the GUI from here.
MainFrame.py
# -*- coding: utf-8 -*-
#
from time import sleep
from worker.LongLogicWorker import LongLogicWorker
from form.ConsoleCtrl import ConsoleCtrl
from utils.MLogger import MLogger
import os
import sys
import wx
import wx.lib.newevent
logger = MLogger(__name__)
TIMER_ID = wx.NewId()
(LongThreadEvent, EVT_LONG_THREAD) = wx.lib.newevent.NewEvent()
#Main GUI
class MainFrame(wx.Frame):
def __init__(self, parent, version_name: str, logging_level: int):
self.version_name = version_name
self.logging_level = logging_level
self.elapsed_time = 0
self.worker = None
#Initialization
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title=u"c01 Long Logic {0}".format(self.version_name), \
pos=wx.DefaultPosition, size=wx.Size(600, 650), style=wx.DEFAULT_FRAME_STYLE)
self.sizer = wx.BoxSizer(wx.VERTICAL)
#processing time
self.loop_cnt_ctrl = wx.SpinCtrl(self, id=wx.ID_ANY, size=wx.Size(100, -1), value="2", min=1, max=999, initial=2)
self.loop_cnt_ctrl.SetToolTip(u"processing time")
self.sizer.Add(self.loop_cnt_ctrl, 0, wx.ALL, 5)
#Check box for parallel processing
self.multi_process_ctrl = wx.CheckBox(self, id=wx.ID_ANY, label="If you want to execute parallel processing, please check it.")
self.sizer.Add(self.multi_process_ctrl, 0, wx.ALL, 5)
#Button Sizer
self.btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
#Run button
self.exec_btn_ctrl = wx.Button(self, wx.ID_ANY, u"Start long logic processing", wx.DefaultPosition, wx.Size(200, 50), 0)
#Binding with mouse left click event [Point.01】
self.exec_btn_ctrl.Bind(wx.EVT_LEFT_DOWN, self.on_exec_click)
#Binding with mouse left double click event [Point.03】
self.exec_btn_ctrl.Bind(wx.EVT_LEFT_DCLICK, self.on_doubleclick)
self.btn_sizer.Add(self.exec_btn_ctrl, 0, wx.ALIGN_CENTER, 5)
#Suspend button
self.kill_btn_ctrl = wx.Button(self, wx.ID_ANY, u"Long logic processing interruption", wx.DefaultPosition, wx.Size(200, 50), 0)
#Bind with mouse left click event
self.kill_btn_ctrl.Bind(wx.EVT_LEFT_DOWN, self.on_kill_click)
#Bind with mouse left double click event
self.kill_btn_ctrl.Bind(wx.EVT_LEFT_DCLICK, self.on_doubleclick)
#Initial state is inactive
self.kill_btn_ctrl.Disable()
self.btn_sizer.Add(self.kill_btn_ctrl, 0, wx.ALIGN_CENTER, 5)
self.sizer.Add(self.btn_sizer, 0, wx.ALIGN_CENTER | wx.SHAPED, 0)
#Console [Point.06】
self.console_ctrl = ConsoleCtrl(self)
self.sizer.Add(self.console_ctrl, 1, wx.ALL | wx.EXPAND, 5)
#print Output destination is console [Point.05】
sys.stdout = self.console_ctrl
#Progress gauge
self.gauge_ctrl = wx.Gauge(self, wx.ID_ANY, 100, wx.DefaultPosition, wx.DefaultSize, wx.GA_HORIZONTAL)
self.gauge_ctrl.SetValue(0)
self.sizer.Add(self.gauge_ctrl, 0, wx.ALL | wx.EXPAND, 5)
#Event bind [Point.05】
self.Bind(EVT_LONG_THREAD, self.on_exec_result)
self.SetSizer(self.sizer)
self.Layout()
#Display in the center of the screen
self.Centre(wx.BOTH)
#Double click invalidation process
def on_doubleclick(self, event: wx.Event):
self.timer.Stop()
logger.warning("It was double-clicked.", decoration=MLogger.DECORATION_BOX)
event.Skip(False)
return False
#Execute 1 Processing when clicked
def on_exec_click(self, event: wx.Event):
#Start with a slight delay with a timer (avoid batting with double click) [Point.04】
self.timer = wx.Timer(self, TIMER_ID)
self.timer.StartOnce(200)
self.Bind(wx.EVT_TIMER, self.on_exec, id=TIMER_ID)
#Interruption 1 Click processing
def on_kill_click(self, event: wx.Event):
self.timer = wx.Timer(self, TIMER_ID)
self.timer.StartOnce(200)
self.Bind(wx.EVT_TIMER, self.on_kill, id=TIMER_ID)
#Processing execution
def on_exec(self, event: wx.Event):
self.timer.Stop()
if not self.worker:
#Console clear
self.console_ctrl.Clear()
#Disable execute button
self.exec_btn_ctrl.Disable()
#Suspend button enabled
self.kill_btn_ctrl.Enable()
#Execute in another thread [Point.09】
self.worker = LongLogicWorker(self, LongThreadEvent, self.loop_cnt_ctrl.GetValue(), self.multi_process_ctrl.GetValue())
self.worker.start()
event.Skip(False)
#Suspend processing execution
def on_kill(self, event: wx.Event):
self.timer.Stop()
if self.worker:
#When the button is pressed in the stopped state, it stops
self.worker.stop()
logger.warning("Interrupts long logic processing.", decoration=MLogger.DECORATION_BOX)
#Worker end
self.worker = None
#Run button enabled
self.exec_btn_ctrl.Enable()
#Disable suspend button
self.kill_btn_ctrl.Disable()
#Hide progress
self.gauge_ctrl.SetValue(0)
event.Skip(False)
#Processing after long logic is over
def on_exec_result(self, event: wx.Event):
# 【Point.12] Make the logic end explicitly known
self.sound_finish()
#Run button enabled
self.exec_btn_ctrl.Enable()
#Disable suspend button
self.kill_btn_ctrl.Disable()
if not event.result:
event.Skip(False)
return False
self.elapsed_time += event.elapsed_time
logger.info("\n Processing time: %s", self.show_worked_time())
#Worker end
self.worker = None
#Hide progress
self.gauge_ctrl.SetValue(0)
def sound_finish(self):
#Sound the end sound
if os.name == "nt":
# Windows
try:
import winsound
winsound.PlaySound("SystemAsterisk", winsound.SND_ALIAS)
except Exception:
pass
def show_worked_time(self):
#Convert elapsed seconds to hours, minutes and seconds
td_m, td_s = divmod(self.elapsed_time, 60)
if td_m == 0:
worked_time = "{0:02d}Seconds".format(int(td_s))
else:
worked_time = "{0:02d}Minutes{1:02d}Seconds".format(int(td_m), int(td_s))
return worked_time
First of all, let's bind the mouse left-click event on the button and the method to be executed.
There are several binding methods, but I personally like to write Parts.Bind (event type, firing method)
for binding GUI parts and events.
If you pick up the left-click event and execute it as it is, it will fire at the same time as the double-click event, and as a result, the double-click event will run.
(In terms of processing, the double-click event is part of the single-click event, so the two events fire at the same time.)
Therefore, by setting a timer in ʻon_exec_click and ʻon_kill_click
fired from a single click and executing it with a slight delay, the execution method bound to the double-click event will be executed first.
Bind the left double-click event in the same procedure as Point ①. You can prevent double processing by picking up double clicks here.
The double-click event will stop the timer event executed at Point②. You can now disable double-clicking.
Only single-click event fires … The corresponding event will be executed with a slight delay
When the double click event fires … The single-click event is not executed because the timer is stopped.
print
to console controlprint
is a wrapper for sys.stdout.write
, so if you set the output destination to a console control, the output destination of print
will be inside the control.
So, what is that console control? It's a subclass of wx.TextCtrl
.
ConsoleCtrl.py
# -*- coding: utf-8 -*-
#
import wx
from utils.MLogger import MLogger # noqa
logger = MLogger(__name__)
class ConsoleCtrl(wx.TextCtrl):
def __init__(self, parent):
#Multiple lines allowed, read-only, no border, vertical scrolling, horizontal scrolling, key event acquisition
super().__init__(parent, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size(-1, -1), \
wx.TE_MULTILINE | wx.TE_READONLY | wx.BORDER_NONE | wx.HSCROLL | wx.VSCROLL | wx.WANTS_CHARS)
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DLIGHT))
#Keyboard event binding
self.Bind(wx.EVT_CHAR, lambda event: self.on_select_all(event, self.console_ctrl))
#Output processing of console part [Point.07】
def write(self, text):
try:
wx.CallAfter(self.AppendText, text)
except: # noqa
pass
#All selection process of console part [Point.08】
def on_select_all(event, target_ctrl):
keyInput = event.GetKeyCode()
if keyInput == 1: # 1 stands for 'ctrl+a'
target_ctrl.SelectAll()
event.Skip()
write
methodCall the ʻAppendText method with
CallAfter, assuming that it will be called from a logic thread different from the GUI thread. This will also stabilize the
print` output from the logic thread.
If there are letters, it is the human saga that makes you want to copy them. Therefore, the all-selection process is executed in the all-selection event (combination of keyboard events).
I finally got into the main subject.
Perform logic processing within LongLogicWorker
.
Reference source: https://doloopwhile.hatenablog.com/entry/20090627/1275175850
LongLogicWorker.py
# -*- coding: utf-8 -*-
#
import os
import wx
import time
from worker.BaseWorker import BaseWorker, task_takes_time
from service.MOptions import MOptions
from service.LongLogicService import LongLogicService
class LongLogicWorker(BaseWorker):
def __init__(self, frame: wx.Frame, result_event: wx.Event, loop_cnt: int, is_multi_process: bool):
#processing time
self.loop_cnt = loop_cnt
#Whether to run in multiple processes
self.is_multi_process = is_multi_process
super().__init__(frame, result_event)
@task_takes_time
def thread_event(self):
start = time.time()
#Para and options stuffing
# max_The maximum value of workers is Python3.Based on the default value of 8
options = MOptions(self.frame.version_name, self.frame.logging_level, self.loop_cnt, max_workers=(1 if not self.is_multi_process else min(32, os.cpu_count() + 4)))
#Logic service execution
LongLogicService(options).execute()
#elapsed time
self.elapsed_time = time.time() - start
def post_event(self):
#Call and execute the event after the logic processing is completed [Point.11】
wx.PostEvent(self.frame, self.result_event(result=self.result and not self.is_killed, elapsed_time=self.elapsed_time))
LongLogicWorker
inherits from BaseWorker
.
BaseWorker.py
# -*- coding: utf-8 -*-
#
import wx
import wx.xrc
from abc import ABCMeta, abstractmethod
from threading import Thread
from functools import wraps
import time
import threading
from utils.MLogger import MLogger # noqa
logger = MLogger(__name__)
# https://wiki.wxpython.org/LongRunningTasks
# https://teratail.com/questions/158458
# http://nobunaga.hatenablog.jp/entry/2016/06/03/204450
class BaseWorker(metaclass=ABCMeta):
"""Worker Thread Class."""
def __init__(self, frame, result_event):
"""Init Worker Thread Class."""
#Parent GUI
self.frame = frame
#elapsed time
self.elapsed_time = 0
#Call event after the thread ends
self.result_event = result_event
#Progress gauge
self.gauge_ctrl = frame.gauge_ctrl
#Successful processing
self.result = True
#With or without stop command
self.is_killed = False
#Thread start
def start(self):
self.run()
#Thread stop
def stop(self):
#Turn on interrupt FLG
self.is_killed = True
def run(self):
#Thread execution
self.thread_event()
#Post-processing execution
self.post_event()
def post_event(self):
wx.PostEvent(self.frame, self.result_event(result=self.result))
@abstractmethod
def thread_event(self):
pass
# https://doloopwhile.hatenablog.com/entry/20090627/1275175850
class SimpleThread(Thread):
"""A thread that just executes a callable object (such as a function)"""
def __init__(self, base_worker, acallable):
#Processing in another thread
self.base_worker = base_worker
#Methods to run in the function decorator
self.acallable = acallable
#Function decorator results
self._result = None
#Suspended FLG=Initialize in the OFF state
super(SimpleThread, self).__init__(name="simple_thread", kwargs={"is_killed": False})
def run(self):
self._result = self.acallable(self.base_worker)
def result(self):
return self._result
def task_takes_time(acallable):
"""
Function Decorator [Point.10】
While executing the original processing of acallable in another thread
Update window wx.Keep calling YieldIfNeeded
"""
@wraps(acallable)
def f(base_worker):
t = SimpleThread(base_worker, acallable)
#A demon that kills a child when a parent dies
t.daemon = True
t.start()
#Keep updating window drawings for the life of the thread
while t.is_alive():
#Twirl the progress gauge
base_worker.gauge_ctrl.Pulse()
#Refresh the window if necessary
wx.YieldIfNeeded()
#Wait a little
time.sleep(0.01)
if base_worker.is_killed:
# 【Point.23] If the caller issues a stop command, you(GUI)Termination command to all threads except
for th in threading.enumerate():
if th.ident != threading.current_thread().ident and "_kwargs" in dir(th):
th._kwargs["is_killed"] = True
break
return t.result()
return f
This is a story I have already received from the reference site, but I will continue to update the drawing of the GUI thread while running another thread with the function decorator.
In SimpleThread
, execute ʻacallable. At this time, the reason for holding
BaseWorkeris to pass the suspension flag. While
SimpleThread` is alive, the GUI thread only updates the drawing and accepts interruptions.
(This area will be shown later)
Since the call event after processing was passed when the worker was initialized in advance, execute it with wx.PostEvent
.
This will bring you back to the process in the GUI.
Until the logic is finished, not all users are stuck on the PC all the time, so to make it easier to understand when it is finished, sizing makes an INFO sound. Depending on the Windows environment and other environments such as Linux, an error may occur, so it seems better to make it sound only when it can be sounded with try-except. By the way, if you give the elapsed time, the feeling of how long it took will be quantified, so I feel that it is easy to understand.
LongLogicService.py
# -*- coding: utf-8 -*-
#
import logging
from time import sleep
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor
from service.MOptions import MOptions
from utils.MException import MLogicException, MKilledException
from utils.MLogger import MLogger # noqa
logger = MLogger(__name__)
class LongLogicService():
def __init__(self, options: MOptions):
self.options = options
def execute(self):
logging.basicConfig(level=self.options.logging_level, format="%(message)s [%(module_name)s]")
# 【Point.13] try the whole-Enclose in except and output the error content
try:
#It is OK to put logic normally
self.execute_inner(-1)
logger.info("--------------")
#It is OK to distribute with parallel tasks
futures = []
# 【Point.14] Give names to parallel tasks
with ThreadPoolExecutor(thread_name_prefix="long_logic", max_workers=self.options.max_workers) as executor:
for n in range(self.options.loop_cnt):
futures.append(executor.submit(self.execute_inner, n))
#【Point.15] Parallel tasks wait for completion after being issued in bulk
concurrent.futures.wait(futures, timeout=None, return_when=concurrent.futures.FIRST_EXCEPTION)
for f in futures:
if not f.result():
return False
logger.info("End of long logic processing", decoration=MLogger.DECORATION_BOX, title="Logic end")
return True
except MKilledException:
#In case of termination by suspend option, only the result is returned as it is
return False
except MLogicException as se:
#Incomplete data error
logger.error("It ended with data that cannot be processed.\n\n%s", se.message, decoration=MLogger.DECORATION_BOX)
return False
except Exception as e:
#Other errors
logger.critical("The process ended with an unintended error.", e, decoration=MLogger.DECORATION_BOX)
return False
finally:
logging.shutdown()
def execute_inner(self, n: int):
for m in range(5):
logger.info("n: %s - m: %s", n, m)
sleep(1)
return True
Since the threads are separated, if you do not exclude the error properly, you may end up suddenly though you do not know what it is. It's hard to chase afterwards, so let's exclude it and log it.
When executing parallel tasks, it is easier to debug if you add a prefix so that you can easily understand which process is the problem.
Concurrent tasks are first issued with ʻexecutor.submitand then waited with
concurrent.futures.waituntil all processing is complete. At that time, the
concurrent.futures.FIRST_EXCEPTION` option is added so that processing will be interrupted if any Exception occurs.
MLogger.py
# -*- coding: utf-8 -*-
#
from datetime import datetime
import logging
import traceback
import threading
from utils.MException import MKilledException
# 【Point.16] Implement your own logger
class MLogger():
DECORATION_IN_BOX = "in_box"
DECORATION_BOX = "box"
DECORATION_LINE = "line"
DEFAULT_FORMAT = "%(message)s [%(funcName)s][P-%(process)s](%(asctime)s)"
DEBUG_FULL = 2
TEST = 5
TIMER = 12
FULL = 15
INFO_DEBUG = 22
DEBUG = logging.DEBUG
INFO = logging.INFO
WARNING = logging.WARNING
ERROR = logging.ERROR
CRITICAL = logging.CRITICAL
total_level = logging.INFO
is_file = False
outout_datetime = ""
logger = None
#Initialization
# 【Point.17] Be able to define the minimum output level for each module
def __init__(self, module_name, level=logging.INFO):
self.module_name = module_name
self.default_level = level
#Logger
self.logger = logging.getLogger("PyLogicSample").getChild(self.module_name)
#Standard output handler
sh = logging.StreamHandler()
sh.setLevel(level)
self.logger.addHandler(sh)
# 【Point.18] Prepare a log method with a lower level than debug
def test(self, msg, *args, **kwargs):
if not kwargs:
kwargs = {}
kwargs["level"] = self.TEST
kwargs["time"] = True
self.print_logger(msg, *args, **kwargs)
def debug(self, msg, *args, **kwargs):
if not kwargs:
kwargs = {}
kwargs["level"] = logging.DEBUG
kwargs["time"] = True
self.print_logger(msg, *args, **kwargs)
def info(self, msg, *args, **kwargs):
if not kwargs:
kwargs = {}
kwargs["level"] = logging.INFO
self.print_logger(msg, *args, **kwargs)
def warning(self, msg, *args, **kwargs):
if not kwargs:
kwargs = {}
kwargs["level"] = logging.WARNING
self.print_logger(msg, *args, **kwargs)
def error(self, msg, *args, **kwargs):
if not kwargs:
kwargs = {}
kwargs["level"] = logging.ERROR
self.print_logger(msg, *args, **kwargs)
def critical(self, msg, *args, **kwargs):
if not kwargs:
kwargs = {}
kwargs["level"] = logging.CRITICAL
self.print_logger(msg, *args, **kwargs)
#Actual output
def print_logger(self, msg, *args, **kwargs):
#【Point.22] Suspend FLG on the thread currently running=If ON is set, an interruption error will occur.
if "is_killed" in threading.current_thread()._kwargs and threading.current_thread()._kwargs["is_killed"]:
#If a stop command is issued, an error
raise MKilledException()
target_level = kwargs.pop("level", logging.INFO)
#Output only when both application and module log levels are met
if self.total_level <= target_level and self.default_level <= target_level:
if self.is_file:
for f in self.logger.handlers:
if isinstance(f, logging.FileHandler):
#Delete all existing file handlers
self.logger.removeHandler(f)
#If there is file output, handler association
#File output handler
fh = logging.FileHandler("log/PyLogic_{0}.log".format(self.outout_datetime))
fh.setLevel(self.default_level)
fh.setFormatter(logging.Formatter(self.DEFAULT_FORMAT))
self.logger.addHandler(fh)
#Added to output module name
extra_args = {}
extra_args["module_name"] = self.module_name
#Log record generation
if args and isinstance(args[0], Exception):
# 【Point.19] When an Exception is received, a stack trace is output.
log_record = self.logger.makeRecord('name', target_level, "(unknown file)", 0, "{0}\n\n{1}".format(msg, traceback.format_exc()), None, None, self.module_name)
else:
log_record = self.logger.makeRecord('name', target_level, "(unknown file)", 0, msg, args, None, self.module_name)
target_decoration = kwargs.pop("decoration", None)
title = kwargs.pop("title", None)
print_msg = "{message}".format(message=log_record.getMessage())
# 【Point.20] Decorate log messages with parameters
if target_decoration:
if target_decoration == MLogger.DECORATION_BOX:
output_msg = self.create_box_message(print_msg, target_level, title)
elif target_decoration == MLogger.DECORATION_LINE:
output_msg = self.create_line_message(print_msg, target_level, title)
elif target_decoration == MLogger.DECORATION_IN_BOX:
output_msg = self.create_in_box_message(print_msg, target_level, title)
else:
output_msg = self.create_simple_message(print_msg, target_level, title)
else:
output_msg = self.create_simple_message(print_msg, target_level, title)
#output
try:
if self.is_file:
#If there is file output, regenerate the record and output both console and GUI
log_record = self.logger.makeRecord('name', target_level, "(unknown file)", 0, output_msg, None, None, self.module_name)
self.logger.handle(log_record)
else:
# 【Point.21] Logic thread is output separately for print and logger
print(output_msg)
self.logger.handle(log_record)
except Exception as e:
raise e
def create_box_message(self, msg, level, title=None):
msg_block = []
msg_block.append("■■■■■■■■■■■■■■■■■")
if level == logging.CRITICAL:
msg_block.append("■ **CRITICAL** ")
if level == logging.ERROR:
msg_block.append("■ **ERROR** ")
if level == logging.WARNING:
msg_block.append("■ **WARNING** ")
if level <= logging.INFO and title:
msg_block.append("■ **{0}** ".format(title))
for msg_line in msg.split("\n"):
msg_block.append("■ {0}".format(msg_line))
msg_block.append("■■■■■■■■■■■■■■■■■")
return "\n".join(msg_block)
def create_line_message(self, msg, level, title=None):
msg_block = []
for msg_line in msg.split("\n"):
msg_block.append("■■ {0} --------------------".format(msg_line))
return "\n".join(msg_block)
def create_in_box_message(self, msg, level, title=None):
msg_block = []
for msg_line in msg.split("\n"):
msg_block.append("■ {0}".format(msg_line))
return "\n".join(msg_block)
def create_simple_message(self, msg, level, title=None):
msg_block = []
for msg_line in msg.split("\n"):
# msg_block.append("[{0}] {1}".format(logging.getLevelName(level)[0], msg_line))
msg_block.append(msg_line)
return "\n".join(msg_block)
@classmethod
def initialize(cls, level=logging.INFO, is_file=False):
# logging.basicConfig(level=level)
logging.basicConfig(level=level, format=cls.DEFAULT_FORMAT)
cls.total_level = level
cls.is_file = is_file
cls.outout_datetime = "{0:%Y%m%d_%H%M%S}".format(datetime.now())
Perhaps the logger part is the one that I have the most ingenuity. It is easier for users to output the log according to a certain format, but it is very troublesome to define it one by one. You can decorate the message box and ruled lines with a single flag. And above all, the logger is used to judge the interruption flag. (Details will be described later)
By defining the minimum output level for each module, you can suppress the debug log of utility methods in particular. Physically erasing the debug log or commenting it out can be a hassle to check if there is a problem. By raising or lowering the minimum level for each module, you can control the output log level, which will lead to easier debugging.
Although it is paired with 17, it is easier to suppress the output by preparing a low-level method.
This is mainly useful when you get an unhandled exception. Also, since the logger is handled by both the GUI thread and the logic thread, there is no need to adjust the output at the source of the logic thread.
Since we've made the console control a regular text control, we've used a lot of message blocking for clarity. Since the amount of messages is variable, it was not possible to assign a fixed character string, so blocking is called for each given parameter. I think there is a way to specify the method at the caller, but I think that this is easier to manage in terms of meaning. Even when decorating the text, the amount of code will be less if it is handled in one place in this way rather than being separated by the caller.
print
and logger.handle
separately during output processingTo print to the console control, you need the output from print
, and to print to the stream, you need the output from logger.handle
.
Both messages output the same information, and information such as the output module and output time is added to the output to the stream to make it easier to follow.
This was where I was most worried ... The following are some of Python's thread-like practices. --Do not kill threads from the outside --Hold the suspend parameter and refer to the parameter every time it is processed internally --If the suspend parameter is ON, exit from the inside
Reference: https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread
Even if you look at the interruption parameters internally, you don't want to see them for each logic process, and you don't want to carry around the parameters ... So, isn't it a logger that always passes any logic? So, if you can see it on the logger and you can change it from the GUI thread, it's about "current thread" ... By saying that, it became like this.
I don't know if it's a good method, but I like it because logic processing doesn't care about interruptions at all.
The suspend FLG is set in the BaseWorker
function decorator.
Suspend of all living threads other than GUI thread By turning on FLG, you can see the interruption by looking at any thread.
Now, when you try to output the log, an error will occur and you will be returned to the GUI.
Normal termination route
Suspended end route
Now that we've created the environment, let's make it run from VS Code.
In the Python Path
field of the workspace, specify the full path of ʻAnaconda> envs> Development Environment> python.exe`.
Use launch
to specify the execution of the exe.
{
"folders": [
{
"path": "src"
}
],
"settings": {
"python.pythonPath": "C:\\Development\\Anaconda3\\envs\\pytest_env\\python.exe"
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Python: debug",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/executor.py",
"console": "integratedTerminal",
"pythonPath": "${command:python.interpreterPath}",
"stopOnEntry": false,
"args": [
// "--verbose", "1", //minimum
// "--verbose", "2", // DEBUG_FULL
// "--verbose", "15", // FULL
"--verbose", "10", // TEST
// "--verbose", "20", // INFO
]
}
]
}
}
You can now launch the GUI from VS Code.
I've put together a lot of code, but in the end I have to make it an exe. So, here are the batches and config files that create PythonExe.
pyinstaller64.bat
@echo off
rem ---
rem ---Generate exe
rem ---
rem ---Change the current directory to the execution destination
cd /d %~dp0
cls
rem ---After switching to the release environment, run pyinstaller
rem ---Return to development environment when finished
activate pytest_release && pyinstaller --clean pytest64.spec && activate pytest_env
pytest64.spec
# -*- coding: utf-8 -*-
# -*- mode: python -*-
#PythonExe sample 64bit version
block_cipher = None
a = Analysis(['src\\executor.py'],
pathex=[],
binaries=[],
datas=[],
#Hidden library import
hiddenimports=['wx._adv', 'wx._html', 'pkg_resources.py2_warn'],
hookspath=[],
runtime_hooks=[],
#Excluded libraries
excludes=['mkl','libopenblas'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
#app name
name='PythonExeSample.exe',
#Whether to display the debug log when making an exe
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
#Whether to display the console
console=False )
If you want to stick to an exe, just run the batch, switch to the release environment, run pyinstaller
, and then back to the development environment.
Now you don't have to worry about inadvertently putting extra libraries in your release environment.
The spec file is a configuration file for pyinstaller
, but it is a setting added by a comment line.
hiddenimports
pyinstaller
basically automatically includes the libraries called inside the code, but there are some libraries that cannot be included as they are.
It is hidden imports
that explicitly imports it.
The only way to find this is to change debug = False
at the bottom to True
and look for the part that is causing the error, which is a very simple task.
excludes
On the contrary, if you want to exclude it because the file size becomes large if it is bundled together, specify it with ʻexcludes. In this case,
mkl and
libopenblas` are excluded by referring to https://www.reddit.com/r/pygame/comments/aelypb/why_is_my_pyinstaller_executable_180_mb_large/.
The completed exe is about 30M. (Even if excluded, this size ...
--About icons --About external memory (json)
I may add it when my energy recovers. How are you doing here with VMD sizing? Questions are also welcome.
All of the above code can be found at https://github.com/miu200521358/PythonExeSample. If you are interested, please fork and take a look at the contents.
Recommended Posts