Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ff6598b
Add utility function to query the plugin status from napari side and …
goanpeca Jun 17, 2024
2bc5f7a
Add import check
goanpeca Jun 17, 2024
6bf33ae
Add import check
goanpeca Jun 17, 2024
b1c3297
Fix imports and format
goanpeca Jul 29, 2024
6f30a71
Add registration and unregistration of processes
goanpeca Jul 29, 2024
7af047b
Cleas up status on refresh
goanpeca Jul 29, 2024
3edb8fa
Change to stringenum
goanpeca Jul 29, 2024
7515662
Rename methods
goanpeca Jul 29, 2024
de0e9f5
Use updated api
goanpeca Jul 29, 2024
eb4e191
Fix typing
goanpeca Jul 29, 2024
2f1a8e7
Merge branch 'main' into enh/napari-status
dalthviz Aug 12, 2025
03cbcbe
Update logic from process to task status
dalthviz Aug 18, 2025
80e7932
Clean up register/update status logic
dalthviz Aug 18, 2025
ee41c9d
Wait for processes to get ended to prevent QProcess destroyed warning…
dalthviz Aug 18, 2025
9542081
Docstring clean up and remove base query_status implementation
dalthviz Aug 19, 2025
cd66266
Add cancelled status
dalthviz Aug 19, 2025
0030bc3
Fixes for actions done after initial task id is created
dalthviz Aug 19, 2025
26506fe
Move waitForFinished process call to be done always after process get…
dalthviz Aug 20, 2025
aefa67e
Testing
dalthviz Aug 20, 2025
7d71e3a
Revert test changes. Call waitForFinished only for cancel all operation
dalthviz Aug 20, 2025
45e3674
Set task status following processes exit codes
dalthviz Aug 26, 2025
1286c0c
Fix widget visibility status/message definition
dalthviz Aug 26, 2025
e16ae29
Handle NoneType exit code for PySide2 cancel_all operation
dalthviz Aug 26, 2025
db7e503
Merge branch 'main' into enh/napari-status
dalthviz Aug 27, 2025
927299e
Update Status enum values
dalthviz Aug 28, 2025
0dae292
Merge branch 'enh/napari-status' of https://github.com/dalthviz/napar…
dalthviz Aug 28, 2025
1454f1c
Merge branch 'main' into enh/napari-status
dalthviz Sep 2, 2025
d2eee7f
Follow changes over napari PR where the status manager is created per…
dalthviz Sep 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/napari_plugin_manager/_tests/test_qt_plugin_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,26 @@ def test_import_plugins(plugin_dialog, tmp_path, qtbot):
path.write_text('requests\npackaging\n')
with qtbot.waitSignal(plugin_dialog.installer.allFinished, timeout=60_000):
plugin_dialog.import_plugins(str(path))


def test_query_status(plugin_dialog, monkeypatch):
status, description = plugin_dialog.query_status()
assert status == qt_plugin_dialog.Status.DONE
assert not description

monkeypatch.setattr(
plugin_dialog.installer,
'_queue',
['mock'],
)
status, description = plugin_dialog.query_status()
assert status == qt_plugin_dialog.Status.BUSY
assert description

monkeypatch.setattr(
plugin_dialog.installer,
'_queue',
['mock', 'other-mock'],
)
assert status == qt_plugin_dialog.Status.BUSY
assert description
1 change: 1 addition & 0 deletions src/napari_plugin_manager/base_qt_package_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ def cancel_all(self):
process.errorOccurred.disconnect(self._on_error_occurred)

self._end_process(process)
process.waitForFinished()

