-
-
Notifications
You must be signed in to change notification settings - Fork 459
Add a tasks manager status for plugins actions and napari processes #8211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
7198ede
433d298
2a412dd
3784987
3711c4c
cd5bb9c
fe708c5
61643a4
db40eb7
44192f5
11498c7
dfa3c92
619a6b5
5b01321
38e4905
12fb26b
45a54aa
61882dc
25d1328
9ad10f6
f508136
33e3e95
617474b
8f55526
fb8473e
f191660
b76e8e8
ea7d4e8
025e50a
77686ee
7f61ada
fbaa5df
e4eea2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| from unittest.mock import Mock | ||
|
|
||
| from napari.utils.task_status import ( | ||
| Status, | ||
| register_task_status, | ||
| task_status_manager, | ||
| update_task_status, | ||
| ) | ||
|
|
||
|
|
||
| def test_task_status(): | ||
| """test task status registration and update using the utils.task_status module.""" | ||
| # Check task status registration | ||
| cancel_callback_mock = Mock() | ||
| task_status_id = register_task_status( | ||
| 'test-task-status', | ||
| Status.BUSY, | ||
| 'Register task status busy', | ||
| cancel_callback=cancel_callback_mock, | ||
| ) | ||
| assert task_status_manager.is_busy() | ||
| assert task_status_manager.get_status() == [ | ||
| 'test-task-status: Register task status busy' | ||
| ] | ||
|
|
||
| # Check task status update | ||
| update_task_status( | ||
| task_status_id, Status.DONE, description='Register task status done' | ||
| ) | ||
| assert not task_status_manager.is_busy() | ||
| assert task_status_manager.get_status() == [] | ||
| update_task_status( | ||
| task_status_id, | ||
| Status.PENDING, | ||
| description='Register task status pending', | ||
| ) | ||
| assert task_status_manager.is_busy() | ||
| assert task_status_manager.get_status() == [ | ||
| 'test-task-status: Register task status pending' | ||
| ] | ||
|
|
||
| # Check cancel behavior | ||
| task_status_manager.cancel_all() | ||
| cancel_callback_mock.assert_called_once() | ||
| assert task_status_manager.get_status() == [] | ||
| assert not task_status_manager.is_busy() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import datetime | ||
| import uuid | ||
| from enum import auto | ||
| from typing import Optional | ||
|
|
||
| from napari.utils.misc import Callable, StringEnum | ||
|
|
||
|
|
||
| class Status(StringEnum): | ||
| PENDING = auto() | ||
| BUSY = auto() | ||
| DONE = auto() | ||
| CANCELLED = auto() | ||
| FAILED = auto() | ||
|
Comment on lines
9
to
14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is also present in https://github.com/napari/napari-plugin-manager/pull/174/files#diff-845746c74ed93a04bf7677b33b197c4a3086400268ab1f18e2223e6c73730ea7R66 (as a fallback?). Same comments apply.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, over
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: This enum should be updated following the discussion at napari/napari-plugin-manager#174 |
||
|
|
||
|
|
||
| class TaskStatusItem: | ||
| def __init__( | ||
| self, | ||
| provider: str, | ||
| status: Status, | ||
| description: str, | ||
| cancel_callback: Optional[Callable] = None, | ||
| ) -> None: | ||
| self.id: uuid.UUID = uuid.uuid4() | ||
| self._provider = provider | ||
| self._timestamp = [self._timestap()] | ||
| self._status = [status] | ||
| self._description = [description] | ||
| self._cancel_callback = cancel_callback | ||
|
|
||
| def _timestap(self) -> str: | ||
| return datetime.datetime.now().isoformat() | ||
|
|
||
| def __str__(self) -> str: | ||
| return f'TaskStatusItem: ({self._provider}, {self.id}, {self._timestamp[-1]}, {self._status[-1]}, {self._description[-1]})' | ||
|
|
||
| def update(self, status: Status, description: str) -> None: | ||
| self._timestamp.append(self._timestap()) | ||
| self._status.append(status) | ||
| self._description.append(description) | ||
|
|
||
| def cancel(self) -> bool: | ||
| self.update(Status.CANCELLED, '') | ||
| if self._cancel_callback is not None: | ||
| return self._cancel_callback() | ||
| return False | ||
|
|
||
| def state(self) -> tuple[str, str, Status, str]: | ||
| return ( | ||
| self._provider, | ||
| self._timestamp[-1], | ||
| self._status[-1], | ||
| self._description[-1], | ||
| ) | ||
|
|
||
|
|
||
| class TaskStatusManager: | ||
| """ | ||
| A task status manager, to store status of long running processes/tasks. | ||
|
|
||
| Only one instance is in general available through napari. | ||
|
|
||
| napari methods and plugins can use it to register and update | ||
| long running tasks. | ||
| """ | ||
|
|
||
| _tasks: dict[uuid.UUID, TaskStatusItem] | ||
|
|
||
| def __init__(self) -> None: | ||
| # Note: we are using a dict here that may not be thread-safe; however | ||
| # given the that the values from it are added/updated using an UUID | ||
| # collision chances are low and it should be ok as long as operations | ||
| # that require its iteration (`is_busy`, `get_status`, `cancel_all`) | ||
| # are done when no task status additions are scheduled (i.e when | ||
| # closing the application). | ||
| self._tasks: dict[uuid.UUID, TaskStatusItem] = {} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any concerns about concurrent access here? Would it be better to have a Queue of small dataclasses that bundle the uuid and the status?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the moment not much since to add and update an item uuids are being used and I think the only point where this dict gets iterated is when calling
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough. I'll let others chime in in case they share these concerns, but for now a comment would suffice. Something along the lines "note we are using a dict here that may not be thread-safe; however given the XXX conditions, it should be ok as long as we only perform XXXX tasks in XXX endpoints". |
||
|
|
||
| def register_task_status( | ||
| self, | ||
| provider: str, | ||
| task_status: Status, | ||
| description: str, | ||
| cancel_callback: Optional[Callable] = None, | ||
| ) -> uuid.UUID: | ||
| item = TaskStatusItem( | ||
| provider, task_status, description, cancel_callback | ||
| ) | ||
| self._tasks[item.id] = item | ||
| return item.id | ||
|
|
||
| def update_task_status( | ||
| self, | ||
| status_id: uuid.UUID, | ||
| task_status: Status, | ||
| description: str = '', | ||
| ) -> bool: | ||
| if status_id in self._tasks: | ||
| item = self._tasks[status_id] | ||
| item.update(task_status, description) | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
| def is_busy(self) -> bool: | ||
| for _, item in self._tasks.items(): | ||
| if item.state()[2] in [Status.PENDING, Status.BUSY]: | ||
dalthviz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return True | ||
| return False | ||
|
|
||
| def get_status(self) -> list[str]: | ||
| messages = [] | ||
| for _, item in self._tasks.items(): | ||
| provider, ts, status, description = item.state() | ||
| if status in [Status.PENDING, Status.BUSY]: | ||
dalthviz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| messages.append(f'{provider}: {description}') | ||
|
|
||
| return messages | ||
|
|
||
| def cancel_all(self) -> None: | ||
| for _, item in self._tasks.items(): | ||
| item.cancel() | ||
|
|
||
|
|
||
| task_status_manager = TaskStatusManager() | ||
|
||
|
|
||
|
|
||
| def register_task_status( | ||
| provider: str, | ||
| task_status: Status, | ||
| description: str, | ||
| cancel_callback: Optional[Callable] = None, | ||
| ) -> uuid.UUID: | ||
| """ | ||
| Register a long running task. | ||
| """ | ||
| return task_status_manager.register_task_status( | ||
| provider, task_status, description, cancel_callback | ||
| ) | ||
|
|
||
|
|
||
| def update_task_status( | ||
| task_status_id: uuid.UUID, | ||
| status: Status, | ||
| description: str = '', | ||
| ) -> bool: | ||
| """ | ||
| Update a long running task. | ||
| """ | ||
| return task_status_manager.update_task_status( | ||
| task_status_id, status, description | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just confirming that this means the tasks busy dialog box will always be shown regardless of the user's preference to show the warning on a regular close
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, as long as there are tasks with a
PENDINGorBUSYstatus the dialog asking to confirm the application close (with a description of the tasks being processed) will be shownThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And it means that it is shown even if action is connected to another napari window?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the napari windows are in the same process I think they could end up sharing the task manager instance indeed 🤔 It is possible to have multiple napari windows launched from the same process? Do you have a code snippet I could use to check this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: the
multiple_viewers_.pyis an example of this possibility