synodic-client 0.0.1.dev66__tar.gz → 0.0.1.dev67__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.dev66 → synodic_client-0.0.1.dev67}/PKG-INFO +2 -2
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/pyproject.toml +2 -2
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/__init__.py +14 -6
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/action_card.py +19 -42
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/install.py +5 -11
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/install_workers.py +8 -8
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/log_panel.py +3 -4
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/schema.py +7 -10
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/theme.py +2 -2
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/update_model.py +1 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_action_card.py +52 -108
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_install_preview.py +29 -13
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_log_panel.py +0 -1
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_preview_model.py +2 -4
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_update_controller.py +21 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/README.md +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/tray.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/update_controller.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_settings.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/windows/test_startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: synodic_client
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev67
|
|
4
4
|
Author-Email: Synodic Software <contact@synodic.software>
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
Project-URL: homepage, https://github.com/synodic/synodic-client
|
|
@@ -8,7 +8,7 @@ Project-URL: repository, https://github.com/synodic/synodic-client
|
|
|
8
8
|
Requires-Python: <3.15,>=3.14
|
|
9
9
|
Requires-Dist: pyside6>=6.10.2
|
|
10
10
|
Requires-Dist: packaging>=26.0
|
|
11
|
-
Requires-Dist: porringer>=0.2.1.
|
|
11
|
+
Requires-Dist: porringer>=0.2.1.dev79
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
13
|
Requires-Dist: velopack>=0.0.1444.dev49733
|
|
14
14
|
Requires-Dist: typer>=0.24.1
|
|
@@ -10,12 +10,12 @@ requires-python = ">=3.14, <3.15"
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"pyside6>=6.10.2",
|
|
12
12
|
"packaging>=26.0",
|
|
13
|
-
"porringer>=0.2.1.
|
|
13
|
+
"porringer>=0.2.1.dev79",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
15
|
"velopack>=0.0.1444.dev49733",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev67"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from datetime import UTC, datetime
|
|
10
10
|
|
|
11
|
-
from porringer.schema import SetupAction, SkipReason
|
|
11
|
+
from porringer.schema import SetupAction, SetupActionResult, SkipReason
|
|
12
12
|
from porringer.schema.plugin import PluginKind
|
|
13
13
|
|
|
14
14
|
_SECONDS_PER_MINUTE = 60
|
|
@@ -63,18 +63,26 @@ def skip_reason_label(reason: SkipReason | None) -> str:
|
|
|
63
63
|
return SKIP_REASON_LABELS.get(reason, reason.name.replace('_', ' ').capitalize())
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def format_cli_command(
|
|
66
|
+
def format_cli_command(
|
|
67
|
+
action: SetupAction,
|
|
68
|
+
*,
|
|
69
|
+
result: SetupActionResult | None = None,
|
|
70
|
+
suppress_description: bool = False,
|
|
71
|
+
) -> str:
|
|
67
72
|
"""Return a human-readable CLI command string for *action*.
|
|
68
73
|
|
|
69
|
-
Prefers ``cli_command
|
|
70
|
-
|
|
74
|
+
Prefers ``result.cli_command`` (populated after dry-run), falls
|
|
75
|
+
back to ``action.command``, then synthesises an
|
|
76
|
+
``installer install <package>`` string for package actions, and
|
|
71
77
|
finally returns the action description as a last resort.
|
|
72
78
|
|
|
73
79
|
When *suppress_description* is ``True`` the final description
|
|
74
80
|
fallback returns an empty string instead.
|
|
75
81
|
"""
|
|
76
|
-
if
|
|
77
|
-
return ' '.join(
|
|
82
|
+
if result is not None and result.cli_command:
|
|
83
|
+
return ' '.join(result.cli_command)
|
|
84
|
+
if action.command:
|
|
85
|
+
return ' '.join(action.command)
|
|
78
86
|
if action.kind == PluginKind.PACKAGE and action.package:
|
|
79
87
|
return f'{action.installer or "pip"} install {action.package}'
|
|
80
88
|
return '' if suppress_description else action.description
|
|
@@ -73,19 +73,6 @@ _SPINNER_INTERVAL = 50
|
|
|
73
73
|
_KIND_ORDER: dict[PluginKind | None, int] = {kind: i for i, kind in enumerate(PHASE_ORDER)}
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def action_key(action: SetupAction) -> tuple[object, ...]:
|
|
77
|
-
"""Return a stable identity key for *action*.
|
|
78
|
-
|
|
79
|
-
Uses content-based fields (kind, installer, package name, command)
|
|
80
|
-
so the same logical action from different ``execute_stream`` runs
|
|
81
|
-
resolves to the same key.
|
|
82
|
-
"""
|
|
83
|
-
pkg_name = str(action.package.name) if action.package else None
|
|
84
|
-
pt_name = str(action.plugin_target.name) if action.plugin_target else None
|
|
85
|
-
cmd = tuple(action.command) if action.command else None
|
|
86
|
-
return (action.kind, action.installer, pkg_name, pt_name, cmd)
|
|
87
|
-
|
|
88
|
-
|
|
89
76
|
def action_sort_key(action: SetupAction) -> int:
|
|
90
77
|
"""Return a sort key that groups cards by execution phase.
|
|
91
78
|
|
|
@@ -396,24 +383,6 @@ class ActionCard(QFrame):
|
|
|
396
383
|
else:
|
|
397
384
|
self._prerelease_cb.hide()
|
|
398
385
|
|
|
399
|
-
def update_command(self, action: SetupAction) -> None:
|
|
400
|
-
"""Update the CLI command label after the resolved preview arrives.
|
|
401
|
-
|
|
402
|
-
Called from the two-phase display flow once ``MANIFEST_LOADED``
|
|
403
|
-
provides actions with their ``cli_command`` populated.
|
|
404
|
-
|
|
405
|
-
Args:
|
|
406
|
-
action: The setup action with resolved CLI command.
|
|
407
|
-
"""
|
|
408
|
-
if self._is_skeleton:
|
|
409
|
-
return
|
|
410
|
-
cmd_text = format_cli_command(action, suppress_description=True)
|
|
411
|
-
if cmd_text:
|
|
412
|
-
self._command_label.setText(cmd_text)
|
|
413
|
-
self._command_row.show()
|
|
414
|
-
else:
|
|
415
|
-
self._command_row.hide()
|
|
416
|
-
|
|
417
386
|
def _populate_status(
|
|
418
387
|
self,
|
|
419
388
|
action: SetupAction,
|
|
@@ -494,7 +463,7 @@ class ActionCard(QFrame):
|
|
|
494
463
|
self._status_label.setText(label)
|
|
495
464
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_UPDATE)
|
|
496
465
|
elif result.skipped:
|
|
497
|
-
label = skip_reason_label(result.skip_reason)
|
|
466
|
+
label = '\u2713 ' + skip_reason_label(result.skip_reason)
|
|
498
467
|
self._status_label.setText(label)
|
|
499
468
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_SATISFIED)
|
|
500
469
|
elif not result.success:
|
|
@@ -517,6 +486,13 @@ class ActionCard(QFrame):
|
|
|
517
486
|
else:
|
|
518
487
|
self._status_label.setToolTip('')
|
|
519
488
|
|
|
489
|
+
# CLI command — update with resolved cli_command from result
|
|
490
|
+
assert self._action is not None
|
|
491
|
+
cmd_text = format_cli_command(self._action, result=result, suppress_description=True)
|
|
492
|
+
if cmd_text:
|
|
493
|
+
self._command_label.setText(cmd_text)
|
|
494
|
+
self._command_row.show()
|
|
495
|
+
|
|
520
496
|
# Version column
|
|
521
497
|
self._check_available_version = result.available_version
|
|
522
498
|
if result.installed_version and result.available_version:
|
|
@@ -524,6 +500,9 @@ class ActionCard(QFrame):
|
|
|
524
500
|
self._version_label.setStyleSheet(ACTION_CARD_VERSION_STYLE + ' color: #d7ba7d;')
|
|
525
501
|
elif result.installed_version:
|
|
526
502
|
self._version_label.setText(result.installed_version)
|
|
503
|
+
elif result.available_version:
|
|
504
|
+
self._version_label.setText(f'\u2192 {result.available_version}')
|
|
505
|
+
self._version_label.setStyleSheet(ACTION_CARD_VERSION_STYLE + ' color: grey;')
|
|
527
506
|
|
|
528
507
|
def finalize_checking(self) -> None:
|
|
529
508
|
"""Resolve a still-pending 'Checking\u2026' status to 'Needed'.
|
|
@@ -611,9 +590,8 @@ class ActionCard(QFrame):
|
|
|
611
590
|
class ActionCardList(QWidget):
|
|
612
591
|
"""Container of :class:`ActionCard` widgets.
|
|
613
592
|
|
|
614
|
-
Cards are keyed by
|
|
615
|
-
look-ups work across different ``execute_stream`` runs
|
|
616
|
-
``SetupAction`` objects are different Python instances.
|
|
593
|
+
Cards are keyed by ``SetupAction`` directly (frozen dataclass) so
|
|
594
|
+
that look-ups work across different ``execute_stream`` runs.
|
|
617
595
|
"""
|
|
618
596
|
|
|
619
597
|
prerelease_toggled = Signal(str, bool)
|
|
@@ -629,7 +607,7 @@ class ActionCardList(QWidget):
|
|
|
629
607
|
self._layout.addStretch()
|
|
630
608
|
|
|
631
609
|
self._cards: list[ActionCard] = []
|
|
632
|
-
self._action_map: dict[
|
|
610
|
+
self._action_map: dict[SetupAction, ActionCard] = {}
|
|
633
611
|
|
|
634
612
|
# ------------------------------------------------------------------
|
|
635
613
|
# Skeleton loading
|
|
@@ -679,7 +657,7 @@ class ActionCardList(QWidget):
|
|
|
679
657
|
card.prerelease_toggled.connect(self.prerelease_toggled.emit)
|
|
680
658
|
self._layout.insertWidget(self._layout.count() - 1, card)
|
|
681
659
|
self._cards.append(card)
|
|
682
|
-
self._action_map[
|
|
660
|
+
self._action_map[act] = card
|
|
683
661
|
|
|
684
662
|
# ------------------------------------------------------------------
|
|
685
663
|
# Card lookup
|
|
@@ -696,11 +674,10 @@ class ActionCardList(QWidget):
|
|
|
696
674
|
return len(self._cards)
|
|
697
675
|
|
|
698
676
|
def get_card(self, action: SetupAction) -> ActionCard | None:
|
|
699
|
-
"""Look up the card for a given action
|
|
677
|
+
"""Look up the card for a given action.
|
|
700
678
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
but the same logical action maps to the same card.
|
|
679
|
+
``SetupAction`` is a frozen dataclass, so the same logical
|
|
680
|
+
action from different ``execute_stream`` runs hashes equally.
|
|
704
681
|
|
|
705
682
|
Args:
|
|
706
683
|
action: The setup action to find.
|
|
@@ -708,7 +685,7 @@ class ActionCardList(QWidget):
|
|
|
708
685
|
Returns:
|
|
709
686
|
The card widget, or ``None`` if not found.
|
|
710
687
|
"""
|
|
711
|
-
return self._action_map.get(
|
|
688
|
+
return self._action_map.get(action)
|
|
712
689
|
|
|
713
690
|
# ------------------------------------------------------------------
|
|
714
691
|
# Bulk operations
|
|
@@ -45,7 +45,7 @@ from PySide6.QtWidgets import (
|
|
|
45
45
|
|
|
46
46
|
from synodic_client.application.package_state import PackageStateStore
|
|
47
47
|
from synodic_client.application.screen import skip_reason_label
|
|
48
|
-
from synodic_client.application.screen.action_card import ActionCardList
|
|
48
|
+
from synodic_client.application.screen.action_card import ActionCardList
|
|
49
49
|
from synodic_client.application.screen.card import CardFrame
|
|
50
50
|
from synodic_client.application.screen.install_workers import run_install, run_preview
|
|
51
51
|
from synodic_client.application.screen.log_panel import ExecutionLogPanel
|
|
@@ -595,11 +595,11 @@ class SetupPreviewWidget(QWidget):
|
|
|
595
595
|
self._install_btn.setEnabled(True)
|
|
596
596
|
|
|
597
597
|
def _on_preview_resolved(self, preview: SetupResults, manifest_path: str, temp_dir_path: str) -> None:
|
|
598
|
-
"""Handle the fully-resolved preview
|
|
598
|
+
"""Handle the fully-resolved preview.
|
|
599
599
|
|
|
600
600
|
Called after ``MANIFEST_LOADED`` — cards are already visible
|
|
601
|
-
from the earlier ``_on_manifest_parsed`` handler. This
|
|
602
|
-
|
|
601
|
+
from the earlier ``_on_manifest_parsed`` handler. This updates
|
|
602
|
+
the temp-dir reference and emits metadata.
|
|
603
603
|
"""
|
|
604
604
|
if self._model.preview is None:
|
|
605
605
|
return
|
|
@@ -609,19 +609,13 @@ class SetupPreviewWidget(QWidget):
|
|
|
609
609
|
if preview.metadata:
|
|
610
610
|
self.metadata_ready.emit(preview)
|
|
611
611
|
|
|
612
|
-
for action in preview.actions:
|
|
613
|
-
if action.cli_command:
|
|
614
|
-
card = self._card_list.get_card(action)
|
|
615
|
-
if card is not None:
|
|
616
|
-
card.update_command(action)
|
|
617
|
-
|
|
618
612
|
def _on_action_checked(self, row: int, result: SetupActionResult) -> None:
|
|
619
613
|
"""Update the model and action card with a dry-run result."""
|
|
620
614
|
m = self._model
|
|
621
615
|
if result.skipped and result.skip_reason == SkipReason.UPDATE_AVAILABLE:
|
|
622
616
|
label = skip_reason_label(result.skip_reason)
|
|
623
617
|
if 0 <= row < len(m.action_states):
|
|
624
|
-
m.upgradable_keys.add(
|
|
618
|
+
m.upgradable_keys.add(m.action_states[row].action)
|
|
625
619
|
elif result.skipped:
|
|
626
620
|
label = skip_reason_label(result.skip_reason)
|
|
627
621
|
elif not result.success:
|
|
@@ -159,10 +159,9 @@ def _dispatch_preview_event(
|
|
|
159
159
|
) -> None:
|
|
160
160
|
"""Route a single preview stream event to the appropriate callback.
|
|
161
161
|
|
|
162
|
-
Mutates *state* in-place
|
|
162
|
+
Mutates *state* in-place (``got_parsed`` flag).
|
|
163
163
|
"""
|
|
164
164
|
if event.kind == ProgressEventKind.MANIFEST_PARSED and event.manifest:
|
|
165
|
-
state.action_index = {id(a): i for i, a in enumerate(event.manifest.actions)}
|
|
166
165
|
if cb.on_manifest_parsed is not None:
|
|
167
166
|
cb.on_manifest_parsed(event.manifest, manifest_path, temp_dir_str)
|
|
168
167
|
state.got_parsed = True
|
|
@@ -176,16 +175,17 @@ def _dispatch_preview_event(
|
|
|
176
175
|
return
|
|
177
176
|
|
|
178
177
|
if event.kind == ProgressEventKind.MANIFEST_LOADED and event.manifest:
|
|
179
|
-
if not state.got_parsed:
|
|
180
|
-
state.action_index = {id(a): i for i, a in enumerate(event.manifest.actions)}
|
|
181
178
|
if cb.on_preview_ready is not None:
|
|
182
179
|
cb.on_preview_ready(event.manifest, manifest_path, temp_dir_str)
|
|
183
180
|
return
|
|
184
181
|
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
if (
|
|
183
|
+
event.kind == ProgressEventKind.ACTION_COMPLETED
|
|
184
|
+
and event.result
|
|
185
|
+
and event.action_index is not None
|
|
186
|
+
and cb.on_action_checked is not None
|
|
187
|
+
):
|
|
188
|
+
cb.on_action_checked(event.action_index, event.result)
|
|
189
189
|
|
|
190
190
|
|
|
191
191
|
# ---------------------------------------------------------------------------
|
|
@@ -26,7 +26,6 @@ from PySide6.QtWidgets import (
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
from synodic_client.application.screen import ACTION_KIND_LABELS, skip_reason_label
|
|
29
|
-
from synodic_client.application.screen.action_card import action_key
|
|
30
29
|
from synodic_client.application.screen.card import CHEVRON_DOWN, CHEVRON_RIGHT, ClickableHeader
|
|
31
30
|
from synodic_client.application.theme import (
|
|
32
31
|
LOG_CHEVRON_STYLE,
|
|
@@ -186,7 +185,7 @@ class ExecutionLogPanel(QWidget):
|
|
|
186
185
|
self._layout.addStretch()
|
|
187
186
|
|
|
188
187
|
# Map action content-key → section widget for quick lookup
|
|
189
|
-
self._sections: dict[
|
|
188
|
+
self._sections: dict[SetupAction, ActionLogSection] = {}
|
|
190
189
|
self._section_count = 0
|
|
191
190
|
|
|
192
191
|
# --- Public API ---
|
|
@@ -204,7 +203,7 @@ class ExecutionLogPanel(QWidget):
|
|
|
204
203
|
section = ActionLogSection(action, self._section_count, self)
|
|
205
204
|
# Insert before the stretch
|
|
206
205
|
self._layout.insertWidget(self._layout.count() - 1, section)
|
|
207
|
-
self._sections[
|
|
206
|
+
self._sections[action] = section
|
|
208
207
|
|
|
209
208
|
return section
|
|
210
209
|
|
|
@@ -217,7 +216,7 @@ class ExecutionLogPanel(QWidget):
|
|
|
217
216
|
Returns:
|
|
218
217
|
The section widget, or ``None`` if not found.
|
|
219
218
|
"""
|
|
220
|
-
return self._sections.get(
|
|
219
|
+
return self._sections.get(action)
|
|
221
220
|
|
|
222
221
|
def on_sub_progress(self, action: SetupAction, progress: SubActionProgress) -> None:
|
|
223
222
|
"""Handle a sub-action progress event.
|
|
@@ -23,7 +23,6 @@ from porringer.schema import (
|
|
|
23
23
|
)
|
|
24
24
|
from porringer.schema.plugin import RuntimePackageResult
|
|
25
25
|
|
|
26
|
-
from synodic_client.application.screen.action_card import action_key
|
|
27
26
|
from synodic_client.application.uri import normalize_manifest_key
|
|
28
27
|
|
|
29
28
|
# ---------------------------------------------------------------------------
|
|
@@ -237,7 +236,6 @@ class PreviewModel:
|
|
|
237
236
|
|
|
238
237
|
def __init__(self) -> None:
|
|
239
238
|
"""Initialise a blank preview model."""
|
|
240
|
-
self._action_key = action_key
|
|
241
239
|
self._normalize = normalize_manifest_key
|
|
242
240
|
|
|
243
241
|
self.phase: PreviewPhase = PreviewPhase.IDLE
|
|
@@ -248,19 +246,19 @@ class PreviewModel:
|
|
|
248
246
|
self.plugin_installed: dict[str, bool] = {}
|
|
249
247
|
self.prerelease_overrides: set[str] = set()
|
|
250
248
|
self.action_states: list[ActionState] = []
|
|
251
|
-
self._action_state_map: dict[
|
|
249
|
+
self._action_state_map: dict[SetupAction, ActionState] = {}
|
|
252
250
|
self._action_state_map_len: int = 0
|
|
253
|
-
self.upgradable_keys: set[
|
|
251
|
+
self.upgradable_keys: set[SetupAction] = set()
|
|
254
252
|
self.checked_count: int = 0
|
|
255
253
|
self.completed_count: int = 0
|
|
256
254
|
self.temp_dir: str | None = None
|
|
257
255
|
|
|
258
256
|
# -- Computed helpers --------------------------------------------------
|
|
259
257
|
|
|
260
|
-
def _ensure_action_state_map(self) -> dict[
|
|
261
|
-
"""Return the action
|
|
258
|
+
def _ensure_action_state_map(self) -> dict[SetupAction, ActionState]:
|
|
259
|
+
"""Return the action → state lookup, rebuilding if stale."""
|
|
262
260
|
if len(self.action_states) != self._action_state_map_len:
|
|
263
|
-
self._action_state_map = {
|
|
261
|
+
self._action_state_map = {s.action: s for s in self.action_states}
|
|
264
262
|
self._action_state_map_len = len(self.action_states)
|
|
265
263
|
return self._action_state_map
|
|
266
264
|
|
|
@@ -279,8 +277,8 @@ class PreviewModel:
|
|
|
279
277
|
return self.actionable_count > 0 or any(s.action.kind is None for s in self.action_states)
|
|
280
278
|
|
|
281
279
|
def action_state_for(self, act: SetupAction) -> ActionState | None:
|
|
282
|
-
"""Look up :class:`ActionState`
|
|
283
|
-
return self._ensure_action_state_map().get(
|
|
280
|
+
"""Look up :class:`ActionState` for *act* (O(1) amortized)."""
|
|
281
|
+
return self._ensure_action_state_map().get(act)
|
|
284
282
|
|
|
285
283
|
def has_same_manifest(self, key: str) -> bool:
|
|
286
284
|
"""Return ``True`` if *key* matches the current manifest key."""
|
|
@@ -339,5 +337,4 @@ class PreviewConfig:
|
|
|
339
337
|
class _DispatchState:
|
|
340
338
|
"""Mutable accumulator for :func:`_dispatch_preview_event`."""
|
|
341
339
|
|
|
342
|
-
action_index: dict[int, int] = field(default_factory=dict)
|
|
343
340
|
got_parsed: bool = False
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/theme.py
RENAMED
|
@@ -427,8 +427,8 @@ ACTION_CARD_STATUS_CHECKING = 'color: grey; font-size: 11px;'
|
|
|
427
427
|
ACTION_CARD_STATUS_NEEDED = 'color: palette(text); font-size: 11px; font-weight: bold;'
|
|
428
428
|
"""Status label: Needed."""
|
|
429
429
|
|
|
430
|
-
ACTION_CARD_STATUS_SATISFIED = 'color:
|
|
431
|
-
"""Status label: Already installed."""
|
|
430
|
+
ACTION_CARD_STATUS_SATISFIED = 'color: #6a9955; font-size: 11px;'
|
|
431
|
+
"""Status label: Already installed (muted green with checkmark)."""
|
|
432
432
|
|
|
433
433
|
ACTION_CARD_STATUS_UPDATE = 'color: #d7ba7d; font-size: 11px; font-weight: bold;'
|
|
434
434
|
"""Status label: Update available (amber)."""
|
|
@@ -17,7 +17,6 @@ from PySide6.QtWidgets import QApplication
|
|
|
17
17
|
from synodic_client.application.screen.action_card import (
|
|
18
18
|
ActionCard,
|
|
19
19
|
ActionCardList,
|
|
20
|
-
action_key,
|
|
21
20
|
action_sort_key,
|
|
22
21
|
)
|
|
23
22
|
from synodic_client.application.theme import (
|
|
@@ -43,7 +42,7 @@ def _make_action(
|
|
|
43
42
|
*,
|
|
44
43
|
kind: PluginKind | None = PluginKind.PACKAGE,
|
|
45
44
|
description: str = 'Install requests',
|
|
46
|
-
installer: str = 'pip',
|
|
45
|
+
installer: str | None = 'pip',
|
|
47
46
|
package: str = 'requests',
|
|
48
47
|
**overrides: Any,
|
|
49
48
|
) -> SetupAction:
|
|
@@ -63,7 +62,6 @@ def _make_action(
|
|
|
63
62
|
action.package = pkg_mock
|
|
64
63
|
action.package_description = overrides.get('package_description', description)
|
|
65
64
|
action.command = overrides.get('command')
|
|
66
|
-
action.cli_command = overrides.get('cli_command')
|
|
67
65
|
action.include_prereleases = overrides.get('include_prereleases', False)
|
|
68
66
|
action.plugin_target = overrides.get('plugin_target')
|
|
69
67
|
return action
|
|
@@ -80,7 +78,7 @@ def _make_result(
|
|
|
80
78
|
"""Create a SetupActionResult.
|
|
81
79
|
|
|
82
80
|
Extra keyword arguments (``action``, ``installed_version``,
|
|
83
|
-
``available_version``) are forwarded to the constructor.
|
|
81
|
+
``available_version``, ``cli_command``) are forwarded to the constructor.
|
|
84
82
|
"""
|
|
85
83
|
return SetupActionResult(
|
|
86
84
|
action=overrides.get('action') or _make_action(),
|
|
@@ -90,6 +88,7 @@ def _make_result(
|
|
|
90
88
|
message=message,
|
|
91
89
|
installed_version=overrides.get('installed_version'),
|
|
92
90
|
available_version=overrides.get('available_version'),
|
|
91
|
+
cli_command=overrides.get('cli_command'),
|
|
93
92
|
)
|
|
94
93
|
|
|
95
94
|
|
|
@@ -244,7 +243,7 @@ class TestActionCardCheckResult:
|
|
|
244
243
|
|
|
245
244
|
@staticmethod
|
|
246
245
|
def test_already_installed_status() -> None:
|
|
247
|
-
"""Skipped ALREADY_INSTALLED shows 'Already installed'."""
|
|
246
|
+
"""Skipped ALREADY_INSTALLED shows '\u2713 Already installed'."""
|
|
248
247
|
card = ActionCard()
|
|
249
248
|
card.populate(_make_action())
|
|
250
249
|
result = _make_result(
|
|
@@ -253,7 +252,7 @@ class TestActionCardCheckResult:
|
|
|
253
252
|
installed_version='3.5.2',
|
|
254
253
|
)
|
|
255
254
|
card.set_check_result(result)
|
|
256
|
-
assert card.status_text() == 'Already installed'
|
|
255
|
+
assert card.status_text() == '\u2713 Already installed'
|
|
257
256
|
assert ACTION_CARD_STATUS_SATISFIED in card._status_label.styleSheet()
|
|
258
257
|
|
|
259
258
|
@staticmethod
|
|
@@ -301,6 +300,20 @@ class TestActionCardCheckResult:
|
|
|
301
300
|
card.set_check_result(result)
|
|
302
301
|
assert card._version_label.text() == '3.5.2'
|
|
303
302
|
|
|
303
|
+
@staticmethod
|
|
304
|
+
def test_available_version_only_shown() -> None:
|
|
305
|
+
"""Version label shows '→ target' when only available_version is set."""
|
|
306
|
+
card = ActionCard()
|
|
307
|
+
card.populate(_make_action())
|
|
308
|
+
result = _make_result(
|
|
309
|
+
success=True,
|
|
310
|
+
skipped=False,
|
|
311
|
+
available_version='1.2.0',
|
|
312
|
+
)
|
|
313
|
+
card.set_check_result(result)
|
|
314
|
+
assert '\u2192 1.2.0' in card._version_label.text()
|
|
315
|
+
assert 'grey' in card._version_label.styleSheet()
|
|
316
|
+
|
|
304
317
|
@staticmethod
|
|
305
318
|
def test_finalize_checking_resolves_to_needed() -> None:
|
|
306
319
|
"""finalize_checking stops spinner and changes to 'Needed'."""
|
|
@@ -349,8 +362,7 @@ class TestActionCardCheckFailure:
|
|
|
349
362
|
def test_failed_check_shows_error_tooltip() -> None:
|
|
350
363
|
"""A failed check result surfaces the error message as a tooltip."""
|
|
351
364
|
card = ActionCard()
|
|
352
|
-
action = _make_action(kind=PluginKind.SCM, package='repo')
|
|
353
|
-
action.installer = None # Simulate an unresolved deferred action
|
|
365
|
+
action = _make_action(kind=PluginKind.SCM, package='repo', installer=None)
|
|
354
366
|
card.populate(action)
|
|
355
367
|
msg = "SCM environment 'None' is not available"
|
|
356
368
|
result = _make_result(success=False, skipped=False, message=msg)
|
|
@@ -506,8 +518,7 @@ class TestActionCardList:
|
|
|
506
518
|
"""Populate includes actions with kind=None."""
|
|
507
519
|
card_list = ActionCardList()
|
|
508
520
|
a1 = _make_action(package='pkg1')
|
|
509
|
-
a2 = _make_action(package='pkg2')
|
|
510
|
-
a2.kind = None # bare command
|
|
521
|
+
a2 = _make_action(package='pkg2', kind=None)
|
|
511
522
|
actions = [a1, a2]
|
|
512
523
|
card_list.populate(actions)
|
|
513
524
|
assert card_list.card_count() == len(actions)
|
|
@@ -528,21 +539,6 @@ class TestActionCardList:
|
|
|
528
539
|
assert c1._package_label.text() == 'first'
|
|
529
540
|
assert c2._package_label.text() == 'second'
|
|
530
541
|
|
|
531
|
-
@staticmethod
|
|
532
|
-
def test_get_card_cross_instance() -> None:
|
|
533
|
-
"""get_card works with a different object that has the same content."""
|
|
534
|
-
card_list = ActionCardList()
|
|
535
|
-
original = _make_action(package='numpy', installer='pip')
|
|
536
|
-
card_list.populate([original])
|
|
537
|
-
|
|
538
|
-
# Create a separate mock with the same content fields
|
|
539
|
-
duplicate = _make_action(package='numpy', installer='pip')
|
|
540
|
-
assert original is not duplicate
|
|
541
|
-
|
|
542
|
-
card = card_list.get_card(duplicate)
|
|
543
|
-
assert card is not None
|
|
544
|
-
assert card._package_label.text() == 'numpy'
|
|
545
|
-
|
|
546
542
|
@staticmethod
|
|
547
543
|
def test_get_card_returns_none_for_unknown() -> None:
|
|
548
544
|
"""get_card returns None for an unknown action."""
|
|
@@ -582,7 +578,7 @@ class TestActionCardList:
|
|
|
582
578
|
|
|
583
579
|
card_list.finalize_all_checking()
|
|
584
580
|
|
|
585
|
-
assert c1.status_text() == 'Already installed' # unchanged
|
|
581
|
+
assert c1.status_text() == '\u2713 Already installed' # unchanged
|
|
586
582
|
c2 = card_list.get_card(a2)
|
|
587
583
|
assert c2 is not None
|
|
588
584
|
assert c2.status_text() == 'Needed' # resolved
|
|
@@ -612,53 +608,6 @@ class TestActionCardList:
|
|
|
612
608
|
assert card_list.card_at(-1) is None
|
|
613
609
|
|
|
614
610
|
|
|
615
|
-
# ---------------------------------------------------------------------------
|
|
616
|
-
# action_key — stable identity
|
|
617
|
-
# ---------------------------------------------------------------------------
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
class TestActionKey:
|
|
621
|
-
"""Tests for the action_key function."""
|
|
622
|
-
|
|
623
|
-
@staticmethod
|
|
624
|
-
def test_same_content_same_key() -> None:
|
|
625
|
-
"""Two actions with identical content produce the same key."""
|
|
626
|
-
a = _make_action(package='numpy', installer='pip')
|
|
627
|
-
b = _make_action(package='numpy', installer='pip')
|
|
628
|
-
assert a is not b
|
|
629
|
-
assert action_key(a) == action_key(b)
|
|
630
|
-
|
|
631
|
-
@staticmethod
|
|
632
|
-
def test_different_package_different_key() -> None:
|
|
633
|
-
"""Actions with different packages produce different keys."""
|
|
634
|
-
a = _make_action(package='numpy')
|
|
635
|
-
b = _make_action(package='scipy')
|
|
636
|
-
assert action_key(a) != action_key(b)
|
|
637
|
-
|
|
638
|
-
@staticmethod
|
|
639
|
-
def test_different_installer_different_key() -> None:
|
|
640
|
-
"""Actions with different installers produce different keys."""
|
|
641
|
-
a = _make_action(package='numpy', installer='pip')
|
|
642
|
-
b = _make_action(package='numpy', installer='uv')
|
|
643
|
-
assert action_key(a) != action_key(b)
|
|
644
|
-
|
|
645
|
-
@staticmethod
|
|
646
|
-
def test_different_kind_different_key() -> None:
|
|
647
|
-
"""Actions with different kinds produce different keys."""
|
|
648
|
-
a = _make_action(package='ruff', kind=PluginKind.PACKAGE)
|
|
649
|
-
b = _make_action(package='ruff', kind=PluginKind.TOOL)
|
|
650
|
-
assert action_key(a) != action_key(b)
|
|
651
|
-
|
|
652
|
-
@staticmethod
|
|
653
|
-
def test_command_action_key() -> None:
|
|
654
|
-
"""Command actions include the command in the key."""
|
|
655
|
-
a = _make_action(command=['echo', 'hello'])
|
|
656
|
-
b = _make_action(command=['echo', 'hello'])
|
|
657
|
-
c = _make_action(command=['echo', 'world'])
|
|
658
|
-
assert action_key(a) == action_key(b)
|
|
659
|
-
assert action_key(a) != action_key(c)
|
|
660
|
-
|
|
661
|
-
|
|
662
611
|
# ---------------------------------------------------------------------------
|
|
663
612
|
# ActionCard — CLI command label
|
|
664
613
|
# ---------------------------------------------------------------------------
|
|
@@ -677,11 +626,18 @@ class TestActionCardCommandLabel:
|
|
|
677
626
|
assert not card._command_row.isHidden()
|
|
678
627
|
|
|
679
628
|
@staticmethod
|
|
680
|
-
def
|
|
681
|
-
"""
|
|
629
|
+
def test_explicit_cli_command_from_result() -> None:
|
|
630
|
+
"""set_check_result with cli_command updates the command label."""
|
|
682
631
|
card = ActionCard()
|
|
683
|
-
action = _make_action(
|
|
632
|
+
action = _make_action(package='ruff', installer='pip')
|
|
684
633
|
card.populate(action)
|
|
634
|
+
assert card._command_label.text() == 'pip install ruff'
|
|
635
|
+
|
|
636
|
+
result = _make_result(
|
|
637
|
+
action=action,
|
|
638
|
+
cli_command=('uv', 'tool', 'install', 'ruff'),
|
|
639
|
+
)
|
|
640
|
+
card.set_check_result(result)
|
|
685
641
|
assert card._command_label.text() == 'uv tool install ruff'
|
|
686
642
|
|
|
687
643
|
@staticmethod
|
|
@@ -694,46 +650,30 @@ class TestActionCardCommandLabel:
|
|
|
694
650
|
assert flags & Qt.TextInteractionFlag.TextSelectableByMouse
|
|
695
651
|
|
|
696
652
|
@staticmethod
|
|
697
|
-
def
|
|
698
|
-
"""
|
|
653
|
+
def test_set_check_result_updates_command_label() -> None:
|
|
654
|
+
"""set_check_result with result cli_command updates the command label."""
|
|
699
655
|
card = ActionCard()
|
|
700
656
|
action = _make_action(package='ruff', installer='pip')
|
|
701
657
|
card.populate(action)
|
|
702
658
|
assert card._command_label.text() == 'pip install ruff'
|
|
703
659
|
|
|
704
|
-
|
|
705
|
-
|
|
660
|
+
result = _make_result(
|
|
661
|
+
action=action,
|
|
662
|
+
cli_command=('uv', 'tool', 'install', 'ruff'),
|
|
663
|
+
)
|
|
664
|
+
card.set_check_result(result)
|
|
706
665
|
assert card._command_label.text() == 'uv tool install ruff'
|
|
707
666
|
assert not card._command_row.isHidden()
|
|
708
667
|
|
|
709
|
-
@staticmethod
|
|
710
|
-
def test_update_command_hides_label_when_empty() -> None:
|
|
711
|
-
"""update_command hides the row when the resolved action has no command."""
|
|
712
|
-
card = ActionCard()
|
|
713
|
-
action = _make_action(package='ruff', installer='pip')
|
|
714
|
-
card.populate(action)
|
|
715
|
-
assert not card._command_row.isHidden()
|
|
716
|
-
|
|
717
|
-
empty_action = _make_action(kind=PluginKind.RUNTIME)
|
|
718
|
-
empty_action.cli_command = None
|
|
719
|
-
empty_action.command = None
|
|
720
|
-
empty_action.package = None
|
|
721
|
-
card.update_command(empty_action)
|
|
722
|
-
assert card._command_row.isHidden()
|
|
723
|
-
|
|
724
|
-
@staticmethod
|
|
725
|
-
def test_update_command_noop_on_skeleton() -> None:
|
|
726
|
-
"""update_command does nothing when card is a skeleton."""
|
|
727
|
-
card = ActionCard(skeleton=True)
|
|
728
|
-
action = _make_action(cli_command=['uv', 'tool', 'install', 'ruff'])
|
|
729
|
-
# Should not raise — skeleton simply returns early
|
|
730
|
-
card.update_command(action)
|
|
731
|
-
|
|
732
668
|
@staticmethod
|
|
733
669
|
def test_copy_button_copies_command(monkeypatch: object) -> None:
|
|
734
670
|
"""Clicking the copy button copies the command text to the clipboard."""
|
|
735
671
|
card = ActionCard()
|
|
736
|
-
action = _make_action(
|
|
672
|
+
action = _make_action(
|
|
673
|
+
package='ruff',
|
|
674
|
+
installer='uv',
|
|
675
|
+
command=('uv', 'tool', 'install', 'ruff'),
|
|
676
|
+
)
|
|
737
677
|
card.populate(action)
|
|
738
678
|
|
|
739
679
|
clipboard = QApplication.clipboard()
|
|
@@ -746,7 +686,11 @@ class TestActionCardCommandLabel:
|
|
|
746
686
|
def test_copy_button_shows_feedback() -> None:
|
|
747
687
|
"""Clicking copy shows a check-mark on the button."""
|
|
748
688
|
card = ActionCard()
|
|
749
|
-
action = _make_action(
|
|
689
|
+
action = _make_action(
|
|
690
|
+
package='ruff',
|
|
691
|
+
installer='uv',
|
|
692
|
+
command=('uv', 'tool', 'install', 'ruff'),
|
|
693
|
+
)
|
|
750
694
|
card.populate(action)
|
|
751
695
|
|
|
752
696
|
card._copy_btn.click()
|
|
@@ -936,7 +880,7 @@ class TestActionCardAlreadyLatest:
|
|
|
936
880
|
skip_reason=SkipReason.ALREADY_LATEST,
|
|
937
881
|
)
|
|
938
882
|
)
|
|
939
|
-
assert card.status_text() == 'Already latest'
|
|
883
|
+
assert card.status_text() == '\u2713 Already latest'
|
|
940
884
|
assert ACTION_CARD_STATUS_SATISFIED in card._status_label.styleSheet()
|
|
941
885
|
|
|
942
886
|
@staticmethod
|
|
@@ -974,8 +918,8 @@ class TestActionCardAlreadyLatest:
|
|
|
974
918
|
)
|
|
975
919
|
)
|
|
976
920
|
|
|
977
|
-
assert card_installed.status_text() == 'Already installed'
|
|
978
|
-
assert card_latest.status_text() == 'Already latest'
|
|
921
|
+
assert card_installed.status_text() == '\u2713 Already installed'
|
|
922
|
+
assert card_latest.status_text() == '\u2713 Already latest'
|
|
979
923
|
# Both use the satisfied stylesheet
|
|
980
924
|
assert ACTION_CARD_STATUS_SATISFIED in card_installed._status_label.styleSheet()
|
|
981
925
|
assert ACTION_CARD_STATUS_SATISFIED in card_latest._status_label.styleSheet()
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_install_preview.py
RENAMED
|
@@ -49,7 +49,6 @@ class TestInstallPreviewWindow:
|
|
|
49
49
|
action.installer = installer
|
|
50
50
|
action.package = package
|
|
51
51
|
action.command = None
|
|
52
|
-
action.cli_command = None
|
|
53
52
|
return action
|
|
54
53
|
|
|
55
54
|
@staticmethod
|
|
@@ -85,7 +84,6 @@ class TestFormatCliCommand:
|
|
|
85
84
|
'description': 'Install test',
|
|
86
85
|
'installer': 'pip',
|
|
87
86
|
'package': 'requests',
|
|
88
|
-
'cli_command': None,
|
|
89
87
|
'command': None,
|
|
90
88
|
}
|
|
91
89
|
defaults.update(overrides)
|
|
@@ -96,19 +94,20 @@ class TestFormatCliCommand:
|
|
|
96
94
|
action.installer = defaults['installer']
|
|
97
95
|
action.package = defaults['package']
|
|
98
96
|
action.command = defaults['command']
|
|
99
|
-
action.cli_command = defaults['cli_command']
|
|
100
97
|
return action
|
|
101
98
|
|
|
102
|
-
def
|
|
103
|
-
"""Verify cli_command takes precedence over command and fallback."""
|
|
104
|
-
action = self._make_action(
|
|
105
|
-
|
|
99
|
+
def test_prefers_cli_command_from_result(self) -> None:
|
|
100
|
+
"""Verify result cli_command takes precedence over command and fallback."""
|
|
101
|
+
action = self._make_action()
|
|
102
|
+
result = MagicMock()
|
|
103
|
+
result.cli_command = ('uv', 'pip', 'install', 'requests')
|
|
104
|
+
assert format_cli_command(action, result=result) == 'uv pip install requests'
|
|
106
105
|
|
|
107
106
|
def test_falls_back_to_command(self) -> None:
|
|
108
107
|
"""Verify command is used when cli_command is absent."""
|
|
109
108
|
action = self._make_action(
|
|
110
109
|
kind='TOOL',
|
|
111
|
-
command=
|
|
110
|
+
command=('echo', 'hello'),
|
|
112
111
|
)
|
|
113
112
|
assert format_cli_command(action) == 'echo hello'
|
|
114
113
|
|
|
@@ -150,6 +149,7 @@ class TestInstallWorker:
|
|
|
150
149
|
kind=ProgressEventKind.ACTION_COMPLETED,
|
|
151
150
|
action=action,
|
|
152
151
|
result=result,
|
|
152
|
+
action_index=0,
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
async def mock_stream(*args, **kwargs):
|
|
@@ -389,7 +389,12 @@ class TestPreviewWorkerSignals:
|
|
|
389
389
|
# Dry-run stream yields manifest loaded then one completed event
|
|
390
390
|
manifest_event = ProgressEvent(kind=ProgressEventKind.MANIFEST_LOADED, manifest=preview)
|
|
391
391
|
result = SetupActionResult(action=action, success=True, skipped=False, skip_reason=None)
|
|
392
|
-
completed_event = ProgressEvent(
|
|
392
|
+
completed_event = ProgressEvent(
|
|
393
|
+
kind=ProgressEventKind.ACTION_COMPLETED,
|
|
394
|
+
action=action,
|
|
395
|
+
result=result,
|
|
396
|
+
action_index=0,
|
|
397
|
+
)
|
|
393
398
|
|
|
394
399
|
async def mock_stream(*args: Any, **kwargs: Any) -> Any:
|
|
395
400
|
yield manifest_event
|
|
@@ -448,7 +453,7 @@ class TestPreviewWorkerSignals:
|
|
|
448
453
|
|
|
449
454
|
@staticmethod
|
|
450
455
|
def test_action_checked_maps_correct_rows(tmp_path: Path) -> None:
|
|
451
|
-
"""Verify on_action_checked receives correct row indices via
|
|
456
|
+
"""Verify on_action_checked receives correct row indices via content-based matching."""
|
|
452
457
|
manifest = tmp_path / 'porringer.json'
|
|
453
458
|
manifest.write_text('{}')
|
|
454
459
|
|
|
@@ -466,8 +471,18 @@ class TestPreviewWorkerSignals:
|
|
|
466
471
|
result_a = SetupActionResult(action=action_a, success=True, skipped=False, skip_reason=None)
|
|
467
472
|
|
|
468
473
|
# Stream returns in execution order (b before a), not preview order
|
|
469
|
-
event_b = ProgressEvent(
|
|
470
|
-
|
|
474
|
+
event_b = ProgressEvent(
|
|
475
|
+
kind=ProgressEventKind.ACTION_COMPLETED,
|
|
476
|
+
action=action_b,
|
|
477
|
+
result=result_b,
|
|
478
|
+
action_index=1,
|
|
479
|
+
)
|
|
480
|
+
event_a = ProgressEvent(
|
|
481
|
+
kind=ProgressEventKind.ACTION_COMPLETED,
|
|
482
|
+
action=action_a,
|
|
483
|
+
result=result_a,
|
|
484
|
+
action_index=0,
|
|
485
|
+
)
|
|
471
486
|
|
|
472
487
|
async def mock_stream(*args: Any, **kwargs: Any) -> Any:
|
|
473
488
|
yield manifest_event
|
|
@@ -815,7 +830,6 @@ class TestSCMPreviewActions:
|
|
|
815
830
|
action.installer = 'git'
|
|
816
831
|
action.package = None
|
|
817
832
|
action.command = None
|
|
818
|
-
action.cli_command = None
|
|
819
833
|
return action
|
|
820
834
|
|
|
821
835
|
def test_scm_already_installed_emits_correct_result(self, tmp_path: Path) -> None:
|
|
@@ -838,6 +852,7 @@ class TestSCMPreviewActions:
|
|
|
838
852
|
kind=ProgressEventKind.ACTION_COMPLETED,
|
|
839
853
|
action=action,
|
|
840
854
|
result=result,
|
|
855
|
+
action_index=0,
|
|
841
856
|
)
|
|
842
857
|
|
|
843
858
|
async def mock_stream(*args: Any, **kwargs: Any) -> Any:
|
|
@@ -884,6 +899,7 @@ class TestSCMPreviewActions:
|
|
|
884
899
|
kind=ProgressEventKind.ACTION_COMPLETED,
|
|
885
900
|
action=action,
|
|
886
901
|
result=result,
|
|
902
|
+
action_index=0,
|
|
887
903
|
)
|
|
888
904
|
|
|
889
905
|
async def mock_stream(*args: Any, **kwargs: Any) -> Any:
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_preview_model.py
RENAMED
|
@@ -8,7 +8,6 @@ from unittest.mock import MagicMock
|
|
|
8
8
|
from porringer.schema import SetupAction
|
|
9
9
|
from porringer.schema.plugin import PluginKind
|
|
10
10
|
|
|
11
|
-
from synodic_client.application.screen.action_card import action_key
|
|
12
11
|
from synodic_client.application.screen.schema import ActionState, PreviewModel, PreviewPhase
|
|
13
12
|
from synodic_client.application.uri import normalize_manifest_key
|
|
14
13
|
|
|
@@ -36,7 +35,6 @@ def _make_action(
|
|
|
36
35
|
action.package = pkg_mock
|
|
37
36
|
action.package_description = overrides.get('package_description', description)
|
|
38
37
|
action.command = overrides.get('command')
|
|
39
|
-
action.cli_command = overrides.get('cli_command')
|
|
40
38
|
action.include_prereleases = overrides.get('include_prereleases', False)
|
|
41
39
|
action.plugin_target = overrides.get('plugin_target')
|
|
42
40
|
return action
|
|
@@ -105,7 +103,7 @@ class TestPreviewModel:
|
|
|
105
103
|
state = ActionState(action=_make_action())
|
|
106
104
|
state.status = 'Already installed'
|
|
107
105
|
model.action_states.append(state)
|
|
108
|
-
model.upgradable_keys.add(
|
|
106
|
+
model.upgradable_keys.add(state.action)
|
|
109
107
|
assert model.install_enabled is True
|
|
110
108
|
|
|
111
109
|
@staticmethod
|
|
@@ -149,7 +147,7 @@ class TestPreviewModel:
|
|
|
149
147
|
upgradable = ActionState(action=_make_action(package='c'))
|
|
150
148
|
upgradable.status = 'Update available'
|
|
151
149
|
model.action_states = [needed, satisfied, upgradable]
|
|
152
|
-
model.upgradable_keys.add(
|
|
150
|
+
model.upgradable_keys.add(upgradable.action)
|
|
153
151
|
expected_actionable = 2 # 1 needed + 1 upgradable
|
|
154
152
|
assert model.actionable_count == expected_actionable
|
|
155
153
|
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_update_controller.py
RENAMED
|
@@ -48,6 +48,27 @@ class ModelSpy:
|
|
|
48
48
|
# ---------------------------------------------------------------------------
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
class ModelSpy:
|
|
52
|
+
"""Records signal emissions from an :class:`UpdateModel`."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, model: UpdateModel) -> None:
|
|
55
|
+
"""Connect to *model* signals and record emissions."""
|
|
56
|
+
self.status: list[tuple[str, str]] = []
|
|
57
|
+
self.check_button_enabled: list[bool] = []
|
|
58
|
+
self.restart_visible: list[bool] = []
|
|
59
|
+
self.last_checked: list[str] = []
|
|
60
|
+
|
|
61
|
+
model.status_text_changed.connect(lambda t, s: self.status.append((t, s)))
|
|
62
|
+
model.check_button_enabled_changed.connect(self.check_button_enabled.append)
|
|
63
|
+
model.restart_visible_changed.connect(self.restart_visible.append)
|
|
64
|
+
model.last_checked_changed.connect(self.last_checked.append)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Helpers
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
51
72
|
def _make_config(**overrides: Any) -> ResolvedConfig:
|
|
52
73
|
"""Create a ``ResolvedConfig`` with sensible defaults and optional overrides."""
|
|
53
74
|
defaults: dict[str, Any] = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/config_store.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/init.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/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
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/tray.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/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
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/synodic_client/subprocess_patch.py
RENAMED
|
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.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/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.dev66 → synodic_client-0.0.1.dev67}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev66 → synodic_client-0.0.1.dev67}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|