synodic-client 0.0.1.dev54__tar.gz → 0.0.1.dev56__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.dev54 → synodic_client-0.0.1.dev56}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/schema.py +33 -1
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/settings.py +6 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/tray.py +1 -1
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/update_controller.py +66 -55
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_controller.py +1 -76
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/README.md +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_settings.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/windows/test_startup.py +0 -0
|
@@ -13,6 +13,7 @@ from collections.abc import Callable
|
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from enum import Enum, auto
|
|
15
15
|
from pathlib import Path
|
|
16
|
+
from typing import Protocol, runtime_checkable
|
|
16
17
|
|
|
17
18
|
from porringer.schema import (
|
|
18
19
|
PluginInfo,
|
|
@@ -336,10 +337,41 @@ class _DispatchState:
|
|
|
336
337
|
|
|
337
338
|
|
|
338
339
|
# ---------------------------------------------------------------------------
|
|
339
|
-
# Update banner data models
|
|
340
|
+
# Update view protocol & banner data models
|
|
340
341
|
# ---------------------------------------------------------------------------
|
|
341
342
|
|
|
342
343
|
|
|
344
|
+
@runtime_checkable
|
|
345
|
+
class UpdateView(Protocol):
|
|
346
|
+
"""Minimal display contract for the self-update lifecycle.
|
|
347
|
+
|
|
348
|
+
:class:`UpdateBanner` satisfies this protocol implicitly via
|
|
349
|
+
structural typing. The controller broadcasts state transitions
|
|
350
|
+
through a ``list[UpdateView]`` so that every window showing update
|
|
351
|
+
status stays in sync.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
def show_downloading(self, version: str) -> None:
|
|
355
|
+
"""Indicate that *version* is being downloaded."""
|
|
356
|
+
...
|
|
357
|
+
|
|
358
|
+
def show_downloading_progress(self, percentage: int) -> None:
|
|
359
|
+
"""Update the download progress indicator."""
|
|
360
|
+
...
|
|
361
|
+
|
|
362
|
+
def show_ready(self, version: str) -> None:
|
|
363
|
+
"""Indicate that *version* is downloaded and ready to install."""
|
|
364
|
+
...
|
|
365
|
+
|
|
366
|
+
def show_error(self, message: str) -> None:
|
|
367
|
+
"""Display an error *message* in the update area."""
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
def hide_banner(self) -> None:
|
|
371
|
+
"""Hide the update banner."""
|
|
372
|
+
...
|
|
373
|
+
|
|
374
|
+
|
|
343
375
|
class UpdateBannerState(Enum):
|
|
344
376
|
"""Visual states for the update banner."""
|
|
345
377
|
|
|
@@ -301,6 +301,12 @@ class SettingsWindow(QMainWindow):
|
|
|
301
301
|
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
302
302
|
self._check_updates_btn.setEnabled(True)
|
|
303
303
|
|
|
304
|
+
def set_last_checked(self, timestamp: str) -> None:
|
|
305
|
+
"""Update the *last updated* label from an ISO 8601 timestamp."""
|
|
306
|
+
relative = _format_relative_time(timestamp)
|
|
307
|
+
self._last_client_update_label.setText(f'Last updated: {relative}')
|
|
308
|
+
self._last_client_update_label.setToolTip(f'Last updated: {timestamp}')
|
|
309
|
+
|
|
304
310
|
def show_restart_button(self) -> None:
|
|
305
311
|
"""Show the *Restart & Update* button."""
|
|
306
312
|
self._restart_btn.show()
|
|
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING
|
|
|
17
17
|
from PySide6.QtCore import QTimer
|
|
18
18
|
from PySide6.QtWidgets import QApplication
|
|
19
19
|
|
|
20
|
+
from synodic_client.application.screen.schema import UpdateView
|
|
20
21
|
from synodic_client.application.screen.update_banner import UpdateBanner
|
|
21
22
|
from synodic_client.application.theme import (
|
|
22
23
|
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
@@ -48,22 +49,20 @@ class UpdateController:
|
|
|
48
49
|
The running ``QApplication`` (needed for ``quit()`` on auto-apply).
|
|
49
50
|
client:
|
|
50
51
|
The Synodic Client service facade.
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
views:
|
|
53
|
+
One or more :class:`UpdateView` implementations to broadcast
|
|
54
|
+
state transitions to (typically ``UpdateBanner`` instances).
|
|
53
55
|
settings_window:
|
|
54
|
-
The ``SettingsWindow`` (
|
|
56
|
+
The ``SettingsWindow`` (check button + last-updated label).
|
|
55
57
|
config:
|
|
56
58
|
Optional pre-resolved configuration. ``None`` resolves from disk.
|
|
57
|
-
is_user_active:
|
|
58
|
-
Predicate returning ``True`` when the user has a visible window.
|
|
59
|
-
Auto-apply is deferred while active; checks still run normally.
|
|
60
59
|
"""
|
|
61
60
|
|
|
62
61
|
def __init__(
|
|
63
62
|
self,
|
|
64
63
|
app: QApplication,
|
|
65
64
|
client: Client,
|
|
66
|
-
|
|
65
|
+
views: list[UpdateView],
|
|
67
66
|
*,
|
|
68
67
|
settings_window: SettingsWindow,
|
|
69
68
|
config: ResolvedConfig | None = None,
|
|
@@ -73,13 +72,13 @@ class UpdateController:
|
|
|
73
72
|
Args:
|
|
74
73
|
app: The running ``QApplication``.
|
|
75
74
|
client: The Synodic Client service facade.
|
|
76
|
-
|
|
77
|
-
settings_window: The settings window
|
|
75
|
+
views: One or more :class:`UpdateView` implementations.
|
|
76
|
+
settings_window: The settings window (check button + timestamp).
|
|
78
77
|
config: Optional pre-resolved configuration.
|
|
79
78
|
"""
|
|
80
79
|
self._app = app
|
|
81
80
|
self._client = client
|
|
82
|
-
self.
|
|
81
|
+
self._views = views
|
|
83
82
|
self._settings_window = settings_window
|
|
84
83
|
self._config = config
|
|
85
84
|
self._is_user_active: Callable[[], bool] = lambda: False
|
|
@@ -94,11 +93,13 @@ class UpdateController:
|
|
|
94
93
|
self._auto_update_timer: QTimer | None = None
|
|
95
94
|
self._restart_auto_update_timer()
|
|
96
95
|
|
|
97
|
-
# Wire banner signals
|
|
98
|
-
self.
|
|
99
|
-
|
|
96
|
+
# Wire banner signals (UpdateBanner-specific, outside the protocol)
|
|
97
|
+
for view in self._views:
|
|
98
|
+
if isinstance(view, UpdateBanner):
|
|
99
|
+
view.restart_requested.connect(self._apply_update)
|
|
100
|
+
view.retry_requested.connect(lambda: self.check_now(silent=True))
|
|
100
101
|
|
|
101
|
-
# Wire settings check-updates
|
|
102
|
+
# Wire settings check-updates and restart buttons
|
|
102
103
|
self._settings_window.check_updates_requested.connect(self._on_manual_check)
|
|
103
104
|
self._settings_window.restart_requested.connect(self._apply_update)
|
|
104
105
|
|
|
@@ -128,6 +129,25 @@ class UpdateController:
|
|
|
128
129
|
"""
|
|
129
130
|
return self._auto_apply and not self._is_user_active()
|
|
130
131
|
|
|
132
|
+
def _persist_check_timestamp(self) -> None:
|
|
133
|
+
"""Persist the current time as *last_client_update* and refresh the label."""
|
|
134
|
+
ts = datetime.now(UTC).isoformat()
|
|
135
|
+
update_user_config(last_client_update=ts)
|
|
136
|
+
self._settings_window.set_last_checked(ts)
|
|
137
|
+
|
|
138
|
+
def _report_error(self, message: str, *, silent: bool) -> None:
|
|
139
|
+
"""Show an error to the user or log it, depending on *silent*.
|
|
140
|
+
|
|
141
|
+
Always updates the settings status line. When not *silent*,
|
|
142
|
+
also broadcasts the error to all update-banner views.
|
|
143
|
+
"""
|
|
144
|
+
self._settings_window.set_update_status('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
145
|
+
if silent:
|
|
146
|
+
logger.warning('%s', message)
|
|
147
|
+
else:
|
|
148
|
+
for view in self._views:
|
|
149
|
+
view.show_error(message)
|
|
150
|
+
|
|
131
151
|
# ------------------------------------------------------------------
|
|
132
152
|
# Timer management
|
|
133
153
|
# ------------------------------------------------------------------
|
|
@@ -212,10 +232,11 @@ class UpdateController:
|
|
|
212
232
|
"""Run an update check."""
|
|
213
233
|
if self._client.updater is None:
|
|
214
234
|
if not silent:
|
|
215
|
-
|
|
235
|
+
for view in self._views:
|
|
236
|
+
view.show_error('Updater is not initialized.')
|
|
216
237
|
return
|
|
217
238
|
|
|
218
|
-
# Preserve the
|
|
239
|
+
# Preserve the banner state when an update is already pending
|
|
219
240
|
if self._pending_version is None:
|
|
220
241
|
self._settings_window.set_checking()
|
|
221
242
|
|
|
@@ -237,21 +258,16 @@ class UpdateController:
|
|
|
237
258
|
self._settings_window.reset_check_updates_button()
|
|
238
259
|
|
|
239
260
|
if result is None:
|
|
240
|
-
self.
|
|
241
|
-
if not silent:
|
|
242
|
-
self._banner.show_error('Failed to check for updates.')
|
|
243
|
-
else:
|
|
244
|
-
logger.warning('Automatic update check failed (no result)')
|
|
261
|
+
self._report_error('Failed to check for updates.', silent=silent)
|
|
245
262
|
return
|
|
246
263
|
|
|
247
264
|
if result.error:
|
|
248
|
-
self.
|
|
249
|
-
if not silent:
|
|
250
|
-
self._banner.show_error(result.error)
|
|
251
|
-
else:
|
|
252
|
-
logger.warning('Automatic update check failed: %s', result.error)
|
|
265
|
+
self._report_error(result.error, silent=silent)
|
|
253
266
|
return
|
|
254
267
|
|
|
268
|
+
# Successful check — refresh the "last updated" timestamp
|
|
269
|
+
self._persist_check_timestamp()
|
|
270
|
+
|
|
255
271
|
if not result.available:
|
|
256
272
|
self._settings_window.set_update_status('Up to date', UPDATE_STATUS_UP_TO_DATE_STYLE)
|
|
257
273
|
if not silent:
|
|
@@ -268,22 +284,15 @@ class UpdateController:
|
|
|
268
284
|
return
|
|
269
285
|
|
|
270
286
|
# New update available — download it
|
|
271
|
-
self._settings_window.set_update_status(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
)
|
|
275
|
-
self._banner.show_downloading(version)
|
|
287
|
+
self._settings_window.set_update_status(f'v{version} available', UPDATE_STATUS_AVAILABLE_STYLE)
|
|
288
|
+
for view in self._views:
|
|
289
|
+
view.show_downloading(version)
|
|
276
290
|
self._start_download(version)
|
|
277
291
|
|
|
278
292
|
def _on_check_error(self, error: str, *, silent: bool = False) -> None:
|
|
279
293
|
"""Handle unexpected exception during update check."""
|
|
280
294
|
self._settings_window.reset_check_updates_button()
|
|
281
|
-
self.
|
|
282
|
-
|
|
283
|
-
if not silent:
|
|
284
|
-
self._banner.show_error(f'Update check error: {error}')
|
|
285
|
-
else:
|
|
286
|
-
logger.warning('Automatic update check error: %s', error)
|
|
295
|
+
self._report_error(f'Update check error: {error}', silent=silent)
|
|
287
296
|
|
|
288
297
|
# ------------------------------------------------------------------
|
|
289
298
|
# Download flow
|
|
@@ -298,50 +307,51 @@ class UpdateController:
|
|
|
298
307
|
try:
|
|
299
308
|
success = await download_update(
|
|
300
309
|
self._client,
|
|
301
|
-
on_progress=self.
|
|
310
|
+
on_progress=self._on_download_progress,
|
|
302
311
|
)
|
|
303
312
|
self._on_download_finished(success, version)
|
|
304
313
|
except Exception as exc:
|
|
305
314
|
logger.exception('Update download failed')
|
|
306
315
|
self._on_download_error(str(exc))
|
|
307
316
|
|
|
317
|
+
def _on_download_progress(self, percentage: int) -> None:
|
|
318
|
+
"""Broadcast download progress to all views."""
|
|
319
|
+
for view in self._views:
|
|
320
|
+
view.show_downloading_progress(percentage)
|
|
321
|
+
|
|
308
322
|
def _on_download_finished(self, success: bool, version: str) -> None:
|
|
309
323
|
"""Handle download completion."""
|
|
310
324
|
if not success:
|
|
311
|
-
self._banner.show_error('Download failed. Please try again later.')
|
|
312
325
|
self._settings_window.set_update_status('Download failed', UPDATE_STATUS_ERROR_STYLE)
|
|
326
|
+
for view in self._views:
|
|
327
|
+
view.show_error('Download failed. Please try again later.')
|
|
313
328
|
return
|
|
314
329
|
|
|
315
|
-
# Persist the client update
|
|
316
|
-
|
|
330
|
+
# Persist the client-update timestamp (actual update downloaded)
|
|
331
|
+
ts = datetime.now(UTC).isoformat()
|
|
332
|
+
update_user_config(last_client_update=ts)
|
|
317
333
|
|
|
318
334
|
self._pending_version = version
|
|
319
335
|
|
|
320
336
|
if self._can_auto_apply():
|
|
321
337
|
# Silently apply and restart — no banner, no user interaction
|
|
322
338
|
logger.info('Auto-applying update v%s', version)
|
|
323
|
-
self._settings_window.set_update_status(
|
|
324
|
-
f'v{version} installing\u2026',
|
|
325
|
-
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
326
|
-
)
|
|
327
339
|
self._apply_update(silent=True)
|
|
328
340
|
return
|
|
329
341
|
|
|
330
342
|
self._show_ready(version)
|
|
331
343
|
|
|
332
344
|
def _show_ready(self, version: str) -> None:
|
|
333
|
-
"""Present the *ready to restart* state
|
|
334
|
-
self.
|
|
335
|
-
self._settings_window.set_update_status(
|
|
336
|
-
f'v{version} ready',
|
|
337
|
-
UPDATE_STATUS_UP_TO_DATE_STYLE,
|
|
338
|
-
)
|
|
345
|
+
"""Present the *ready to restart* state across all views."""
|
|
346
|
+
self._settings_window.set_update_status(f'v{version} ready', UPDATE_STATUS_UP_TO_DATE_STYLE)
|
|
339
347
|
self._settings_window.show_restart_button()
|
|
348
|
+
for view in self._views:
|
|
349
|
+
view.show_ready(version)
|
|
340
350
|
|
|
341
351
|
def _on_download_error(self, error: str) -> None:
|
|
342
|
-
"""Handle download error — show error
|
|
343
|
-
self.
|
|
344
|
-
|
|
352
|
+
"""Handle download error — show error across all views."""
|
|
353
|
+
for view in self._views:
|
|
354
|
+
view.show_error(f'Download error: {error}')
|
|
345
355
|
|
|
346
356
|
# ------------------------------------------------------------------
|
|
347
357
|
# Apply
|
|
@@ -364,4 +374,5 @@ class UpdateController:
|
|
|
364
374
|
self._app.quit()
|
|
365
375
|
except Exception as e:
|
|
366
376
|
logger.error('Failed to apply update: %s', e)
|
|
367
|
-
|
|
377
|
+
for view in self._views:
|
|
378
|
+
view.show_error(f'Failed to apply update: {e}')
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_controller.py
RENAMED
|
@@ -74,7 +74,7 @@ def _make_controller(
|
|
|
74
74
|
controller = UpdateController(
|
|
75
75
|
app,
|
|
76
76
|
client,
|
|
77
|
-
banner,
|
|
77
|
+
[banner],
|
|
78
78
|
settings_window=settings,
|
|
79
79
|
config=config,
|
|
80
80
|
)
|
|
@@ -228,81 +228,6 @@ class TestDownloadFinished:
|
|
|
228
228
|
assert banner.state.name == 'ERROR'
|
|
229
229
|
settings.set_update_status.assert_called_with('Download failed', UPDATE_STATUS_ERROR_STYLE)
|
|
230
230
|
|
|
231
|
-
@staticmethod
|
|
232
|
-
def test_download_sets_pending_version() -> None:
|
|
233
|
-
"""A successful download should set _pending_version."""
|
|
234
|
-
ctrl, app, client, banner, settings = _make_controller(auto_apply=False)
|
|
235
|
-
ctrl._on_download_finished(True, '2.0.0')
|
|
236
|
-
|
|
237
|
-
assert ctrl._pending_version == '2.0.0'
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# ---------------------------------------------------------------------------
|
|
241
|
-
# Pending version — skip redundant downloads
|
|
242
|
-
# ---------------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
class TestPendingVersion:
|
|
246
|
-
"""Verify behaviour when an update is already downloaded and pending."""
|
|
247
|
-
|
|
248
|
-
@staticmethod
|
|
249
|
-
def test_check_skips_download_when_version_already_pending() -> None:
|
|
250
|
-
"""Re-checking the same version should restore ready state, not re-download."""
|
|
251
|
-
ctrl, _app, _client, banner, settings = _make_controller(auto_apply=False)
|
|
252
|
-
ctrl._pending_version = '2.0.0'
|
|
253
|
-
|
|
254
|
-
result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
|
|
255
|
-
|
|
256
|
-
with patch.object(ctrl, '_start_download') as mock_dl:
|
|
257
|
-
ctrl._on_check_finished(result, silent=True)
|
|
258
|
-
|
|
259
|
-
mock_dl.assert_not_called()
|
|
260
|
-
assert banner.state.name == 'READY'
|
|
261
|
-
settings.show_restart_button.assert_called_once()
|
|
262
|
-
|
|
263
|
-
@staticmethod
|
|
264
|
-
def test_check_downloads_when_newer_version() -> None:
|
|
265
|
-
"""A different version should trigger a fresh download."""
|
|
266
|
-
ctrl, _app, _client, banner, settings = _make_controller(auto_apply=False)
|
|
267
|
-
ctrl._pending_version = '1.5.0'
|
|
268
|
-
|
|
269
|
-
result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
|
|
270
|
-
|
|
271
|
-
with patch.object(ctrl, '_start_download') as mock_dl:
|
|
272
|
-
ctrl._on_check_finished(result, silent=True)
|
|
273
|
-
|
|
274
|
-
mock_dl.assert_called_once_with('2.0.0')
|
|
275
|
-
|
|
276
|
-
@staticmethod
|
|
277
|
-
def test_do_check_preserves_settings_ui_when_pending() -> None:
|
|
278
|
-
"""set_checking should NOT be called when an update is already pending."""
|
|
279
|
-
ctrl, _app, _client, banner, settings = _make_controller()
|
|
280
|
-
ctrl._pending_version = '2.0.0'
|
|
281
|
-
|
|
282
|
-
with patch('asyncio.create_task'):
|
|
283
|
-
ctrl._do_check(silent=True)
|
|
284
|
-
|
|
285
|
-
settings.set_checking.assert_not_called()
|
|
286
|
-
|
|
287
|
-
@staticmethod
|
|
288
|
-
def test_do_check_shows_checking_when_no_pending() -> None:
|
|
289
|
-
"""set_checking should be called when there is no pending update."""
|
|
290
|
-
ctrl, _app, _client, banner, settings = _make_controller()
|
|
291
|
-
|
|
292
|
-
with patch('asyncio.create_task'):
|
|
293
|
-
ctrl._do_check(silent=True)
|
|
294
|
-
|
|
295
|
-
settings.set_checking.assert_called_once()
|
|
296
|
-
|
|
297
|
-
@staticmethod
|
|
298
|
-
def test_apply_clears_pending_version() -> None:
|
|
299
|
-
"""_apply_update should clear _pending_version."""
|
|
300
|
-
ctrl, app, client, banner, settings = _make_controller()
|
|
301
|
-
ctrl._pending_version = '2.0.0'
|
|
302
|
-
ctrl._apply_update()
|
|
303
|
-
|
|
304
|
-
assert ctrl._pending_version is None
|
|
305
|
-
|
|
306
231
|
|
|
307
232
|
# ---------------------------------------------------------------------------
|
|
308
233
|
# User-active gating
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/init.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/card.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
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/synodic_client/application/workers.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
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_preview_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_feedback.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
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev56}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|