From c37d15e3c9ab40cbd40c4725679d50d2b7fb4690 Mon Sep 17 00:00:00 2001 From: George Yohng Date: Sun, 21 Apr 2024 21:28:28 +0800 Subject: [PATCH 01/16] Convert the rotations field data to string for the GUI framework to recognise it. --- mainwindow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mainwindow.py b/mainwindow.py index f6a56a1..9cfa916 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -616,13 +616,13 @@ def populate_footprint_list(self, *_): # First check if the part name mathes for regex, correction in corrections: if re.search(regex, str(part[1])): - part[8] = correction + part[8] = str(correction) break # If there was no match for the part name, check if the package matches if part[8] == "": for regex, correction in corrections: if re.search(regex, str(part[2])): - part[8] = correction + part[8] = str(correction) break self.footprint_list.AppendItem(part) From 3c5c730d6a6da5809fd417ede946d0e1e3a77bf8 Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Wed, 24 Apr 2024 20:39:55 -0400 Subject: [PATCH 02/16] library.py: search() - fts5 search improvement for keywords shorter than 3 unicode characters FTS5 'match' only evaluates substrings longer than 3 unicode characters, searching for shorter substrings results in no matches being returned. Fixes an issue where attempting to search for substrings shorter than 3 characters was resulting in empty search results. Add "AND description LIKE '%SUBSTRING%'" terms for each substring shorter than 3 characters. --- library.py | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/library.py b/library.py index 0ebbd12..bbff9e0 100644 --- a/library.py +++ b/library.py @@ -128,18 +128,36 @@ def search(self, parameters): query = f"SELECT {s} FROM parts WHERE " match_chunks = [] + like_chunks = [] + query_chunks = [] + # Build 'match_chunks' and 'like_chunks' arrays + # + # FTS5 (https://www.sqlite.org/fts5.html) has a substring limit of + # at least 3 characters. + # 'Substrings consisting of fewer than 3 unicode characters do not + # match any rows when used with a full-text query' + # + # However, they will still match with a LIKE. + # + # So extract out the <3 character strings and add a 'LIKE' term + # for each of those. if parameters["keyword"] != "": keywords = parameters["keyword"].split(" ") - keywords_intermediate = [] + match_keywords_intermediate = [] for w in keywords: # skip over empty keywords if w != "": - kw = f'"{w}"' - keywords_intermediate.append(kw) - keywords_entry = " AND ".join(keywords_intermediate) - match_chunks.append(f"{keywords_entry}") + if len(w) < 3: # LIKE entry + kw = f"description LIKE '%{w}%'" + like_chunks.append(kw) + else: # MATCH entry + kw = f'"{w}"' + match_keywords_intermediate.append(kw) + if match_keywords_intermediate: + match_entry = " AND ".join(match_keywords_intermediate) + match_chunks.append(f"{match_entry}") if "manufacturer" in parameters and parameters["manufacturer"] != "": p = parameters["manufacturer"] @@ -171,7 +189,7 @@ def search(self, parameters): if parameters["stock"]: query_chunks.append('"Stock" > "0"') - if not match_chunks and not query_chunks: + if not match_chunks and not like_chunks and not query_chunks: return [] if match_chunks: @@ -179,9 +197,14 @@ def search(self, parameters): query += " AND ".join(match_chunks) query += "'" - if query_chunks: + if like_chunks: if match_chunks: query += " AND " + query += " AND ".join(like_chunks) + + if query_chunks: + if match_chunks or like_chunks: + query += " AND " query += " AND ".join(query_chunks) query += f' ORDER BY "{self.order_by}" COLLATE naturalsort {self.order_dir}' From c271ad63a2896d967e3cd30b47b7cc872516fe99 Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Fri, 26 Apr 2024 17:37:49 -0400 Subject: [PATCH 03/16] unzip_parts.py - Reduce working memory size by reading and writing smaller chunks into memory vs. the whole zip file parts Reduces the data read into ram from 76MB (the size of each zip file at present) to 1MB. --- unzip_parts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/unzip_parts.py b/unzip_parts.py index 99d1e2e..9d00688 100644 --- a/unzip_parts.py +++ b/unzip_parts.py @@ -25,10 +25,9 @@ def unzip_parts(path): # Open the split file with open(split_path, "rb") as split_file: # Read the file data - file_data = split_file.read() - - # Append the file data to the original file - db.write(file_data) + while (file_data := split_file.read(1024 * 1024)): + # Append the file data to the original file + db.write(file_data) # Delete the split file os.unlink(split_path) From 0bc003957129ae82f53eb29b3aedd1cad8afbb90 Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Sat, 27 Apr 2024 16:32:29 -0400 Subject: [PATCH 04/16] LogBoxHandler - Marshal widget interactions to the main thread for proper thread safety --- events.py | 1 + mainwindow.py | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/events.py b/events.py index 504cce8..fcb5bb4 100644 --- a/events.py +++ b/events.py @@ -8,3 +8,4 @@ AssignPartsEvent, EVT_ASSIGN_PARTS_EVENT = NewEvent() PopulateFootprintListEvent, EVT_POPULATE_FOOTPRINT_LIST_EVENT = NewEvent() UpdateSetting, EVT_UPDATE_SETTING = NewEvent() +LogboxAppendEvent, EVT_LOGBOX_APPEND_EVENT = NewEvent() diff --git a/mainwindow.py b/mainwindow.py index 9cfa916..7b5cc30 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -15,11 +15,13 @@ from .const import Column from .events import ( EVT_ASSIGN_PARTS_EVENT, + EVT_LOGBOX_APPEND_EVENT, EVT_MESSAGE_EVENT, EVT_POPULATE_FOOTPRINT_LIST_EVENT, EVT_RESET_GAUGE_EVENT, EVT_UPDATE_GAUGE_EVENT, EVT_UPDATE_SETTING, + LogboxAppendEvent, ) from .fabrication import Fabrication from .helpers import ( @@ -501,6 +503,7 @@ def __init__(self, parent, kicad_provider=KicadProvider()): self.Bind(EVT_ASSIGN_PARTS_EVENT, self.assign_parts) self.Bind(EVT_POPULATE_FOOTPRINT_LIST_EVENT, self.populate_footprint_list) self.Bind(EVT_UPDATE_SETTING, self.update_settings) + self.Bind(EVT_LOGBOX_APPEND_EVENT, self.logbox_append) self.enable_part_specific_toolbar_buttons(False) @@ -897,6 +900,10 @@ def update_settings(self, e): self.settings[e.section][e.setting] = e.value self.save_settings() + def logbox_append(self, e): + """Write text to the logbox.""" + self.logbox.WriteText(e.msg) + def load_settings(self): """Load settings from settings.json.""" with open(os.path.join(PLUGIN_PATH, "settings.json"), encoding="utf-8") as j: @@ -1092,7 +1099,7 @@ def init_logger(self): handler1 = logging.StreamHandler(sys.stderr) handler1.setLevel(logging.DEBUG) # and to our GUI - handler2 = LogBoxHandler(self.logbox) + handler2 = LogBoxHandler(self.logbox, self) handler2.setLevel(logging.DEBUG) formatter = logging.Formatter( "%(asctime)s - %(levelname)s - %(funcName)s - %(message)s", @@ -1112,15 +1119,15 @@ def __del__(self): class LogBoxHandler(logging.StreamHandler): """Logging class for the logging textbox at th ebottom of the mainwindow.""" - def __init__(self, textctrl): + def __init__(self, textctrl, event_destination): logging.StreamHandler.__init__(self) self.textctrl = textctrl + self.event_destination = event_destination def emit(self, record): - """Pokemon exception that hopefully helps getting this working with threads.""" - try: - msg = self.format(record) - self.textctrl.WriteText(msg + "\n") - self.flush() - except: # pylint: disable=bare-except - pass + """Marshal the event over to the main thread.""" + msg = self.format(record) + wx.QueueEvent(self.event_destination, LogboxAppendEvent( + msg=f"{msg}\n" + ) + ) From 3770b841659f457e5b7b0032d7f1dc51826b6ea6 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 3 May 2024 10:10:52 -0400 Subject: [PATCH 05/16] Update README.md to change "Fit" to "Git" Line 142, step 2 said "Fit clone forked repo", corrected "Fit" to "Git" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a4cebe..d78cb78 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ This plugin makes use of a lot of icons from the excellent [Material Design Icon ## Development 1. Fork repo -2. Fit clone forked repo +2. Git clone forked repo 3. Install pre-commit `pip install pre-commit` 4. Setup pre-commit `pre-commit run` 5. Create feature branch `git switch -c my-awesome-feature` From a9f3036f4d555a9de9c241b9336917fb722c8b9c Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 3 May 2024 10:13:53 -0400 Subject: [PATCH 06/16] Update library.py fix spelling Fix spelling of "spurious" --- library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.py b/library.py index bbff9e0..cdfc130 100644 --- a/library.py +++ b/library.py @@ -429,7 +429,7 @@ def download(self): "Parts db is split into %s parts. Proceeding to download...", r.text ) cnt = int(r.text) - self.logger.debug("Removing any spurios old zip part files...") + self.logger.debug("Removing any spurious old zip part files...") for p in glob(str(Path(self.datadir) / (chunk_file_stub + "*"))): self.logger.debug("Removing %s.", p) os.unlink(p) From 8742c8c9441c69e916f76cc5e4bc39795a7453f7 Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Sun, 26 May 2024 21:29:48 -0400 Subject: [PATCH 07/16] LogBoxHandler - Remove unused textctrl reference, events are used now --- mainwindow.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mainwindow.py b/mainwindow.py index 7b5cc30..d41b60d 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -1099,7 +1099,7 @@ def init_logger(self): handler1 = logging.StreamHandler(sys.stderr) handler1.setLevel(logging.DEBUG) # and to our GUI - handler2 = LogBoxHandler(self.logbox, self) + handler2 = LogBoxHandler(self) handler2.setLevel(logging.DEBUG) formatter = logging.Formatter( "%(asctime)s - %(levelname)s - %(funcName)s - %(message)s", @@ -1119,9 +1119,8 @@ def __del__(self): class LogBoxHandler(logging.StreamHandler): """Logging class for the logging textbox at th ebottom of the mainwindow.""" - def __init__(self, textctrl, event_destination): + def __init__(self, event_destination): logging.StreamHandler.__init__(self) - self.textctrl = textctrl self.event_destination = event_destination def emit(self, record): From 88451336ea4a4af8e0bed12d333ac133e5c914c8 Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Sun, 26 May 2024 21:38:19 -0400 Subject: [PATCH 08/16] =?UTF-8?q?mainwindow=20-=20Fix=20=E2=80=98RuntimeEr?= =?UTF-8?q?ror:=20wrapped=20C/C++=20object=20of=20type=20JLCPCBTools=20has?= =?UTF-8?q?=20been=20deleted=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To reproduce: - Open kicad-jlcpcb-tools dialog - Close dialog - Re-open kicad-jlcpcb-tools dialog - Attempt to download the database (this causes log messages to be generated), see RuntimeError dialog pop up. Each time the mainwindow dialog is opened, init_logger() is called. init_logger() retrieves the root logger via a call to logging.getLogger(). Handlers are then attached to the root logger, one to log to stderr and one to log via the LogBoxHandler class to the wxWidgets textctrl log box at the bottom of the mainwindow dialog. Recently there was a change to use wxPython events to log messages, to fix an issue that was believed to be due to manipulation of the log output textctrl widget from the backgound thread that was downloading the database. This change removed a try/except that was discarding exceptions, as it was believed that this change fixed the cause of the exceptions by marshalling, via wx.queueEvent(), the log text to the main thread for addition to the log output textctrl widget. Root cause is that the Python instance persists for the duration of Kicad's execution. Thus calls to addHandler() to the root logger are cumulative. Each time you re-open the mainwindow the logging handlers are added. The previous handlers, which now refer to deleted instances of the mainwindow (class JLCPCBTools, see the error message in the commit subject), persist. Each subsequent call to Python's logging functions results in all of these loggers being called. Previously the try/except in each of these loggers was discarding the wxPython/wxWidgets errors but removing the try/except meant they were now unhandled. Fix the crash by calling removeHandler() in quit_dialog() so there are no old logger handlers with references to now deleted dialogs. Note that due to the try/except it is likely that this issue was latent in the plugin for months or years, at least since the addHandler() was called without removeHandler(). Special thanks to Aleksandr Shvartzkop took time to look at the code and almost immediately spotted the logger addHandler() calls without corresponding removeHandler() calls. This was the first step that led to debugging the issue. --- mainwindow.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/mainwindow.py b/mainwindow.py index d41b60d..c46cf4b 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -518,6 +518,10 @@ def __init__(self, parent, kicad_provider=KicadProvider()): def quit_dialog(self, *_): """Destroy dialog on close.""" + root = logging.getLogger() + root.removeHandler(self.logging_handler1) + root.removeHandler(self.logging_handler2) + self.Destroy() self.EndModal(0) @@ -1096,19 +1100,19 @@ def init_logger(self): root = logging.getLogger() root.setLevel(logging.DEBUG) # Log to stderr - handler1 = logging.StreamHandler(sys.stderr) - handler1.setLevel(logging.DEBUG) + self.logging_handler1 = logging.StreamHandler(sys.stderr) + self.logging_handler1.setLevel(logging.DEBUG) # and to our GUI - handler2 = LogBoxHandler(self) - handler2.setLevel(logging.DEBUG) + self.logging_handler2 = LogBoxHandler(self) + self.logging_handler2.setLevel(logging.DEBUG) formatter = logging.Formatter( "%(asctime)s - %(levelname)s - %(funcName)s - %(message)s", datefmt="%Y.%m.%d %H:%M:%S", ) - handler1.setFormatter(formatter) - handler2.setFormatter(formatter) - root.addHandler(handler1) - root.addHandler(handler2) + self.logging_handler1.setFormatter(formatter) + self.logging_handler2.setFormatter(formatter) + root.addHandler(self.logging_handler1) + root.addHandler(self.logging_handler2) self.logger = logging.getLogger(__name__) def __del__(self): From bca3b9e78e46e46c7f338e435ea5d6cb7c1949bb Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 7 Jun 2024 15:11:04 +0200 Subject: [PATCH 09/16] Fix issue #440 with KiKit Panels KiKit Panels have the same ref, C1 for example on all boards of a panel. In order to get all of them in the POS / BOM files we now iterate over all parts of the board, matching them with a ref from the database. --- fabrication.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/fabrication.py b/fabrication.py index 69d6f9d..158ed03 100644 --- a/fabrication.py +++ b/fabrication.py @@ -258,9 +258,10 @@ def generate_cpl(self): writer.writerow( ["Designator", "Val", "Package", "Mid X", "Mid Y", "Rotation", "Layer"] ) - for part in self.parent.store.read_pos_parts(): - fp = self.board.FindFootprintByReference(part[0]) - if get_exclude_from_pos(fp): + footprints = sorted(self.board.Footprints(), key = lambda x: x.GetReference()) + for fp in footprints: + part = self.parent.store.get_part(fp.GetReference()) + if part[6] == 1: # Exclude from POS continue if not add_without_lcsc and not part[3]: continue @@ -276,7 +277,7 @@ def generate_cpl(self): "top" if fp.GetLayer() == 0 else "bottom", ] ) - self.logger.info("Finished generating CPL file") + self.logger.info("Finished generating CPL file %s", os.path.join(self.outputdir, cplname)) def generate_bom(self): """Generate BOM file.""" @@ -289,7 +290,12 @@ def generate_bom(self): ) as csvfile: writer = csv.writer(csvfile, delimiter=",") writer.writerow(["Comment", "Designator", "Footprint", "LCSC"]) - for part in self.parent.store.read_bom_parts(): + footprints = sorted(self.board.Footprints(), key = lambda x: x.GetReference()) + for fp in footprints: + # for part in self.parent.store.read_bom_parts(): + part = self.parent.store.get_part(fp.GetReference()) + if part[5] == 1: # Exclude from BOM + continue if not add_without_lcsc and not part[3]: continue writer.writerow(part) From 068ad5dd360fcea238f5213d38864c90f487441c Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 7 Jun 2024 15:27:48 +0200 Subject: [PATCH 10/16] Add better log messages --- fabrication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fabrication.py b/fabrication.py index 158ed03..612e9e0 100644 --- a/fabrication.py +++ b/fabrication.py @@ -241,7 +241,7 @@ def zip_gerber_excellon(self): continue filePath = os.path.join(folderName, filename) zipfile.write(filePath, os.path.basename(filePath)) - self.logger.info("Finished generating ZIP file") + self.logger.info("Finished generating ZIP file %s", os.path.join(self.outputdir, zipname)) def generate_cpl(self): """Generate placement file (CPL).""" @@ -299,4 +299,4 @@ def generate_bom(self): if not add_without_lcsc and not part[3]: continue writer.writerow(part) - self.logger.info("Finished generating BOM file") + self.logger.info("Finished generating BOM file %s", os.path.join(self.outputdir, bomname)) From 5d497a5228437b4b703a32e1a89ba2de8f1dc3a0 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 7 Jun 2024 15:29:14 +0200 Subject: [PATCH 11/16] Make ruff happy --- fabrication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fabrication.py b/fabrication.py index 612e9e0..bc1aaa6 100644 --- a/fabrication.py +++ b/fabrication.py @@ -35,7 +35,6 @@ except ImportError: NO_DRILL_SHAPE = PCB_PLOT_PARAMS.NO_DRILL_SHAPE -from .helpers import get_exclude_from_pos class Fabrication: From 79d0d6839a2ccd8cb4c6782e420c9e82ec908947 Mon Sep 17 00:00:00 2001 From: Elias Bonauer Date: Fri, 14 Jun 2024 09:21:40 +0200 Subject: [PATCH 12/16] Revert changes made to BOM creation in #480 --- fabrication.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/fabrication.py b/fabrication.py index bc1aaa6..9d4e3b7 100644 --- a/fabrication.py +++ b/fabrication.py @@ -278,6 +278,7 @@ def generate_cpl(self): ) self.logger.info("Finished generating CPL file %s", os.path.join(self.outputdir, cplname)) + def generate_bom(self): """Generate BOM file.""" bomname = f"BOM-{Path(self.filename).stem}.csv" @@ -289,12 +290,7 @@ def generate_bom(self): ) as csvfile: writer = csv.writer(csvfile, delimiter=",") writer.writerow(["Comment", "Designator", "Footprint", "LCSC"]) - footprints = sorted(self.board.Footprints(), key = lambda x: x.GetReference()) - for fp in footprints: - # for part in self.parent.store.read_bom_parts(): - part = self.parent.store.get_part(fp.GetReference()) - if part[5] == 1: # Exclude from BOM - continue + for part in self.parent.store.read_bom_parts(): if not add_without_lcsc and not part[3]: continue writer.writerow(part) From 93ebf4dd03102f1964f4a3d230b320af408add77 Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 20 Jun 2024 15:56:39 +0200 Subject: [PATCH 13/16] Do not fail creating BOM if part is not found in database --- fabrication.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fabrication.py b/fabrication.py index 9d4e3b7..b98440e 100644 --- a/fabrication.py +++ b/fabrication.py @@ -260,6 +260,8 @@ def generate_cpl(self): footprints = sorted(self.board.Footprints(), key = lambda x: x.GetReference()) for fp in footprints: part = self.parent.store.get_part(fp.GetReference()) + if not part: # No matching part in the database, continue + continue if part[6] == 1: # Exclude from POS continue if not add_without_lcsc and not part[3]: From ee6ca505461ce3d2bb1a32a6af3eee100bb266dd Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 20 Jun 2024 16:08:50 +0200 Subject: [PATCH 14/16] Add debug message --- fabrication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fabrication.py b/fabrication.py index b98440e..db83b56 100644 --- a/fabrication.py +++ b/fabrication.py @@ -261,6 +261,7 @@ def generate_cpl(self): for fp in footprints: part = self.parent.store.get_part(fp.GetReference()) if not part: # No matching part in the database, continue + self.logger.debug("No matching database entry found for %s", fp.GetReference()) continue if part[6] == 1: # Exclude from POS continue From a70c548ff7a00efdb9e7cb7e44161261acccc5d4 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 21 Jun 2024 09:04:23 +0200 Subject: [PATCH 15/16] Remove debug message --- fabrication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fabrication.py b/fabrication.py index db83b56..b98440e 100644 --- a/fabrication.py +++ b/fabrication.py @@ -261,7 +261,6 @@ def generate_cpl(self): for fp in footprints: part = self.parent.store.get_part(fp.GetReference()) if not part: # No matching part in the database, continue - self.logger.debug("No matching database entry found for %s", fp.GetReference()) continue if part[6] == 1: # Exclude from POS continue From 50baeebfcfa79727f2ab7248e5efb6e32abcd2eb Mon Sep 17 00:00:00 2001 From: Elias Bonauer Date: Wed, 3 Jul 2024 09:40:25 +0200 Subject: [PATCH 16/16] Scale right hand toolbar according to DPI settings --- mainwindow.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mainwindow.py b/mainwindow.py index c46cf4b..d9328bc 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -67,6 +67,7 @@ ID_CONTEXT_MENU_ADD_ROT_BY_PACKAGE = wx.NewIdRef() ID_CONTEXT_MENU_ADD_ROT_BY_NAME = wx.NewIdRef() + class KicadProvider: """KiCad implementation of the provider, see standalone_impl.py for the stub version.""" @@ -74,6 +75,7 @@ def get_pcbnew(self): """Get the pcbnew instance.""" return kicad_pcbnew + class JLCPCBTools(wx.Dialog): """JLCPCBTools main dialog.""" @@ -220,7 +222,7 @@ def __init__(self, parent, kicad_provider=KicadProvider()): self, wx.ID_ANY, wx.DefaultPosition, - wx.Size(128, -1), + wx.Size(int(self.scale_factor * 128), -1), wx.TB_VERTICAL | wx.TB_TEXT | wx.TB_NODIVIDER, ) @@ -1130,7 +1132,4 @@ def __init__(self, event_destination): def emit(self, record): """Marshal the event over to the main thread.""" msg = self.format(record) - wx.QueueEvent(self.event_destination, LogboxAppendEvent( - msg=f"{msg}\n" - ) - ) + wx.QueueEvent(self.event_destination, LogboxAppendEvent(msg=f"{msg}\n"))