self._queue.clear()
self._current_process = None
Expand Down
65 changes: 62 additions & 3 deletions src/napari_plugin_manager/base_qt_plugin_dialog.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import contextlib
import importlib.metadata
import os
import uuid
import webbrowser
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from enum import Enum, auto
from functools import partial
from logging import getLogger
from typing import (
Expand Down Expand Up @@ -61,6 +63,14 @@
log = getLogger(__name__)


class Status(Enum):
PENDING = auto()
BUSY = auto()
DONE = auto()
CANCELLED = auto()
FAILED = auto()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this "Done" but exit_code != 0 or "failed to start"? With non-subprocess tasks I think it's easy to tell, but I'm not sure what it means in a subprocess-based task.

In other words, how can we identify:

  • Tasks that could start and then finished with exit code 0
  • Tasks that could start and then finished with exit code != 0
  • Tasks that failed to even start

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that a status DONE should only be used for successful finished task and otherwise a FAILED status should be set. So, taking into account the cases above:

Tasks that could start and then finished with exit code 0

That corresponds to a DONE status

Tasks that could start and then finished with exit code != 0
Tasks that failed to even start

Those cases correspond to a FAILED status. However, if a more granular set of statuses should be created then for the cases above maybe something like:

Tasks that could start and then finished with exit code != 0

FAILED status

Tasks that failed to even start

START_FAILED status

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think START_FAILED makes sense, but I'd like to hear from others too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like having these be more explicit variable names would be great.
for example COMPLETED instead of DONE implies more success to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So It would be better to have something like:

  • PENDING
  • BUSY
  • COMPLETED
  • FAILED
  • START_FAILED

?

Are there are any other statuses/names that could be preferred? Or maybe removed/added? Let me know!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was checking how Qt handles this and they have several enums:

I think our enum is merging several of these, perhaps orthogonal, states.

  • PENDING means ProcessState.NotRunning, plus our own notion that we are waiting for it in queue.
  • BUSY means ProcessState.Starting || ProcessState.Running.
  • COMPLETED means ProcessState.NotRunning && ExitStatus.NormalExit && .exitCode() == 0
  • FAILED means ProcessState.NotRunning && ( (ExitStatus.NormalExit && .exitCode() != 0) || ExitStatus.CrashedExit || any ProcessError that is not FailedToStart).
  • START_FAILED means ProcessState.NotRunning && ProcessError.FailedToStart.

Are we ok with FAILED meaning all those possible cases? I think it's ok and we don't further granularity, but I wanted to bring this to our attention to show how complex it can get if we start thinking about it more thoroughly.



class PackageMetadataProtocol(Protocol):
"""
Protocol class defining the minimum atributtes/properties needed for package metadata.
Expand Down Expand Up @@ -1048,6 +1058,7 @@ def __init__(self, parent=None, prefix=None) -> None:
self._plugin_data = [] # Store all plugin data
self._filter_texts = []
self._filter_idxs_cache = set()
self._task_status_id = None
self.worker = None
self._plugin_data_map = {}
self._add_items_timer = QTimer(self)
Expand Down Expand Up @@ -1130,13 +1141,33 @@ def _update_theme(self, event: Any) -> None:
"""
raise NotImplementedError

def _register_task_status(self):
status, description = self.query_status()

if self._task_status_id is not None:
self._update_task_status(status, description=description)
return

self._task_status_id = self.register_task_status(
status, description, cancel_callback=self.installer.cancel_all
)

def _update_task_status(
self, status: Status = Status.DONE, description: str = ''
):
if self._task_status_id is not None:
self.update_task_status(
self._task_status_id, status, description=description
)

def _on_installer_start(self):
"""Updates dialog buttons and status when installing a plugin."""
self.cancel_all_btn.setVisible(True)
self.working_indicator.show()
self.process_success_indicator.hide()
self.process_error_indicator.hide()
self.refresh_button.setDisabled(True)
self._register_task_status()

def _on_process_finished(self, process_finished_data: ProcessFinishedData):
action = process_finished_data['action']
Expand Down Expand Up @@ -1210,6 +1241,7 @@ def _on_installer_all_finished(self, exit_codes):
self._trans('Plugin Manager: process completed\n')
)

self._update_task_status()
self.search()

