From 771988a6a61d55eab88803ea51d4e240bb1f8d25 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 28 Jan 2026 18:14:34 -0800 Subject: [PATCH 1/4] Enable Pyrefly --- pyproject.toml | 3 +++ synodic_client/application/qt.py | 5 ++++- synodic_client/updater.py | 27 +++++++++++++++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 54e7ddb..e961d6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,9 @@ quote-style = "single" [tool.coverage.report] skip_empty = true +[tool.pyrefly] +search_path = ["synodic_client/..."] + [tool.pdm.version] source = "scm" diff --git a/synodic_client/application/qt.py b/synodic_client/application/qt.py index 566a0f5..1df55c8 100644 --- a/synodic_client/application/qt.py +++ b/synodic_client/application/qt.py @@ -44,7 +44,10 @@ def application() -> None: screen = Screen() - app.tray = TrayScreen(app, client, icon, screen.window) + # Store tray screen as instance attribute using object.__setattr__ + # to avoid type checking issues with dynamic attributes + tray_screen = TrayScreen(app, client, icon, screen.window) + object.__setattr__(app, 'tray', tray_screen) app.exec_() diff --git a/synodic_client/updater.py b/synodic_client/updater.py index e5bb809..3e3f470 100644 --- a/synodic_client/updater.py +++ b/synodic_client/updater.py @@ -133,7 +133,7 @@ def check_for_update(self) -> UpdateInfo: result = self._porringer.update.check(params) if result.available and result.latest_version: - latest = Version(result.latest_version) + latest = Version(str(result.latest_version)) self._update_info = UpdateInfo( available=True, current_version=self._current_version, @@ -239,7 +239,8 @@ def apply_update(self) -> bool: except Exception as e: logger.exception('Failed to apply update') self._state = UpdateState.ROLLBACK_REQUIRED - self._update_info.error = str(e) + if self._update_info is not None: + self._update_info.error = str(e) return False def rollback(self) -> bool: @@ -341,9 +342,13 @@ def _get_bundled_root_metadata(self) -> Path | None: Path to root.json if bundled, None otherwise """ if self.is_frozen: - # PyInstaller bundle - bundle_dir = Path(sys._MEIPASS) # type: ignore[attr-defined] - root_path = bundle_dir / 'data' / 'tuf_root.json' + # PyInstaller bundle - _MEIPASS is set by PyInstaller at runtime + meipass = getattr(sys, '_MEIPASS', None) + if meipass is not None: + bundle_dir = Path(meipass) + root_path = bundle_dir / 'data' / 'tuf_root.json' + else: + return None else: # Development mode root_path = Path(__file__).parent.parent / 'data' / 'tuf_root.json' @@ -401,6 +406,10 @@ def _apply_frozen_update(self) -> bool: backup_path = self._get_backup_path() new_exe = self._downloaded_path + if new_exe is None: + logger.error('No downloaded executable found') + return False + # Create backup of current executable logger.info('Creating backup: %s -> %s', current_exe, backup_path) shutil.copy2(current_exe, backup_path) @@ -448,9 +457,15 @@ def _apply_windows_update(self, current_exe: Path, new_exe: Path, backup_path: P script_path.write_text(script_content) # Schedule the script to run + # Windows-specific process creation flags + flags = 0 + if sys.platform == 'win32': + # CREATE_NEW_CONSOLE = 0x00000200, DETACHED_PROCESS = 0x00000008 + flags = 0x00000200 | 0x00000008 + subprocess.Popen( ['cmd', '/c', str(script_path)], - creationflags=subprocess.CREATE_NEW_CONSOLE | subprocess.DETACHED_PROCESS, + creationflags=flags, ) self._state = UpdateState.APPLIED From 0016f5a214287c7b6df57ea6420a477dda997cea Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 28 Jan 2026 18:16:27 -0800 Subject: [PATCH 2/4] Add Spec File --- .gitignore | 2 ++ tool/pyinstaller/synodic.spec | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tool/pyinstaller/synodic.spec diff --git a/.gitignore b/.gitignore index 5394dee..4a42255 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ MANIFEST # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec +# Exception: track our custom spec file +!tool/pyinstaller/synodic.spec # Installer logs pip-log.txt diff --git a/tool/pyinstaller/synodic.spec b/tool/pyinstaller/synodic.spec new file mode 100644 index 0000000..afc9243 --- /dev/null +++ b/tool/pyinstaller/synodic.spec @@ -0,0 +1,52 @@ +# -*- mode: python ; coding: utf-8 -*- + +from PyInstaller.utils.hooks import collect_all, copy_metadata + +# Collect porringer and its plugins with metadata +datas = [('../../data', 'data')] +hiddenimports = [] + +# Add porringer metadata so entry points work +datas += copy_metadata('porringer') + +# Add TUF metadata for secure updates +datas += copy_metadata('tuf') + +# Add your plugin packages here as you add them to dependencies +# Example: datas += copy_metadata('porringer-plugin-name') +# Example: hiddenimports += ['porringer_plugin_name'] + +a = Analysis( + ['../../synodic_client/application/qt.py'], + pathex=[], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='synodic', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) From 4f73a2b5d681ef7099cd0f0eb46e0e70d3a4b8c8 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 28 Jan 2026 18:22:57 -0800 Subject: [PATCH 3/4] Lifetime fixes --- synodic_client/application/qt.py | 12 +++++------- synodic_client/updater.py | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/synodic_client/application/qt.py b/synodic_client/application/qt.py index 1df55c8..b17f3aa 100644 --- a/synodic_client/application/qt.py +++ b/synodic_client/application/qt.py @@ -42,14 +42,12 @@ def application() -> None: app = QApplication([]) app.setQuitOnLastWindowClosed(False) - screen = Screen() + _screen = Screen() + _tray = TrayScreen(app, client, icon, _screen.window) - # Store tray screen as instance attribute using object.__setattr__ - # to avoid type checking issues with dynamic attributes - tray_screen = TrayScreen(app, client, icon, screen.window) - object.__setattr__(app, 'tray', tray_screen) - - app.exec_() + # sys.exit ensures proper cleanup and exit code propagation + # Leading underscore indicates references kept alive intentionally until exec() returns + sys.exit(app.exec()) if __name__ == '__main__': diff --git a/synodic_client/updater.py b/synodic_client/updater.py index 3e3f470..caca249 100644 --- a/synodic_client/updater.py +++ b/synodic_client/updater.py @@ -462,7 +462,7 @@ def _apply_windows_update(self, current_exe: Path, new_exe: Path, backup_path: P if sys.platform == 'win32': # CREATE_NEW_CONSOLE = 0x00000200, DETACHED_PROCESS = 0x00000008 flags = 0x00000200 | 0x00000008 - + subprocess.Popen( ['cmd', '/c', str(script_path)], creationflags=flags, From 448d5c3cb42e2822782e1cbb3e735f9f5d8dbed5 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 28 Jan 2026 19:08:54 -0800 Subject: [PATCH 4/4] Performance Improvements --- synodic_client/application/qt.py | 4 ++++ synodic_client/application/screen/screen.py | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/synodic_client/application/qt.py b/synodic_client/application/qt.py index b17f3aa..0a6b8d7 100644 --- a/synodic_client/application/qt.py +++ b/synodic_client/application/qt.py @@ -6,6 +6,7 @@ from porringer.api import API, APIParameters from porringer.schema import ListPluginsParameters, LocalConfiguration +from PySide6.QtCore import Qt from PySide6.QtWidgets import QApplication from synodic_client.application.screen.screen import Screen @@ -42,6 +43,9 @@ def application() -> None: app = QApplication([]) app.setQuitOnLastWindowClosed(False) + # Reduce CPU usage when idle - process events less aggressively + app.setAttribute(Qt.ApplicationAttribute.AA_CompressHighFrequencyEvents) + _screen = Screen() _tray = TrayScreen(app, client, icon, _screen.window) diff --git a/synodic_client/application/screen/screen.py b/synodic_client/application/screen/screen.py index 92cd4a2..f2a7e9a 100644 --- a/synodic_client/application/screen/screen.py +++ b/synodic_client/application/screen/screen.py @@ -9,13 +9,26 @@ class MainWindow(QMainWindow): def __init__(self) -> None: """Initialize the main window.""" super().__init__() + self.setWindowTitle('Synodic Client') + + def show(self) -> None: + """Show the window, initializing UI lazily on first show.""" + # Future: Initialize heavy UI components here on first show + super().show() class Screen: """Screen class for the Synodic Client application.""" - def __init__(self): - """Initialize the screen.""" - self.window = MainWindow() + _window: MainWindow | None = None + + @property + def window(self) -> MainWindow: + """Lazily create the main window on first access. - self.window.setWindowTitle('Synodic Client') + Returns: + The MainWindow instance. + """ + if self._window is None: + self._window = MainWindow() + return self._window