From 95a691180b5eaaad43396e13ed4d60d32976b735 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Mon, 9 Mar 2026 22:42:56 -0700 Subject: [PATCH 1/2] lint --- synodic_client/application/config_store.py | 2 +- .../application/screen/tool_update_controller.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/synodic_client/application/config_store.py b/synodic_client/application/config_store.py index 41ed3fc..86791fb 100644 --- a/synodic_client/application/config_store.py +++ b/synodic_client/application/config_store.py @@ -24,7 +24,7 @@ class ConfigStore(QObject): store = ConfigStore(initial_config) store.changed.connect(some_consumer.on_config_changed) - store.update(auto_apply=False) # persists + emits + store.update(auto_apply=False) # persists + emits """ changed = Signal(object) diff --git a/synodic_client/application/screen/tool_update_controller.py b/synodic_client/application/screen/tool_update_controller.py index ddcb13a..eb7abf0 100644 --- a/synodic_client/application/screen/tool_update_controller.py +++ b/synodic_client/application/screen/tool_update_controller.py @@ -509,7 +509,10 @@ async def _async_single_package_remove( except Exception as exc: logger.exception('Package removal failed') self._fail_package_update( - plugin_name, package_name, f'Failed to remove {package_name}: {exc}', removing=True, + plugin_name, + package_name, + f'Failed to remove {package_name}: {exc}', + removing=True, ) def _on_package_remove_finished( @@ -523,7 +526,10 @@ def _on_package_remove_finished( detail = result.message or 'Unknown error' logger.warning('Package removal failed for %s/%s: %s', plugin_name, package_name, detail) self._fail_package_update( - plugin_name, package_name, f'Could not remove {package_name}: {detail}', removing=True, + plugin_name, + package_name, + f'Could not remove {package_name}: {detail}', + removing=True, ) return From bff958e8c3ec73aa474a2cbb75bd77c3baac5575 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Tue, 10 Mar 2026 12:02:55 -0700 Subject: [PATCH 2/2] Update Chore --- pdm.lock | 8 +- pyproject.toml | 2 +- synodic_client/application/package_state.py | 125 ++++++++++++++++++ synodic_client/application/screen/install.py | 21 ++- .../application/screen/install_workers.py | 1 - synodic_client/application/screen/projects.py | 6 +- synodic_client/application/screen/schema.py | 1 - synodic_client/application/screen/screen.py | 91 ++++++++----- synodic_client/application/screen/settings.py | 10 -- .../screen/tool_update_controller.py | 4 +- synodic_client/resolution.py | 1 - synodic_client/schema.py | 5 - tests/unit/qt/test_gather_packages.py | 3 +- tests/unit/qt/test_install_preview.py | 15 +-- tests/unit/qt/test_settings.py | 27 ---- tests/unit/qt/test_tray_window_show.py | 1 - tests/unit/qt/test_update_controller.py | 1 - tests/unit/test_config.py | 2 - tests/unit/test_resolution.py | 1 - 19 files changed, 218 insertions(+), 107 deletions(-) create mode 100644 synodic_client/application/package_state.py diff --git a/pdm.lock b/pdm.lock index 9c64821..f2d9875 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "build", "lint", "test"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:f567743c6be6eef49f781b1386b32e50eee0b2dab4cb0ba0efc1ec29f5d1ac78" +content_hash = "sha256:18995a70ffa148eee18c62fe7dca60cfe1028305073ededcde318b8bf056d404" [[metadata.targets]] requires_python = ">=3.14,<3.15" @@ -336,7 +336,7 @@ files = [ [[package]] name = "porringer" -version = "0.2.1.dev77" +version = "0.2.1.dev78" requires_python = ">=3.14" summary = "" groups = ["default"] @@ -349,8 +349,8 @@ dependencies = [ "userpath>=1.9.2", ] files = [ - {file = "porringer-0.2.1.dev77-py3-none-any.whl", hash = "sha256:0ef3501d381b05cae54e83b37cdaded22672602bd69d1e2a715837a068ba405a"}, - {file = "porringer-0.2.1.dev77.tar.gz", hash = "sha256:3bfabd5cf2c467c7a792e271c36c05d993e18e01da024f143beadd7a23337f32"}, + {file = "porringer-0.2.1.dev78-py3-none-any.whl", hash = "sha256:557edbcda23f37b7f6b3226ba7d0ec76c31b9aa8f6c1af64a12537b3f0d35433"}, + {file = "porringer-0.2.1.dev78.tar.gz", hash = "sha256:7118736c60b371c5ca8a56fd583acdc5ca263a679eb6ea85fa1fc477525f570d"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index b184aba..52d5731 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ requires-python = ">=3.14, <3.15" dependencies = [ "pyside6>=6.10.2", "packaging>=26.0", - "porringer>=0.2.1.dev77", + "porringer>=0.2.1.dev78", "qasync>=0.28.0", "velopack>=0.0.1444.dev49733", "typer>=0.24.1", diff --git a/synodic_client/application/package_state.py b/synodic_client/application/package_state.py new file mode 100644 index 0000000..f63bb52 --- /dev/null +++ b/synodic_client/application/package_state.py @@ -0,0 +1,125 @@ +"""Shared package update state registry. + +Provides :class:`PackageStateStore`, a centralised record of +per-package update status used by both ToolsView and ProjectsView +so that version/update information discovered in one view is +immediately available to the other. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from PySide6.QtCore import QObject, Signal + + +@dataclass(slots=True) +class PackageState: + """Canonical update state for a single package. + + Keyed by ``(signal_key, name)`` inside :class:`PackageStateStore`. + """ + + name: str + """Package name (e.g. ``"ruff"``).""" + + installed_version: str = '' + """Currently installed version, or empty if unknown.""" + + available_version: str = '' + """Latest available version, or empty if unknown.""" + + has_update: bool = False + """Whether the package has a newer version available.""" + + +class PackageStateStore(QObject): + """Shared registry of package update states across views. + + Both ToolsView and ProjectsView write discovered update information + into the store; each view can then read the canonical state + regardless of which view made the discovery. + + :attr:`state_changed` is emitted whenever new data arrives so + listeners can refresh their badges/labels without polling. + """ + + state_changed = Signal() + """Emitted whenever any package state is created or modified.""" + + def __init__(self, parent: QObject | None = None) -> None: + """Initialise with an empty registry.""" + super().__init__(parent) + self._data: dict[str, dict[str, PackageState]] = {} + + # -- Bulk write (ToolsView check_updates) ---------------------------- + + def set_check_results(self, available: dict[str, dict[str, str]]) -> None: + """Populate from ``check_updates`` results. + + *available* maps ``{signal_key: {package_name: latest_version}}`` + — the same shape previously stored in + ``ToolsView._updates_available``. + """ + self._data.clear() + for key, packages in available.items(): + for pkg_name, latest in packages.items(): + self._data.setdefault(key, {})[pkg_name] = PackageState( + name=pkg_name, + available_version=latest, + has_update=True, + ) + self.state_changed.emit() + + # -- Single-action write (ProjectsView dry-run) ---------------------- + + def record_action_result( + self, + signal_key: str, + pkg_name: str, + *, + installed_version: str = '', + available_version: str = '', + has_update: bool = False, + ) -> None: + """Record a single dry-run result. + + Merges with any existing state so that data discovered by + different views accumulates rather than overwrites. + """ + existing = self._data.get(signal_key, {}).get(pkg_name) + state = PackageState( + name=pkg_name, + installed_version=installed_version or (existing.installed_version if existing else ''), + available_version=available_version or (existing.available_version if existing else ''), + has_update=has_update or (existing.has_update if existing else False), + ) + self._data.setdefault(signal_key, {})[pkg_name] = state + self.state_changed.emit() + + # -- Read API -------------------------------------------------------- + + def get_updates(self, signal_key: str) -> dict[str, str]: + """Return ``{package_name: latest_version}`` for packages with updates. + + Drop-in replacement for ``_updates_available.get(key, {})``. + """ + bucket = self._data.get(signal_key, {}) + return {name: s.available_version for name, s in bucket.items() if s.has_update} + + def has_updates_for(self, signal_key: str) -> bool: + """Return whether any package under *signal_key* has an update.""" + return any(s.has_update for s in self._data.get(signal_key, {}).values()) + + def get(self, signal_key: str, pkg_name: str) -> PackageState | None: + """Return the state for a specific package, or ``None``.""" + return self._data.get(signal_key, {}).get(pkg_name) + + @property + def has_data(self) -> bool: + """Return whether any update data has been recorded.""" + return bool(self._data) + + def clear(self) -> None: + """Remove all recorded state.""" + self._data.clear() diff --git a/synodic_client/application/screen/install.py b/synodic_client/application/screen/install.py index a699581..0b4335b 100644 --- a/synodic_client/application/screen/install.py +++ b/synodic_client/application/screen/install.py @@ -43,6 +43,7 @@ QWidget, ) +from synodic_client.application.package_state import PackageStateStore from synodic_client.application.screen import skip_reason_label from synodic_client.application.screen.action_card import ActionCardList, action_key from synodic_client.application.screen.card import CardFrame @@ -113,6 +114,7 @@ def __init__( *, show_close: bool = True, config: ResolvedConfig | None = None, + package_store: PackageStateStore | None = None, ) -> None: """Initialize the preview widget. @@ -122,11 +124,13 @@ def __init__( show_close: Whether to show the Close button. Set ``False`` when embedding inside a persistent view (e.g. a tab). config: Global configuration for per-manifest pre-release state. + package_store: Shared package update state registry. """ super().__init__(parent) self._porringer = porringer self._show_close = show_close self._config = config + self._package_store = package_store self._discovered_plugins: DiscoveredPlugins | None = None self._model = PreviewModel() @@ -293,7 +297,6 @@ def load( path_or_url: str, *, project_directory: Path | None = None, - detect_updates: bool = True, ) -> None: """Load a manifest preview, or skip if the same manifest is already showing results. @@ -305,7 +308,6 @@ def load( Args: path_or_url: Manifest path or URL. project_directory: Working directory for project sync actions. - detect_updates: Query package indices for newer versions. """ key = normalize_manifest_key(path_or_url) @@ -345,7 +347,6 @@ def load( self._run_preview_task( path_or_url, project_directory=self._model.project_directory, - detect_updates=detect_updates, prerelease_packages=overrides, ), ) @@ -474,7 +475,6 @@ async def _run_preview_task( path_or_url: str, *, project_directory: Path | None = None, - detect_updates: bool = True, prerelease_packages: set[str] | None = None, ) -> None: """Run the preview coroutine and route completion/errors.""" @@ -484,7 +484,6 @@ async def _run_preview_task( path_or_url, config=PreviewConfig( project_directory=project_directory, - detect_updates=detect_updates, prerelease_packages=prerelease_packages, ), callbacks=PreviewCallbacks( @@ -640,6 +639,16 @@ def _on_action_checked(self, row: int, result: SetupActionResult) -> None: if card is not None: card.set_check_result(result) + # Record in shared store so ToolsView can reflect the update + if self._package_store is not None and action.installer and action.package: + self._package_store.record_action_result( + action.installer, + str(action.package.name), + installed_version=result.installed_version or '', + available_version=result.available_version or '', + has_update=result.skip_reason == SkipReason.UPDATE_AVAILABLE, + ) + # Update phase text m.checked_count += 1 total = len(m.action_states) @@ -987,11 +996,9 @@ def start(self) -> None: logger.info('Starting install preview for: %s', self._manifest_url) self._url_label.setText(f'Manifest: {self._manifest_url}') - detect = self._config.detect_updates if self._config else True self._preview_widget.load( self._manifest_url, project_directory=self._project_directory, - detect_updates=detect, ) # --- Callbacks --- diff --git a/synodic_client/application/screen/install_workers.py b/synodic_client/application/screen/install_workers.py index 810a40d..86bb87c 100644 --- a/synodic_client/application/screen/install_workers.py +++ b/synodic_client/application/screen/install_workers.py @@ -232,7 +232,6 @@ async def run_preview( paths=[manifest_path], dry_run=True, project_directory=cfg.project_directory, - detect_updates=cfg.detect_updates, prerelease_packages=cfg.prerelease_packages, ) state = _DispatchState() diff --git a/synodic_client/application/screen/projects.py b/synodic_client/application/screen/projects.py index 4475d61..033e108 100644 --- a/synodic_client/application/screen/projects.py +++ b/synodic_client/application/screen/projects.py @@ -20,6 +20,7 @@ ) from synodic_client.application.data import DataCoordinator +from synodic_client.application.package_state import PackageStateStore from synodic_client.application.screen.install import SetupPreviewWidget from synodic_client.application.screen.schema import PreviewPhase from synodic_client.application.screen.sidebar import ManifestSidebar @@ -48,6 +49,7 @@ def __init__( parent: QWidget | None = None, *, coordinator: DataCoordinator | None = None, + package_store: PackageStateStore | None = None, ) -> None: """Initialize the projects view. @@ -57,11 +59,13 @@ def __init__( parent: Optional parent widget. coordinator: Shared data coordinator for validated directory data. + package_store: Shared package update state registry. """ super().__init__(parent) self._porringer = porringer self._store = store self._coordinator = coordinator + self._package_store = package_store self._refresh_in_progress = False self._pending_select: Path | None = None self._widgets: dict[Path, SetupPreviewWidget] = {} @@ -166,7 +170,6 @@ async def _async_refresh(self) -> None: widget.load( str(path), project_directory=path if path.is_dir() else path.parent, - detect_updates=self._store.config.detect_updates, ) except Exception: @@ -200,6 +203,7 @@ def _create_directory_widgets( self, show_close=False, config=self._store.config, + package_store=self._package_store, ) widget._discovered_plugins = discovered widget.install_finished.connect(self._on_install_finished) diff --git a/synodic_client/application/screen/schema.py b/synodic_client/application/screen/schema.py index 1321c02..ceb96b7 100644 --- a/synodic_client/application/screen/schema.py +++ b/synodic_client/application/screen/schema.py @@ -334,7 +334,6 @@ class PreviewConfig: """Optional execution parameters for :func:`run_preview`.""" project_directory: Path | None = None - detect_updates: bool = True prerelease_packages: set[str] | None = None diff --git a/synodic_client/application/screen/screen.py b/synodic_client/application/screen/screen.py index 76a3836..10074c8 100644 --- a/synodic_client/application/screen/screen.py +++ b/synodic_client/application/screen/screen.py @@ -37,6 +37,7 @@ from synodic_client.application.config_store import ConfigStore from synodic_client.application.data import DataCoordinator from synodic_client.application.icon import app_icon +from synodic_client.application.package_state import PackageStateStore from synodic_client.application.screen.plugin_row import ( FilterChip, PluginKindHeader, @@ -115,6 +116,7 @@ def __init__( parent: QWidget | None = None, *, coordinator: DataCoordinator | None = None, + package_store: PackageStateStore | None = None, ) -> None: """Initialize the tools view. @@ -125,22 +127,25 @@ def __init__( coordinator: Shared data coordinator. When provided, the view delegates plugin/directory fetching to the coordinator instead of calling porringer directly. + package_store: Shared package update state registry. """ super().__init__(parent) self._porringer = porringer self._store = store self._coordinator = coordinator + self._package_store = package_store self._section_widgets: list[QWidget] = [] self._filter_chips: dict[str, FilterChip] = {} self._deselected_plugins: set[str] = set() self._refresh_in_progress = False self._check_in_progress = False - self._updates_checked = False - self._updates_available: dict[str, dict[str, str]] = {} self._directories: list[ManifestDirectory] = [] self._timestamp_timer: QTimer | None = None self._init_ui() + if self._package_store is not None: + self._package_store.state_changed.connect(self._on_package_state_changed) + def _init_ui(self) -> None: """Initialize the UI components.""" outer = QVBoxLayout(self) @@ -234,6 +239,11 @@ def _build_toolbar(self) -> QHBoxLayout: # --- Public API --- + def invalidate_update_data(self) -> None: + """Clear cached update state so the next refresh re-checks.""" + if self._package_store is not None: + self._package_store.clear() + def refresh(self) -> None: """Schedule an asynchronous rebuild of the tool list.""" if self._refresh_in_progress: @@ -257,7 +267,7 @@ async def _async_refresh(self) -> None: try: data = await self._gather_refresh_data() - need_deferred_check = not self._updates_checked + need_deferred_check = not self._has_update_data self._build_widget_tree(data) except Exception: logger.exception('Failed to refresh tools') @@ -272,7 +282,7 @@ async def _async_refresh(self) -> None: # Fire-and-forget: detect updates in the background, then patch # the just-rendered widget tree with update badges. if need_deferred_check: - asyncio.create_task(self._deferred_update_check(self._directories)) + asyncio.create_task(self._deferred_update_check()) # ------------------------------------------------------------------ # _async_refresh helper methods @@ -445,7 +455,6 @@ def _build_runtime_sections( return auto_val = auto_update_map.get(plugin.name, True) - plugin_updates = self._updates_available.get(plugin.name, {}) tool_timestamps = self._store.config.last_tool_updates or {} default_exe = data.default_runtime_executable @@ -462,11 +471,14 @@ def _sort_key(rt: RuntimePackageResult) -> tuple[int, str]: if is_default: tag_text += ' (default)' + # Runtime updates use composite keys "plugin:tag" + rt_updates = self._get_plugin_updates(f'{plugin.name}:{rt.tag}') + provider = PluginProviderHeader( plugin, auto_val is not False, show_controls=True, - has_updates=bool(plugin_updates), + has_updates=bool(rt_updates), parent=self._container, ) provider.set_runtime(rt.tag, label=tag_text) @@ -496,7 +508,7 @@ def _sort_key(rt: RuntimePackageResult) -> tuple[int, str]: plugin_name=plugin.name, auto_update=pkg_auto, show_toggle=True, - has_update=pkg.name in plugin_updates, + has_update=pkg.name in rt_updates, is_global=True, host_tool=pkg.host_tool, runtime_tag=rt.tag, @@ -518,7 +530,7 @@ def _build_plugin_section( ``ProjectChildRow`` widgets are no longer used. """ auto_val = auto_update_map.get(plugin.name, True) - plugin_updates = self._updates_available.get(plugin.name, {}) + plugin_updates = self._get_plugin_updates(plugin.name) provider = PluginProviderHeader( plugin, @@ -1121,11 +1133,15 @@ def _on_check_for_updates(self) -> None: asyncio.create_task(self._run_inline_update_check()) async def _run_inline_update_check(self) -> None: - """Check for updates with inline spinners (no overlay / rebuild).""" + """Check for updates with inline spinners (no overlay / rebuild). + + Used by both the manual *Check for Updates* button and the + automatic deferred check after initial refresh. + """ self._check_in_progress = True try: - self._updates_available = await self._check_for_updates(self._directories) - self._updates_checked = True + available = await self._check_for_updates(self._directories) + self._store_check_results(available) self._apply_update_badges() except Exception: logger.debug('Inline update check failed', exc_info=True) @@ -1225,7 +1241,6 @@ async def _check_directory_updates( params = SetupParameters( paths=[str(manifest_path)], dry_run=True, - detect_updates=True, project_directory=path, ) async for event in self._porringer.sync.execute_stream(params): @@ -1247,31 +1262,42 @@ async def _check_directory_updates( ) return available - async def _deferred_update_check( - self, - directories: list[ManifestDirectory], - ) -> None: + async def _deferred_update_check(self) -> None: """Run update detection in the background, then patch the widget tree. Called after the initial render so the user sees the tool list immediately while update badges are populated asynchronously. - Inline per-row spinners provide visual feedback. + Delegates to :meth:`_run_inline_update_check`. """ - self._check_in_progress = True self._check_btn.setEnabled(False) self._check_btn.setText('Checking\u2026') self._set_all_checking(True) - try: - self._updates_available = await self._check_for_updates(directories) - self._updates_checked = True + await self._run_inline_update_check() + + def _get_plugin_updates(self, signal_key: str) -> dict[str, str]: + """Return ``{package_name: latest_version}`` for *signal_key*. + + Reads from the shared :class:`PackageStateStore` when available, + otherwise returns an empty dict. + """ + if self._package_store is not None: + return self._package_store.get_updates(signal_key) + return {} + + def _store_check_results(self, available: dict[str, dict[str, str]]) -> None: + """Push check results into the store.""" + if self._package_store is not None: + self._package_store.set_check_results(available) + + @property + def _has_update_data(self) -> bool: + """Return whether update data has been fetched at least once.""" + return self._package_store is not None and self._package_store.has_data + + def _on_package_state_changed(self) -> None: + """Re-apply badges when another view writes to the shared store.""" + if not self._check_in_progress and not self._refresh_in_progress: self._apply_update_badges() - except Exception: - logger.debug('Deferred update check failed', exc_info=True) - finally: - self._set_all_checking(False) - self._check_btn.setEnabled(True) - self._check_btn.setText('Check for Updates') - self._check_in_progress = False def _apply_update_badges(self) -> None: """Walk existing widgets and show/hide Update buttons + set inline status.""" @@ -1279,12 +1305,12 @@ def _apply_update_badges(self) -> None: for widget in self._section_widgets: if isinstance(widget, PluginProviderHeader): current_plugin = widget._signal_key - plugin_updates = self._updates_available.get(current_plugin, {}) + plugin_updates = self._get_plugin_updates(current_plugin) has = bool(plugin_updates) if widget._update_btn is not None: widget._update_btn.setVisible(has) elif isinstance(widget, PluginRow) and widget._plugin_name: - plugin_updates = self._updates_available.get(widget._signal_key, {}) + plugin_updates = self._get_plugin_updates(widget._signal_key) latest_version = plugin_updates.get(widget._package_name) has_update = latest_version is not None @@ -1295,7 +1321,7 @@ def _apply_update_badges(self) -> None: if has_update: version_text = f'v{latest_version} available' if latest_version else 'Update available' widget.set_update_status(version_text, PLUGIN_ROW_STATUS_AVAILABLE_STYLE) - elif self._updates_checked: + elif self._has_update_data: widget.set_update_status('Up to date', PLUGIN_ROW_STATUS_UP_TO_DATE_STYLE) def _set_all_checking(self, checking: bool) -> None: @@ -1401,6 +1427,7 @@ def __init__( self._porringer = porringer self._store = store self._coordinator: DataCoordinator | None = DataCoordinator(porringer) if porringer is not None else None + self._package_store: PackageStateStore | None = PackageStateStore(self) if porringer is not None else None self.setWindowTitle('Synodic Client') self.setMinimumSize(*MAIN_WINDOW_MIN_SIZE) self.setWindowIcon(app_icon()) @@ -1453,6 +1480,7 @@ def show(self) -> None: self._store, self, coordinator=self._coordinator, + package_store=self._package_store, ) self._tabs.addTab(self._projects_view, 'Projects') @@ -1461,6 +1489,7 @@ def show(self) -> None: self._store, self, coordinator=self._coordinator, + package_store=self._package_store, ) self._tabs.addTab(self._tools_view, 'Tools') self.tools_view_created.emit(self._tools_view) diff --git a/synodic_client/application/screen/settings.py b/synodic_client/application/screen/settings.py index b058cc6..fbc5af1 100644 --- a/synodic_client/application/screen/settings.py +++ b/synodic_client/application/screen/settings.py @@ -182,11 +182,6 @@ def _add_update_controls(self, content: QVBoxLayout) -> None: row.addStretch() content.addLayout(row) - # Detect updates during previews - self._detect_updates_check = QCheckBox('Detect updates during previews') - self._detect_updates_check.toggled.connect(self._on_detect_updates_changed) - content.addWidget(self._detect_updates_check) - # Automatically apply updates self._auto_apply_check = QCheckBox('Automatically apply updates') self._auto_apply_check.toggled.connect(self._on_auto_apply_changed) @@ -266,7 +261,6 @@ def sync_from_config(self) -> None: self._tool_update_spin.setValue(config.tool_update_interval_minutes) # Checkboxes - self._detect_updates_check.setChecked(config.detect_updates) self._auto_apply_check.setChecked(config.auto_apply) self._auto_start_check.setChecked(is_startup_registered()) @@ -351,7 +345,6 @@ def _block_signals(self) -> Iterator[None]: self._source_edit, self._auto_update_spin, self._tool_update_spin, - self._detect_updates_check, self._auto_apply_check, self._auto_start_check, self._debug_logging_check, @@ -390,9 +383,6 @@ def _on_auto_update_interval_changed(self, value: int) -> None: def _on_tool_update_interval_changed(self, value: int) -> None: self._persist(tool_update_interval_minutes=value) - def _on_detect_updates_changed(self, checked: bool) -> None: - self._persist(detect_updates=checked) - def _on_auto_apply_changed(self, checked: bool) -> None: self._persist(auto_apply=checked) diff --git a/synodic_client/application/screen/tool_update_controller.py b/synodic_client/application/screen/tool_update_controller.py index eb7abf0..ccc3588 100644 --- a/synodic_client/application/screen/tool_update_controller.py +++ b/synodic_client/application/screen/tool_update_controller.py @@ -438,7 +438,7 @@ def _on_tool_update_finished( # not None) call show() below which triggers the refresh. tools_view = self._window.tools_view if tools_view is not None: - tools_view._updates_checked = False + tools_view.invalidate_update_data() if self._window.isVisible() and target is None: tools_view.refresh() @@ -538,5 +538,5 @@ def _on_package_remove_finished( tools_view = self._window.tools_view if tools_view is not None: tools_view.set_package_removing(plugin_name, package_name, False) - tools_view._updates_checked = False + tools_view.invalidate_update_data() tools_view.refresh() diff --git a/synodic_client/resolution.py b/synodic_client/resolution.py index 0316122..9bbed91 100644 --- a/synodic_client/resolution.py +++ b/synodic_client/resolution.py @@ -122,7 +122,6 @@ def _resolve_from_user(user: UserConfig) -> ResolvedConfig: auto_update_interval_minutes=auto_interval, tool_update_interval_minutes=tool_interval, plugin_auto_update=user.plugin_auto_update, - detect_updates=user.detect_updates, prerelease_packages=user.prerelease_packages, auto_apply=auto_apply, auto_start=auto_start, diff --git a/synodic_client/schema.py b/synodic_client/schema.py index d607a46..2cb1354 100644 --- a/synodic_client/schema.py +++ b/synodic_client/schema.py @@ -78,10 +78,6 @@ class UserConfig(BaseModel): # ``None`` or absent means all plugins auto-update with manifest-aware defaults. plugin_auto_update: dict[str, bool | dict[str, bool]] | None = None - # Check for updates during dry-run previews. When True the preview - # will query package indices for newer versions. - detect_updates: bool = True - # Per-manifest pre-release overrides. Outer key is a normalised # manifest path (or URL for remote manifests) produced by # ``normalize_manifest_key()``. Inner value is a sorted list of @@ -231,7 +227,6 @@ class ResolvedConfig: auto_update_interval_minutes: int tool_update_interval_minutes: int plugin_auto_update: dict[str, bool | dict[str, bool]] | None - detect_updates: bool prerelease_packages: dict[str, list[str]] | None auto_apply: bool auto_start: bool diff --git a/tests/unit/qt/test_gather_packages.py b/tests/unit/qt/test_gather_packages.py index b67730f..70f6def 100644 --- a/tests/unit/qt/test_gather_packages.py +++ b/tests/unit/qt/test_gather_packages.py @@ -1,4 +1,4 @@ -"""Tests for ToolsView._gather_packages global + per-directory queries.""" +"""Tests for ToolsView._gather_packages global + per-directory queries.""" from __future__ import annotations @@ -45,7 +45,6 @@ def _make_config() -> ResolvedConfig: auto_update_interval_minutes=60, tool_update_interval_minutes=60, plugin_auto_update=None, - detect_updates=False, prerelease_packages=None, auto_apply=True, auto_start=False, diff --git a/tests/unit/qt/test_install_preview.py b/tests/unit/qt/test_install_preview.py index d6e03a5..bc144ec 100644 --- a/tests/unit/qt/test_install_preview.py +++ b/tests/unit/qt/test_install_preview.py @@ -662,12 +662,12 @@ async def mock_stream(*args: Any, **kwargs: Any) -> Any: assert order == ['parsed', 'plugins', 'ready'] -class TestPreviewWorkerUpdateDetection: - """Tests for run_preview passing update-detection flags to porringer.""" +class TestPreviewWorkerPrerelease: + """Tests for run_preview passing prerelease config to porringer.""" @staticmethod - def test_passes_detect_updates_and_prerelease_packages(tmp_path: Path) -> None: - """Verify detect_updates and prerelease_packages are forwarded to SetupParameters.""" + def test_passes_prerelease_packages(tmp_path: Path) -> None: + """Verify prerelease_packages are forwarded to SetupParameters.""" manifest = tmp_path / 'porringer.json' manifest.write_text('{}') @@ -688,19 +688,17 @@ async def mock_stream(params: Any, **kwargs: Any) -> Any: porringer, str(manifest), config=PreviewConfig( - detect_updates=True, prerelease_packages={'some-pkg'}, ), ), ) assert len(captured_params) == 1 - assert captured_params[0].detect_updates is True assert captured_params[0].prerelease_packages == {'some-pkg'} @staticmethod - def test_defaults_detect_updates_true(tmp_path: Path) -> None: - """Verify detect_updates defaults to True.""" + def test_defaults_prerelease_none(tmp_path: Path) -> None: + """Verify prerelease_packages defaults to None.""" manifest = tmp_path / 'porringer.json' manifest.write_text('{}') @@ -719,7 +717,6 @@ async def mock_stream(params: Any, **kwargs: Any) -> Any: asyncio.run(run_preview(porringer, str(manifest))) assert len(captured_params) == 1 - assert captured_params[0].detect_updates is True assert captured_params[0].prerelease_packages is None diff --git a/tests/unit/qt/test_settings.py b/tests/unit/qt/test_settings.py index 7eec9d0..4e5ff1a 100644 --- a/tests/unit/qt/test_settings.py +++ b/tests/unit/qt/test_settings.py @@ -24,7 +24,6 @@ def _make_config(**overrides: Any) -> ResolvedConfig: 'auto_update_interval_minutes': DEFAULT_AUTO_UPDATE_INTERVAL_MINUTES, 'tool_update_interval_minutes': DEFAULT_TOOL_UPDATE_INTERVAL_MINUTES, 'plugin_auto_update': None, - 'detect_updates': True, 'prerelease_packages': None, 'auto_apply': True, 'auto_start': True, @@ -144,20 +143,6 @@ def test_tool_update_interval_custom() -> None: window.sync_from_config() assert window._tool_update_spin.value() == custom_interval - @staticmethod - def test_detect_updates_true_default() -> None: - """Default detect_updates is checked.""" - window = _make_window(_make_config()) - window.sync_from_config() - assert window._detect_updates_check.isChecked() is True - - @staticmethod - def test_detect_updates_false() -> None: - """Disabled detect_updates is unchecked.""" - window = _make_window(_make_config(detect_updates=False)) - window.sync_from_config() - assert window._detect_updates_check.isChecked() is False - @staticmethod def test_auto_start_reflects_registry() -> None: """Auto-start checkbox mirrors the OS registration state.""" @@ -250,18 +235,6 @@ def test_tool_update_interval_change() -> None: mock_update.assert_called_with(tool_update_interval_minutes=new_interval) - @staticmethod - def test_detect_updates_change() -> None: - """Toggling detect_updates saves via the store.""" - config = _make_config() - window = _make_window(config) - window.sync_from_config() - - with patch.object(window._store, 'update') as mock_update: - window._detect_updates_check.setChecked(False) - - mock_update.assert_called_with(detect_updates=False) - @staticmethod def test_auto_start_registers_startup_when_frozen() -> None: """Enabling auto-start calls register_startup in frozen builds.""" diff --git a/tests/unit/qt/test_tray_window_show.py b/tests/unit/qt/test_tray_window_show.py index e6b1307..6c4b449 100644 --- a/tests/unit/qt/test_tray_window_show.py +++ b/tests/unit/qt/test_tray_window_show.py @@ -20,7 +20,6 @@ def _make_config() -> ResolvedConfig: auto_update_interval_minutes=DEFAULT_AUTO_UPDATE_INTERVAL_MINUTES, tool_update_interval_minutes=DEFAULT_TOOL_UPDATE_INTERVAL_MINUTES, plugin_auto_update=None, - detect_updates=True, prerelease_packages=None, auto_apply=True, auto_start=True, diff --git a/tests/unit/qt/test_update_controller.py b/tests/unit/qt/test_update_controller.py index 51a3ffa..f4bf50a 100644 --- a/tests/unit/qt/test_update_controller.py +++ b/tests/unit/qt/test_update_controller.py @@ -35,7 +35,6 @@ def _make_config(**overrides: Any) -> ResolvedConfig: 'auto_update_interval_minutes': DEFAULT_AUTO_UPDATE_INTERVAL_MINUTES, 'tool_update_interval_minutes': DEFAULT_TOOL_UPDATE_INTERVAL_MINUTES, 'plugin_auto_update': None, - 'detect_updates': True, 'prerelease_packages': None, 'auto_apply': True, 'auto_start': True, diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 1709cce..4ed06a8 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -45,7 +45,6 @@ def test_defaults() -> None: assert config.auto_update_interval_minutes is None assert config.tool_update_interval_minutes is None assert config.plugin_auto_update is None - assert config.detect_updates is True assert config.prerelease_packages is None assert config.auto_apply is None assert config.auto_start is None @@ -170,7 +169,6 @@ def test_saves_all_fields(tmp_path: Path) -> None: assert data['update_channel'] == 'dev' assert 'update_source' in data assert 'auto_update_interval_minutes' in data - assert 'detect_updates' in data @staticmethod def test_creates_directory(tmp_path: Path) -> None: diff --git a/tests/unit/test_resolution.py b/tests/unit/test_resolution.py index c59707e..00008aa 100644 --- a/tests/unit/test_resolution.py +++ b/tests/unit/test_resolution.py @@ -34,7 +34,6 @@ def _make_resolved(**overrides: Any) -> ResolvedConfig: 'auto_update_interval_minutes': DEFAULT_AUTO_UPDATE_INTERVAL_MINUTES, 'tool_update_interval_minutes': DEFAULT_TOOL_UPDATE_INTERVAL_MINUTES, 'plugin_auto_update': None, - 'detect_updates': True, 'prerelease_packages': None, 'auto_apply': True, 'auto_start': True,