def _add_to_installed(
Expand Down Expand Up @@ -1774,7 +1806,7 @@ def search(self, text: str | None = None, skip=False) -> None:
if len(text.strip()) == 0:
self.installed_list.filter('')
self.available_list.hideAll()
self._plugin_queue = None
self._plugin_queue = []
self._add_items_timer.stop()
self._plugins_found = 0
else:
Expand All @@ -1792,7 +1824,7 @@ def search(self, text: str | None = None, skip=False) -> None:
self._plugins_found = len(items)
self._add_items_timer.start()
else:
self._plugin_queue = None
self._plugin_queue = []
self._add_items_timer.stop()
self._plugins_found = 0

Expand All @@ -1811,6 +1843,7 @@ def refresh(self, clear_cache: bool = False):
self._plugin_queue = []
self._plugin_data = []
self._plugin_data_map = {}
self._latest_status = None

self.installed_list.clear()
self.available_list.clear()
Expand Down Expand Up @@ -1868,4 +1901,30 @@ def import_plugins(self, fpath: str) -> None:
plugins = [p for p in plugins if p]
self._install_packages(plugins)

def register_task_status(
self,
status: Status,
description: str,
cancel_callback: Callable | None = None,
) -> uuid.UUID:
"""Register a task status for the plugin manager."""
raise NotImplementedError

def update_task_status(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm this method name and _update_task_status are leaving me with confusion. Could we find some better names to help it not slide off my smooth brain so easily.

Copy link
Member Author

@dalthviz dalthviz Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecking the code I think it should be possible to only have the update_task_status public method defined. Could having only the public method make things more clear? Let me know if that could help with the confusion!

Edit: Or let me know if there is a naming that could be less confusing!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any suggestion for the naming here @TimMonko ? Or do you think that leaving only the public method could help make the logic easier to follow? Let me know!

self, task_status_id: uuid.UUID, status: Status, description: str = ''
) -> bool:
"""Update task status for the plugin manager."""
raise NotImplementedError

def query_status(self) -> tuple[Status, str]:
"""
Return the current status of plugins installations.

Returns
-------
A tuple containing the current status (`Status`) and a description.

"""
raise NotImplementedError

# endregion - Public methods
64 changes: 64 additions & 0 deletions src/napari_plugin_manager/qt_plugin_dialog.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import sys
import uuid
from collections.abc import Callable
from pathlib import Path

import napari.plugins
Expand Down Expand Up @@ -37,6 +39,31 @@
from napari_plugin_manager.qt_package_installer import NapariInstallerQueue
from napari_plugin_manager.utils import is_conda_package

try:
from napari.utils.task_status import (
Status,
register_task_status,
update_task_status,
)
except ImportError:
from napari_plugin_manager.base_qt_plugin_dialog import Status

def register_task_status(
provider: str,
task_status: Status,
description: str,
cancel_callback: Callable | None = None,
) -> uuid.UUID:
pass

def update_task_status(
task_status_id: uuid.UUID,
status: Status,
description: str = '',
) -> bool:
pass


# Scaling factor for each list widget item when expanding.
STYLES_PATH = Path(__file__).parent / 'styles.qss'
DISMISS_WARN_PYPI_INSTALL_DLG = False
Expand Down Expand Up @@ -265,6 +292,43 @@ def _show_warning(self, warning):
def _trans(self, text, **kwargs):
return trans._(text, **kwargs)

def register_task_status(
self,
task_status: Status,
description: str,
cancel_callback: Callable | None = None,
) -> uuid.UUID:
return register_task_status(
'napari-plugin-manager',
task_status,
description,
cancel_callback=cancel_callback,
)

def update_task_status(
self,
task_status_id: uuid.UUID,
status: Status,
description: str = '',
) -> bool:
return update_task_status(
task_status_id, status, description=description
)

def query_status(self) -> tuple[Status, str]:
if self.installer.hasJobs():
task_status = Status.BUSY
description = trans._n(
'The plugin manager is currently busy with {n} task.',
'The plugin manager is currently busy with {n} tasks.',
n=self.installer.currentJobs(),
)
else:
task_status = Status.DONE
description = ''

return task_status, description


if __name__ == '__main__':
from qtpy.QtWidgets import QApplication
Expand Down
Loading