synodic-client 0.0.1.dev32__tar.gz → 0.0.1.dev33__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/pyproject.toml +1 -1
- synodic_client-0.0.1.dev33/synodic_client/_version.py +1 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/settings.py +30 -2
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/tray.py +68 -171
- synodic_client-0.0.1.dev33/synodic_client/application/workers.py +112 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/updater.py +1 -1
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/qt/test_settings.py +48 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_updater.py +23 -20
- synodic_client-0.0.1.dev32/synodic_client/_version.py +0 -1
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/README.md +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/windows/test_startup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.0.1.dev33'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Settings window for the Synodic Client application.
|
|
2
2
|
|
|
3
3
|
Provides a single-page window with grouped sections for all application
|
|
4
|
-
settings
|
|
5
|
-
|
|
4
|
+
settings including update-channel selection and a manual *Check for
|
|
5
|
+
Updates* button with inline status feedback.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
@@ -52,6 +52,9 @@ class SettingsWindow(QMainWindow):
|
|
|
52
52
|
settings_changed = Signal()
|
|
53
53
|
"""Emitted whenever a setting is changed and persisted."""
|
|
54
54
|
|
|
55
|
+
check_updates_requested = Signal()
|
|
56
|
+
"""Emitted when the user clicks the *Check for Updates* button."""
|
|
57
|
+
|
|
55
58
|
def __init__(
|
|
56
59
|
self,
|
|
57
60
|
config: GlobalConfiguration,
|
|
@@ -155,6 +158,16 @@ class SettingsWindow(QMainWindow):
|
|
|
155
158
|
self._detect_updates_check.toggled.connect(self._on_detect_updates_changed)
|
|
156
159
|
content.addWidget(self._detect_updates_check)
|
|
157
160
|
|
|
161
|
+
# Check for Updates
|
|
162
|
+
row = QHBoxLayout()
|
|
163
|
+
self._check_updates_btn = QPushButton('Check for Updates\u2026')
|
|
164
|
+
self._check_updates_btn.clicked.connect(self._on_check_updates_clicked)
|
|
165
|
+
row.addWidget(self._check_updates_btn)
|
|
166
|
+
self._update_status_label = QLabel('')
|
|
167
|
+
row.addWidget(self._update_status_label)
|
|
168
|
+
row.addStretch()
|
|
169
|
+
content.addLayout(row)
|
|
170
|
+
|
|
158
171
|
return card
|
|
159
172
|
|
|
160
173
|
def _build_startup_section(self) -> CardFrame:
|
|
@@ -209,6 +222,14 @@ class SettingsWindow(QMainWindow):
|
|
|
209
222
|
self._detect_updates_check.setChecked(config.detect_updates)
|
|
210
223
|
self._auto_start_check.setChecked(is_startup_registered())
|
|
211
224
|
|
|
225
|
+
def set_update_status(self, text: str) -> None:
|
|
226
|
+
"""Set the inline status text next to the *Check for Updates* button."""
|
|
227
|
+
self._update_status_label.setText(text)
|
|
228
|
+
|
|
229
|
+
def reset_check_updates_button(self) -> None:
|
|
230
|
+
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
231
|
+
self._check_updates_btn.setEnabled(True)
|
|
232
|
+
|
|
212
233
|
def show(self) -> None:
|
|
213
234
|
"""Sync controls from config, then show the window."""
|
|
214
235
|
self.sync_from_config()
|
|
@@ -235,6 +256,7 @@ class SettingsWindow(QMainWindow):
|
|
|
235
256
|
self._tool_update_spin,
|
|
236
257
|
self._detect_updates_check,
|
|
237
258
|
self._auto_start_check,
|
|
259
|
+
self._check_updates_btn,
|
|
238
260
|
)
|
|
239
261
|
for w in widgets:
|
|
240
262
|
w.blockSignals(True)
|
|
@@ -244,6 +266,12 @@ class SettingsWindow(QMainWindow):
|
|
|
244
266
|
for w in widgets:
|
|
245
267
|
w.blockSignals(False)
|
|
246
268
|
|
|
269
|
+
def _on_check_updates_clicked(self) -> None:
|
|
270
|
+
"""Handle the *Check for Updates* button click."""
|
|
271
|
+
self._check_updates_btn.setEnabled(False)
|
|
272
|
+
self._update_status_label.setText('Checking\u2026')
|
|
273
|
+
self.check_updates_requested.emit()
|
|
274
|
+
|
|
247
275
|
def _on_channel_changed(self, index: int) -> None:
|
|
248
276
|
self._config.update_channel = 'dev' if index == 1 else 'stable'
|
|
249
277
|
self._persist()
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Callable
|
|
6
6
|
|
|
7
7
|
from porringer.api import API
|
|
8
|
-
from
|
|
9
|
-
from PySide6.QtCore import QThread, QTimer, Signal
|
|
8
|
+
from PySide6.QtCore import QThread, QTimer
|
|
10
9
|
from PySide6.QtGui import QAction
|
|
11
10
|
from PySide6.QtWidgets import (
|
|
12
11
|
QApplication,
|
|
@@ -19,6 +18,7 @@ from PySide6.QtWidgets import (
|
|
|
19
18
|
from synodic_client.application.icon import app_icon
|
|
20
19
|
from synodic_client.application.screen.screen import MainWindow
|
|
21
20
|
from synodic_client.application.screen.settings import SettingsWindow
|
|
21
|
+
from synodic_client.application.workers import ToolUpdateWorker, UpdateCheckWorker, UpdateDownloadWorker
|
|
22
22
|
from synodic_client.client import Client
|
|
23
23
|
from synodic_client.config import GlobalConfiguration
|
|
24
24
|
from synodic_client.resolution import (
|
|
@@ -27,106 +27,11 @@ from synodic_client.resolution import (
|
|
|
27
27
|
resolve_update_config,
|
|
28
28
|
update_and_resolve,
|
|
29
29
|
)
|
|
30
|
-
from synodic_client.updater import
|
|
30
|
+
from synodic_client.updater import UpdateInfo
|
|
31
31
|
|
|
32
32
|
logger = logging.getLogger(__name__)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
class UpdateCheckWorker(QThread):
|
|
36
|
-
"""Worker for checking updates in a background thread."""
|
|
37
|
-
|
|
38
|
-
finished = Signal(object) # UpdateInfo
|
|
39
|
-
error = Signal(str)
|
|
40
|
-
|
|
41
|
-
def __init__(self, client: Client) -> None:
|
|
42
|
-
"""Initialize the worker."""
|
|
43
|
-
super().__init__()
|
|
44
|
-
self._client = client
|
|
45
|
-
|
|
46
|
-
def run(self) -> None:
|
|
47
|
-
"""Run the update check."""
|
|
48
|
-
try:
|
|
49
|
-
result = self._client.check_for_update()
|
|
50
|
-
self.finished.emit(result)
|
|
51
|
-
except Exception as e:
|
|
52
|
-
logger.exception('Update check failed')
|
|
53
|
-
self.error.emit(str(e))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class UpdateDownloadWorker(QThread):
|
|
57
|
-
"""Worker for downloading updates in a background thread."""
|
|
58
|
-
|
|
59
|
-
finished = Signal(bool) # success status
|
|
60
|
-
progress = Signal(int) # percentage (0-100)
|
|
61
|
-
error = Signal(str)
|
|
62
|
-
|
|
63
|
-
def __init__(self, client: Client) -> None:
|
|
64
|
-
"""Initialize the worker."""
|
|
65
|
-
super().__init__()
|
|
66
|
-
self._client = client
|
|
67
|
-
|
|
68
|
-
def run(self) -> None:
|
|
69
|
-
"""Run the update download."""
|
|
70
|
-
try:
|
|
71
|
-
|
|
72
|
-
def progress_callback(percentage: int) -> None:
|
|
73
|
-
self.progress.emit(percentage)
|
|
74
|
-
|
|
75
|
-
success = self._client.download_update(progress_callback)
|
|
76
|
-
self.finished.emit(success)
|
|
77
|
-
except Exception as e:
|
|
78
|
-
logger.exception('Update download failed')
|
|
79
|
-
self.error.emit(str(e))
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class ToolUpdateWorker(QThread):
|
|
83
|
-
"""Worker for re-syncing manifest-declared tools in a background thread."""
|
|
84
|
-
|
|
85
|
-
finished = Signal(int) # number of manifests processed
|
|
86
|
-
error = Signal(str)
|
|
87
|
-
|
|
88
|
-
def __init__(self, porringer: API, plugins: list[str] | None = None) -> None:
|
|
89
|
-
"""Initialize the worker.
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
porringer: The porringer API instance.
|
|
93
|
-
plugins: Optional include-list of plugin names. When set, only
|
|
94
|
-
actions handled by these plugins are executed. ``None``
|
|
95
|
-
means all plugins.
|
|
96
|
-
"""
|
|
97
|
-
super().__init__()
|
|
98
|
-
self._porringer = porringer
|
|
99
|
-
self._plugins = plugins
|
|
100
|
-
|
|
101
|
-
def run(self) -> None:
|
|
102
|
-
"""Re-sync all cached project manifests."""
|
|
103
|
-
try:
|
|
104
|
-
directories = self._porringer.cache.list_directories()
|
|
105
|
-
count = 0
|
|
106
|
-
for directory in directories:
|
|
107
|
-
path = Path(directory.path)
|
|
108
|
-
if not self._porringer.sync.has_manifest(path):
|
|
109
|
-
logger.debug('Skipping path without manifest: %s', path)
|
|
110
|
-
continue
|
|
111
|
-
params = SetupParameters(
|
|
112
|
-
paths=[path],
|
|
113
|
-
project_directory=path if path.is_dir() else None,
|
|
114
|
-
strategy=SyncStrategy.LATEST,
|
|
115
|
-
plugins=self._plugins,
|
|
116
|
-
)
|
|
117
|
-
asyncio.run(self._sync(params))
|
|
118
|
-
count += 1
|
|
119
|
-
self.finished.emit(count)
|
|
120
|
-
except Exception as e:
|
|
121
|
-
logger.exception('Tool update failed')
|
|
122
|
-
self.error.emit(str(e))
|
|
123
|
-
|
|
124
|
-
async def _sync(self, params: SetupParameters) -> None:
|
|
125
|
-
"""Execute a sync stream for the given parameters."""
|
|
126
|
-
async for _event in self._porringer.sync.execute_stream(params):
|
|
127
|
-
pass # consume events to completion
|
|
128
|
-
|
|
129
|
-
|
|
130
35
|
class TrayScreen:
|
|
131
36
|
"""Tray screen for the application."""
|
|
132
37
|
|
|
@@ -169,17 +74,18 @@ class TrayScreen:
|
|
|
169
74
|
# Settings window (created once, shown/hidden on demand)
|
|
170
75
|
self._settings_window = SettingsWindow(self._resolve_config())
|
|
171
76
|
self._settings_window.settings_changed.connect(self._on_settings_changed)
|
|
77
|
+
self._settings_window.check_updates_requested.connect(self._on_check_updates)
|
|
172
78
|
|
|
173
79
|
# MainWindow gear button → open settings
|
|
174
80
|
window.settings_requested.connect(self._show_settings)
|
|
175
81
|
|
|
176
82
|
# Periodic auto-update checking
|
|
177
83
|
self._auto_update_timer: QTimer | None = None
|
|
178
|
-
self.
|
|
84
|
+
self._restart_auto_update_timer()
|
|
179
85
|
|
|
180
86
|
# Periodic tool update checking
|
|
181
87
|
self._tool_update_timer: QTimer | None = None
|
|
182
|
-
self.
|
|
88
|
+
self._restart_tool_update_timer()
|
|
183
89
|
|
|
184
90
|
# Connect PluginsView signals when available
|
|
185
91
|
plugins_view = window.plugins_view
|
|
@@ -201,23 +107,6 @@ class TrayScreen:
|
|
|
201
107
|
self.update_action.triggered.connect(self._on_check_updates)
|
|
202
108
|
self.menu.addAction(self.update_action)
|
|
203
109
|
|
|
204
|
-
# Update Channel submenu
|
|
205
|
-
self.channel_menu = QMenu('Update Channel', self.menu)
|
|
206
|
-
self.menu.addMenu(self.channel_menu)
|
|
207
|
-
|
|
208
|
-
self._channel_stable_action = QAction('Stable', self.channel_menu)
|
|
209
|
-
self._channel_stable_action.setCheckable(True)
|
|
210
|
-
self._channel_stable_action.triggered.connect(lambda: self._on_channel_changed(UpdateChannel.STABLE))
|
|
211
|
-
self.channel_menu.addAction(self._channel_stable_action)
|
|
212
|
-
|
|
213
|
-
self._channel_dev_action = QAction('Development', self.channel_menu)
|
|
214
|
-
self._channel_dev_action.setCheckable(True)
|
|
215
|
-
self._channel_dev_action.triggered.connect(lambda: self._on_channel_changed(UpdateChannel.DEVELOPMENT))
|
|
216
|
-
self.channel_menu.addAction(self._channel_dev_action)
|
|
217
|
-
|
|
218
|
-
# Set initial channel check state from config
|
|
219
|
-
self._sync_channel_checks()
|
|
220
|
-
|
|
221
110
|
self.menu.addSeparator()
|
|
222
111
|
|
|
223
112
|
self.settings_action = QAction('Settings\u2026', self.menu)
|
|
@@ -240,50 +129,57 @@ class TrayScreen:
|
|
|
240
129
|
return self._config
|
|
241
130
|
return resolve_config()
|
|
242
131
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if interval_minutes <= 0:
|
|
252
|
-
logger.info('Automatic update checking is disabled')
|
|
253
|
-
return
|
|
132
|
+
def _restart_timer(
|
|
133
|
+
self,
|
|
134
|
+
current: QTimer | None,
|
|
135
|
+
interval_minutes: int,
|
|
136
|
+
slot: Callable[[], None],
|
|
137
|
+
label: str,
|
|
138
|
+
) -> QTimer | None:
|
|
139
|
+
"""Stop *current* and return a new periodic timer, or ``None``.
|
|
254
140
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
logger.info('Automatic update checking enabled (every %d minute(s))', interval_minutes)
|
|
141
|
+
Args:
|
|
142
|
+
current: The existing timer to stop (may be ``None``).
|
|
143
|
+
interval_minutes: Interval in minutes. ``0`` disables.
|
|
144
|
+
slot: The callable to invoke on each tick.
|
|
145
|
+
label: Human-readable name for log messages.
|
|
261
146
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
147
|
+
Returns:
|
|
148
|
+
A running ``QTimer``, or ``None`` when disabled.
|
|
149
|
+
"""
|
|
150
|
+
if current is not None:
|
|
151
|
+
current.stop()
|
|
267
152
|
|
|
268
|
-
config = resolve_update_config(self._resolve_config())
|
|
269
|
-
interval_minutes = config.tool_update_interval_minutes
|
|
270
153
|
if interval_minutes <= 0:
|
|
271
|
-
logger.info('
|
|
272
|
-
return
|
|
154
|
+
logger.info('%s is disabled', label)
|
|
155
|
+
return None
|
|
273
156
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
157
|
+
timer = QTimer()
|
|
158
|
+
timer.setInterval(interval_minutes * 60 * 1000)
|
|
159
|
+
timer.timeout.connect(slot)
|
|
160
|
+
timer.start()
|
|
161
|
+
logger.info('%s enabled (every %d minute(s))', label, interval_minutes)
|
|
162
|
+
return timer
|
|
280
163
|
|
|
281
|
-
def
|
|
282
|
-
"""
|
|
283
|
-
config = self._resolve_config()
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
164
|
+
def _restart_auto_update_timer(self) -> None:
|
|
165
|
+
"""Start (or restart) the periodic auto-update timer from config."""
|
|
166
|
+
config = resolve_update_config(self._resolve_config())
|
|
167
|
+
self._auto_update_timer = self._restart_timer(
|
|
168
|
+
self._auto_update_timer,
|
|
169
|
+
config.auto_update_interval_minutes,
|
|
170
|
+
self._on_auto_check_updates,
|
|
171
|
+
'Automatic update checking',
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _restart_tool_update_timer(self) -> None:
|
|
175
|
+
"""Start (or restart) the periodic tool update timer from config."""
|
|
176
|
+
config = resolve_update_config(self._resolve_config())
|
|
177
|
+
self._tool_update_timer = self._restart_timer(
|
|
178
|
+
self._tool_update_timer,
|
|
179
|
+
config.tool_update_interval_minutes,
|
|
180
|
+
self._on_tool_update,
|
|
181
|
+
'Automatic tool updating',
|
|
182
|
+
)
|
|
287
183
|
|
|
288
184
|
def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason) -> None:
|
|
289
185
|
"""Handle tray icon activation (e.g. double-click)."""
|
|
@@ -300,25 +196,13 @@ class TrayScreen:
|
|
|
300
196
|
"""React to a change made in the settings window."""
|
|
301
197
|
config = self._resolve_config()
|
|
302
198
|
self._reinitialize_updater(config)
|
|
303
|
-
self._sync_channel_checks()
|
|
304
|
-
|
|
305
|
-
def _on_channel_changed(self, channel: UpdateChannel) -> None:
|
|
306
|
-
"""Handle channel selection change from the tray submenu."""
|
|
307
|
-
config = self._resolve_config()
|
|
308
|
-
config.update_channel = 'dev' if channel == UpdateChannel.DEVELOPMENT else 'stable'
|
|
309
|
-
logger.info('Update channel changed to: %s', config.update_channel)
|
|
310
|
-
self._sync_channel_checks()
|
|
311
|
-
self._reinitialize_updater(config)
|
|
312
|
-
# Keep the settings window in sync if it is visible
|
|
313
|
-
if self._settings_window.isVisible():
|
|
314
|
-
self._settings_window.sync_from_config()
|
|
315
199
|
|
|
316
200
|
def _reinitialize_updater(self, config: GlobalConfiguration) -> None:
|
|
317
201
|
"""Re-derive update settings and restart the updater and timers."""
|
|
318
202
|
update_cfg = update_and_resolve(config)
|
|
319
203
|
self._client.initialize_updater(update_cfg)
|
|
320
|
-
self.
|
|
321
|
-
self.
|
|
204
|
+
self._restart_auto_update_timer()
|
|
205
|
+
self._restart_tool_update_timer()
|
|
322
206
|
logger.info('Updater re-initialized (channel: %s, source: %s)', update_cfg.channel.name, update_cfg.repo_url)
|
|
323
207
|
|
|
324
208
|
def _reset_update_action(self) -> None:
|
|
@@ -361,9 +245,11 @@ class TrayScreen:
|
|
|
361
245
|
)
|
|
362
246
|
return
|
|
363
247
|
|
|
364
|
-
# Disable the action while checking
|
|
248
|
+
# Disable both the tray action and the settings button while checking
|
|
365
249
|
self.update_action.setEnabled(False)
|
|
366
250
|
self.update_action.setText('Checking for Updates...')
|
|
251
|
+
self._settings_window._check_updates_btn.setEnabled(False)
|
|
252
|
+
self._settings_window.set_update_status('Checking\u2026')
|
|
367
253
|
|
|
368
254
|
worker = UpdateCheckWorker(self._client)
|
|
369
255
|
worker.finished.connect(lambda result: self._on_update_check_finished(result, silent=silent))
|
|
@@ -375,8 +261,10 @@ class TrayScreen:
|
|
|
375
261
|
def _on_update_check_finished(self, result: UpdateInfo | None, *, silent: bool = False) -> None:
|
|
376
262
|
"""Handle update check completion."""
|
|
377
263
|
self._reset_update_action()
|
|
264
|
+
self._settings_window.reset_check_updates_button()
|
|
378
265
|
|
|
379
266
|
if result is None:
|
|
267
|
+
self._settings_window.set_update_status('Check failed')
|
|
380
268
|
if not silent:
|
|
381
269
|
self.tray.showMessage(
|
|
382
270
|
'Update Check Failed',
|
|
@@ -388,6 +276,7 @@ class TrayScreen:
|
|
|
388
276
|
return
|
|
389
277
|
|
|
390
278
|
if result.error:
|
|
279
|
+
self._settings_window.set_update_status(result.error)
|
|
391
280
|
if not silent:
|
|
392
281
|
# Distinguish informational messages (no releases for channel)
|
|
393
282
|
# from genuine failures.
|
|
@@ -402,6 +291,9 @@ class TrayScreen:
|
|
|
402
291
|
return
|
|
403
292
|
|
|
404
293
|
if not result.available:
|
|
294
|
+
self._settings_window.set_update_status(
|
|
295
|
+
f'Up to date ({result.current_version})',
|
|
296
|
+
)
|
|
405
297
|
if not silent:
|
|
406
298
|
self.tray.showMessage(
|
|
407
299
|
'No Updates Available',
|
|
@@ -414,6 +306,9 @@ class TrayScreen:
|
|
|
414
306
|
|
|
415
307
|
# Update available - always show notification, clicking it starts download
|
|
416
308
|
self._pending_update_info = result
|
|
309
|
+
self._settings_window.set_update_status(
|
|
310
|
+
f'Update available: {result.latest_version}',
|
|
311
|
+
)
|
|
417
312
|
self.tray.showMessage(
|
|
418
313
|
'Update Available',
|
|
419
314
|
f'Version {result.latest_version} is available (current: {result.current_version}).\nClick to download.',
|
|
@@ -423,6 +318,8 @@ class TrayScreen:
|
|
|
423
318
|
def _on_update_check_error(self, error: str, *, silent: bool = False) -> None:
|
|
424
319
|
"""Handle update check error."""
|
|
425
320
|
self._reset_update_action()
|
|
321
|
+
self._settings_window.reset_check_updates_button()
|
|
322
|
+
self._settings_window.set_update_status(f'Error: {error}')
|
|
426
323
|
|
|
427
324
|
if not silent:
|
|
428
325
|
self.tray.showMessage(
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Background worker threads for the Synodic Client application.
|
|
2
|
+
|
|
3
|
+
Each worker wraps an off-main-thread operation and communicates results
|
|
4
|
+
back via Qt signals so that callers remain responsive.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from porringer.api import API
|
|
12
|
+
from porringer.schema import SetupParameters, SyncStrategy
|
|
13
|
+
from PySide6.QtCore import QThread, Signal
|
|
14
|
+
|
|
15
|
+
from synodic_client.client import Client
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UpdateCheckWorker(QThread):
|
|
21
|
+
"""Worker for checking updates in a background thread."""
|
|
22
|
+
|
|
23
|
+
finished = Signal(object) # UpdateInfo
|
|
24
|
+
error = Signal(str)
|
|
25
|
+
|
|
26
|
+
def __init__(self, client: Client) -> None:
|
|
27
|
+
"""Initialize the worker."""
|
|
28
|
+
super().__init__()
|
|
29
|
+
self._client = client
|
|
30
|
+
|
|
31
|
+
def run(self) -> None:
|
|
32
|
+
"""Run the update check."""
|
|
33
|
+
try:
|
|
34
|
+
result = self._client.check_for_update()
|
|
35
|
+
self.finished.emit(result)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.exception('Update check failed')
|
|
38
|
+
self.error.emit(str(e))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UpdateDownloadWorker(QThread):
|
|
42
|
+
"""Worker for downloading updates in a background thread."""
|
|
43
|
+
|
|
44
|
+
finished = Signal(bool) # success status
|
|
45
|
+
progress = Signal(int) # percentage (0-100)
|
|
46
|
+
error = Signal(str)
|
|
47
|
+
|
|
48
|
+
def __init__(self, client: Client) -> None:
|
|
49
|
+
"""Initialize the worker."""
|
|
50
|
+
super().__init__()
|
|
51
|
+
self._client = client
|
|
52
|
+
|
|
53
|
+
def run(self) -> None:
|
|
54
|
+
"""Run the update download."""
|
|
55
|
+
try:
|
|
56
|
+
|
|
57
|
+
def progress_callback(percentage: int) -> None:
|
|
58
|
+
self.progress.emit(percentage)
|
|
59
|
+
|
|
60
|
+
success = self._client.download_update(progress_callback)
|
|
61
|
+
self.finished.emit(success)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.exception('Update download failed')
|
|
64
|
+
self.error.emit(str(e))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ToolUpdateWorker(QThread):
|
|
68
|
+
"""Worker for re-syncing manifest-declared tools in a background thread."""
|
|
69
|
+
|
|
70
|
+
finished = Signal(int) # number of manifests processed
|
|
71
|
+
error = Signal(str)
|
|
72
|
+
|
|
73
|
+
def __init__(self, porringer: API, plugins: list[str] | None = None) -> None:
|
|
74
|
+
"""Initialize the worker.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
porringer: The porringer API instance.
|
|
78
|
+
plugins: Optional include-list of plugin names. When set, only
|
|
79
|
+
actions handled by these plugins are executed. ``None``
|
|
80
|
+
means all plugins.
|
|
81
|
+
"""
|
|
82
|
+
super().__init__()
|
|
83
|
+
self._porringer = porringer
|
|
84
|
+
self._plugins = plugins
|
|
85
|
+
|
|
86
|
+
def run(self) -> None:
|
|
87
|
+
"""Re-sync all cached project manifests."""
|
|
88
|
+
try:
|
|
89
|
+
directories = self._porringer.cache.list_directories()
|
|
90
|
+
count = 0
|
|
91
|
+
for directory in directories:
|
|
92
|
+
path = Path(directory.path)
|
|
93
|
+
if not self._porringer.sync.has_manifest(path):
|
|
94
|
+
logger.debug('Skipping path without manifest: %s', path)
|
|
95
|
+
continue
|
|
96
|
+
params = SetupParameters(
|
|
97
|
+
paths=[path],
|
|
98
|
+
project_directory=path if path.is_dir() else None,
|
|
99
|
+
strategy=SyncStrategy.LATEST,
|
|
100
|
+
plugins=self._plugins,
|
|
101
|
+
)
|
|
102
|
+
asyncio.run(self._sync(params))
|
|
103
|
+
count += 1
|
|
104
|
+
self.finished.emit(count)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.exception('Tool update failed')
|
|
107
|
+
self.error.emit(str(e))
|
|
108
|
+
|
|
109
|
+
async def _sync(self, params: SetupParameters) -> None:
|
|
110
|
+
"""Execute a sync stream for the given parameters."""
|
|
111
|
+
async for _event in self._porringer.sync.execute_stream(params):
|
|
112
|
+
pass # consume events to completion
|
|
@@ -210,7 +210,7 @@ class Updater:
|
|
|
210
210
|
velopack_info = manager.check_for_updates()
|
|
211
211
|
|
|
212
212
|
if velopack_info is not None:
|
|
213
|
-
latest = Version(velopack_info.
|
|
213
|
+
latest = Version(velopack_info.TargetFullRelease.Version)
|
|
214
214
|
|
|
215
215
|
self._update_info = UpdateInfo(
|
|
216
216
|
available=True,
|
|
@@ -306,3 +306,51 @@ class TestSyncDoesNotEmit:
|
|
|
306
306
|
window.sync_from_config()
|
|
307
307
|
|
|
308
308
|
signal_spy.assert_not_called()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
# Check for Updates button
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TestCheckForUpdatesButton:
|
|
317
|
+
"""Verify the Check for Updates button and inline status label."""
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def test_button_and_label_exist() -> None:
|
|
321
|
+
"""Window has the check-updates button and status label."""
|
|
322
|
+
window = _make_window()
|
|
323
|
+
assert hasattr(window, '_check_updates_btn')
|
|
324
|
+
assert hasattr(window, '_update_status_label')
|
|
325
|
+
assert window._check_updates_btn.text() == 'Check for Updates\u2026'
|
|
326
|
+
assert window._update_status_label.text() == ''
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def test_click_emits_signal_and_disables() -> None:
|
|
330
|
+
"""Clicking the button emits check_updates_requested and disables it."""
|
|
331
|
+
window = _make_window()
|
|
332
|
+
signal_spy = MagicMock()
|
|
333
|
+
window.check_updates_requested.connect(signal_spy)
|
|
334
|
+
|
|
335
|
+
window._check_updates_btn.click()
|
|
336
|
+
|
|
337
|
+
signal_spy.assert_called_once()
|
|
338
|
+
assert window._check_updates_btn.isEnabled() is False
|
|
339
|
+
assert window._update_status_label.text() == 'Checking\u2026'
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def test_set_update_status() -> None:
|
|
343
|
+
"""set_update_status sets the label text."""
|
|
344
|
+
window = _make_window()
|
|
345
|
+
window.set_update_status('Up to date (v1.0.0)')
|
|
346
|
+
assert window._update_status_label.text() == 'Up to date (v1.0.0)'
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def test_reset_check_updates_button() -> None:
|
|
350
|
+
"""reset_check_updates_button re-enables the button."""
|
|
351
|
+
window = _make_window()
|
|
352
|
+
window._check_updates_btn.setEnabled(False)
|
|
353
|
+
|
|
354
|
+
window.reset_check_updates_button()
|
|
355
|
+
|
|
356
|
+
assert window._check_updates_btn.isEnabled() is True
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from unittest.mock import MagicMock, PropertyMock, patch
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
+
import velopack
|
|
6
7
|
from packaging.version import Version
|
|
7
8
|
|
|
8
9
|
from synodic_client.updater import (
|
|
@@ -119,7 +120,7 @@ class TestUpdater:
|
|
|
119
120
|
@staticmethod
|
|
120
121
|
def test_is_installed_with_velopack(updater: Updater) -> None:
|
|
121
122
|
"""Verify is_installed returns True when Velopack manager available."""
|
|
122
|
-
mock_manager = MagicMock()
|
|
123
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
123
124
|
with patch.object(updater, '_get_velopack_manager', return_value=mock_manager):
|
|
124
125
|
assert updater.is_installed is True
|
|
125
126
|
|
|
@@ -146,7 +147,7 @@ class TestUpdaterCheckForUpdate:
|
|
|
146
147
|
@staticmethod
|
|
147
148
|
def test_check_no_update(updater: Updater) -> None:
|
|
148
149
|
"""Verify check_for_update handles no update available."""
|
|
149
|
-
mock_manager = MagicMock()
|
|
150
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
150
151
|
mock_manager.check_for_updates.return_value = None
|
|
151
152
|
|
|
152
153
|
with patch.object(updater, '_get_velopack_manager', return_value=mock_manager):
|
|
@@ -159,10 +160,12 @@ class TestUpdaterCheckForUpdate:
|
|
|
159
160
|
@staticmethod
|
|
160
161
|
def test_check_update_available(updater: Updater) -> None:
|
|
161
162
|
"""Verify check_for_update handles update available."""
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
mock_target = MagicMock(spec=velopack.VelopackAsset)
|
|
164
|
+
mock_target.Version = '2.0.0'
|
|
165
|
+
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
|
|
166
|
+
mock_velopack_info.TargetFullRelease = mock_target
|
|
164
167
|
|
|
165
|
-
mock_manager = MagicMock()
|
|
168
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
166
169
|
mock_manager.check_for_updates.return_value = mock_velopack_info
|
|
167
170
|
|
|
168
171
|
with patch.object(updater, '_get_velopack_manager', return_value=mock_manager):
|
|
@@ -176,7 +179,7 @@ class TestUpdaterCheckForUpdate:
|
|
|
176
179
|
@staticmethod
|
|
177
180
|
def test_check_error(updater: Updater) -> None:
|
|
178
181
|
"""Verify check_for_update handles errors gracefully."""
|
|
179
|
-
mock_manager = MagicMock()
|
|
182
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
180
183
|
mock_manager.check_for_updates.side_effect = Exception('Network error')
|
|
181
184
|
|
|
182
185
|
with patch.object(updater, '_get_velopack_manager', return_value=mock_manager):
|
|
@@ -189,7 +192,7 @@ class TestUpdaterCheckForUpdate:
|
|
|
189
192
|
@staticmethod
|
|
190
193
|
def test_check_404_returns_friendly_message(updater: Updater) -> None:
|
|
191
194
|
"""Verify a 404 from GitHub returns a friendly no-releases message."""
|
|
192
|
-
mock_manager = MagicMock()
|
|
195
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
193
196
|
mock_manager.check_for_updates.side_effect = RuntimeError('Network error: Http error: http status: 404')
|
|
194
197
|
|
|
195
198
|
with patch.object(updater, '_get_velopack_manager', return_value=mock_manager):
|
|
@@ -205,7 +208,7 @@ class TestUpdaterCheckForUpdate:
|
|
|
205
208
|
@staticmethod
|
|
206
209
|
def test_check_non_404_http_error_is_failed(updater: Updater) -> None:
|
|
207
210
|
"""Verify non-404 HTTP errors still produce FAILED state."""
|
|
208
|
-
mock_manager = MagicMock()
|
|
211
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
209
212
|
mock_manager.check_for_updates.side_effect = RuntimeError('Network error: Http error: http status: 500')
|
|
210
213
|
|
|
211
214
|
with patch.object(updater, '_get_velopack_manager', return_value=mock_manager):
|
|
@@ -238,7 +241,7 @@ class TestUpdaterDownloadUpdate:
|
|
|
238
241
|
@staticmethod
|
|
239
242
|
def test_download_success(updater: Updater) -> None:
|
|
240
243
|
"""Verify download_update succeeds with valid update info."""
|
|
241
|
-
mock_velopack_info = MagicMock()
|
|
244
|
+
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
|
|
242
245
|
updater._update_info = UpdateInfo(
|
|
243
246
|
available=True,
|
|
244
247
|
current_version=Version('1.0.0'),
|
|
@@ -247,7 +250,7 @@ class TestUpdaterDownloadUpdate:
|
|
|
247
250
|
)
|
|
248
251
|
updater._state = UpdateState.UPDATE_AVAILABLE
|
|
249
252
|
|
|
250
|
-
mock_manager = MagicMock()
|
|
253
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
251
254
|
|
|
252
255
|
with (
|
|
253
256
|
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
|
|
@@ -262,7 +265,7 @@ class TestUpdaterDownloadUpdate:
|
|
|
262
265
|
@staticmethod
|
|
263
266
|
def test_download_with_progress_callback(updater: Updater) -> None:
|
|
264
267
|
"""Verify download_update passes progress callback."""
|
|
265
|
-
mock_velopack_info = MagicMock()
|
|
268
|
+
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
|
|
266
269
|
updater._update_info = UpdateInfo(
|
|
267
270
|
available=True,
|
|
268
271
|
current_version=Version('1.0.0'),
|
|
@@ -271,7 +274,7 @@ class TestUpdaterDownloadUpdate:
|
|
|
271
274
|
)
|
|
272
275
|
updater._state = UpdateState.UPDATE_AVAILABLE
|
|
273
276
|
|
|
274
|
-
mock_manager = MagicMock()
|
|
277
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
275
278
|
progress_cb = MagicMock()
|
|
276
279
|
|
|
277
280
|
with (
|
|
@@ -286,7 +289,7 @@ class TestUpdaterDownloadUpdate:
|
|
|
286
289
|
@staticmethod
|
|
287
290
|
def test_download_error(updater: Updater) -> None:
|
|
288
291
|
"""Verify download_update handles errors gracefully."""
|
|
289
|
-
mock_velopack_info = MagicMock()
|
|
292
|
+
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
|
|
290
293
|
updater._update_info = UpdateInfo(
|
|
291
294
|
available=True,
|
|
292
295
|
current_version=Version('1.0.0'),
|
|
@@ -295,7 +298,7 @@ class TestUpdaterDownloadUpdate:
|
|
|
295
298
|
)
|
|
296
299
|
updater._state = UpdateState.UPDATE_AVAILABLE
|
|
297
300
|
|
|
298
|
-
mock_manager = MagicMock()
|
|
301
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
299
302
|
mock_manager.download_updates.side_effect = Exception('Download failed')
|
|
300
303
|
|
|
301
304
|
with (
|
|
@@ -351,7 +354,7 @@ class TestUpdaterApplyUpdate:
|
|
|
351
354
|
@staticmethod
|
|
352
355
|
def test_apply_on_exit_success(updater: Updater) -> None:
|
|
353
356
|
"""Verify apply_update_on_exit schedules update."""
|
|
354
|
-
mock_velopack_info = MagicMock()
|
|
357
|
+
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
|
|
355
358
|
updater._update_info = UpdateInfo(
|
|
356
359
|
available=True,
|
|
357
360
|
current_version=Version('1.0.0'),
|
|
@@ -360,7 +363,7 @@ class TestUpdaterApplyUpdate:
|
|
|
360
363
|
)
|
|
361
364
|
updater._state = UpdateState.DOWNLOADED
|
|
362
365
|
|
|
363
|
-
mock_manager = MagicMock()
|
|
366
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
364
367
|
|
|
365
368
|
with (
|
|
366
369
|
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
|
|
@@ -374,7 +377,7 @@ class TestUpdaterApplyUpdate:
|
|
|
374
377
|
@staticmethod
|
|
375
378
|
def test_apply_on_exit_no_restart(updater: Updater) -> None:
|
|
376
379
|
"""Verify apply_update_on_exit can disable restart (note: not supported by Velopack)."""
|
|
377
|
-
mock_velopack_info = MagicMock()
|
|
380
|
+
mock_velopack_info = MagicMock(spec=velopack.UpdateInfo)
|
|
378
381
|
updater._update_info = UpdateInfo(
|
|
379
382
|
available=True,
|
|
380
383
|
current_version=Version('1.0.0'),
|
|
@@ -383,7 +386,7 @@ class TestUpdaterApplyUpdate:
|
|
|
383
386
|
)
|
|
384
387
|
updater._state = UpdateState.DOWNLOADED
|
|
385
388
|
|
|
386
|
-
mock_manager = MagicMock()
|
|
389
|
+
mock_manager = MagicMock(spec=velopack.UpdateManager)
|
|
387
390
|
|
|
388
391
|
with (
|
|
389
392
|
patch.object(Updater, 'is_installed', new_callable=PropertyMock, return_value=True),
|
|
@@ -401,7 +404,7 @@ class TestInitializeVelopack:
|
|
|
401
404
|
@staticmethod
|
|
402
405
|
def test_initialize_success() -> None:
|
|
403
406
|
"""Verify initialize_velopack calls App().run()."""
|
|
404
|
-
mock_app = MagicMock()
|
|
407
|
+
mock_app = MagicMock(spec=velopack.App)
|
|
405
408
|
with patch('synodic_client.updater.velopack.App', return_value=mock_app) as mock_app_class:
|
|
406
409
|
initialize_velopack()
|
|
407
410
|
mock_app_class.assert_called_once()
|
|
@@ -410,7 +413,7 @@ class TestInitializeVelopack:
|
|
|
410
413
|
@staticmethod
|
|
411
414
|
def test_initialize_handles_exception() -> None:
|
|
412
415
|
"""Verify initialize_velopack handles exceptions gracefully."""
|
|
413
|
-
mock_app = MagicMock()
|
|
416
|
+
mock_app = MagicMock(spec=velopack.App)
|
|
414
417
|
mock_app.run.side_effect = Exception('Test')
|
|
415
418
|
with patch('synodic_client.updater.velopack.App', return_value=mock_app):
|
|
416
419
|
# Should not raise
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.0.1.dev32'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/screen/card.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev32 → synodic_client-0.0.1.dev33}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|