napari-plugin-manager 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl
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.
- napari_plugin_manager/_tests/test_installer_process.py +1 -1
- napari_plugin_manager/_tests/test_npe2api.py +17 -9
- napari_plugin_manager/_tests/test_qt_plugin_dialog.py +112 -49
- napari_plugin_manager/_version.py +9 -4
- napari_plugin_manager/base_qt_package_installer.py +97 -37
- napari_plugin_manager/base_qt_plugin_dialog.py +72 -20
- napari_plugin_manager/npe2api.py +1 -2
- napari_plugin_manager/qt_package_installer.py +3 -3
- napari_plugin_manager/qt_plugin_dialog.py +32 -52
- napari_plugin_manager/qt_warning_dialog.py +19 -0
- napari_plugin_manager/utils.py +1 -2
- {napari_plugin_manager-0.1.4.dist-info → napari_plugin_manager-0.1.5.dist-info}/METADATA +26 -6
- napari_plugin_manager-0.1.5.dist-info/RECORD +23 -0
- {napari_plugin_manager-0.1.4.dist-info → napari_plugin_manager-0.1.5.dist-info}/WHEEL +1 -1
- napari_plugin_manager-0.1.4.dist-info/RECORD +0 -22
- {napari_plugin_manager-0.1.4.dist-info → napari_plugin_manager-0.1.5.dist-info/licenses}/LICENSE +0 -0
- {napari_plugin_manager-0.1.4.dist-info → napari_plugin_manager-0.1.5.dist-info}/top_level.txt +0 -0
|
@@ -346,7 +346,7 @@ def test_constraints_are_in_sync():
|
|
|
346
346
|
|
|
347
347
|
name_re = re.compile(r"([a-z0-9_\-]+).*")
|
|
348
348
|
for conda_constraint, pip_constraint in zip(
|
|
349
|
-
conda_constraints, pip_constraints
|
|
349
|
+
conda_constraints, pip_constraints, strict=False
|
|
350
350
|
):
|
|
351
351
|
conda_name = name_re.match(conda_constraint).group(1)
|
|
352
352
|
pip_name = name_re.match(pip_constraint).group(1)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from urllib.error import HTTPError, URLError
|
|
2
|
+
|
|
1
3
|
from flaky import flaky
|
|
2
4
|
|
|
3
5
|
from napari_plugin_manager.npe2api import (
|
|
@@ -26,20 +28,26 @@ def test_plugin_summaries():
|
|
|
26
28
|
"pypi_versions",
|
|
27
29
|
"conda_versions",
|
|
28
30
|
]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
try:
|
|
32
|
+
data = plugin_summaries()
|
|
33
|
+
test_data = dict(data[0])
|
|
34
|
+
for key in keys:
|
|
35
|
+
assert key in test_data
|
|
36
|
+
test_data.pop(key)
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
assert not test_data
|
|
39
|
+
except (HTTPError, URLError):
|
|
40
|
+
pass
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
def test_conda_map():
|
|
39
44
|
pkgs = ["napari-svg"]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
try:
|
|
46
|
+
data = conda_map()
|
|
47
|
+
for pkg in pkgs:
|
|
48
|
+
assert pkg in data
|
|
49
|
+
except (HTTPError, URLError):
|
|
50
|
+
pass
|
|
43
51
|
|
|
44
52
|
|
|
45
53
|
def test_iter_napari_plugin_info():
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
-
from
|
|
5
|
-
from unittest.mock import patch
|
|
4
|
+
from collections.abc import Generator
|
|
5
|
+
from unittest.mock import MagicMock, call, patch
|
|
6
6
|
|
|
7
7
|
import napari.plugins
|
|
8
8
|
import npe2
|
|
@@ -10,9 +10,12 @@ import pytest
|
|
|
10
10
|
import qtpy
|
|
11
11
|
from napari.plugins._tests.test_npe2 import mock_pm # noqa
|
|
12
12
|
from napari.utils.translations import trans
|
|
13
|
-
from qtpy.QtCore import QMimeData, QPointF, Qt, QUrl
|
|
13
|
+
from qtpy.QtCore import QMimeData, QPointF, Qt, QTimer, QUrl
|
|
14
14
|
from qtpy.QtGui import QDropEvent
|
|
15
|
-
from qtpy.QtWidgets import
|
|
15
|
+
from qtpy.QtWidgets import (
|
|
16
|
+
QApplication,
|
|
17
|
+
QMessageBox,
|
|
18
|
+
)
|
|
16
19
|
|
|
17
20
|
if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] > (3, 10):
|
|
18
21
|
pytest.skip(
|
|
@@ -21,8 +24,11 @@ if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] > (3, 10):
|
|
|
21
24
|
allow_module_level=True,
|
|
22
25
|
)
|
|
23
26
|
|
|
24
|
-
from napari_plugin_manager import qt_plugin_dialog
|
|
25
|
-
from napari_plugin_manager.base_qt_package_installer import
|
|
27
|
+
from napari_plugin_manager import base_qt_plugin_dialog, qt_plugin_dialog
|
|
28
|
+
from napari_plugin_manager.base_qt_package_installer import (
|
|
29
|
+
InstallerActions,
|
|
30
|
+
InstallerTools,
|
|
31
|
+
)
|
|
26
32
|
|
|
27
33
|
N_MOCKED_PLUGINS = 2
|
|
28
34
|
|
|
@@ -30,7 +36,7 @@ N_MOCKED_PLUGINS = 2
|
|
|
30
36
|
def _iter_napari_pypi_plugin_info(
|
|
31
37
|
conda_forge: bool = True,
|
|
32
38
|
) -> Generator[
|
|
33
|
-
|
|
39
|
+
tuple[npe2.PackageMetadata | None, bool], None, None
|
|
34
40
|
]: # pragma: no cover (this function is used in thread and codecov has a problem with the collection of coverage in such cases)
|
|
35
41
|
"""Mock the pypi method to collect available plugins.
|
|
36
42
|
|
|
@@ -87,29 +93,6 @@ def plugins(qtbot):
|
|
|
87
93
|
return PluginsMock()
|
|
88
94
|
|
|
89
95
|
|
|
90
|
-
class WarnPopupMock:
|
|
91
|
-
def __init__(self, text):
|
|
92
|
-
self._is_visible = False
|
|
93
|
-
|
|
94
|
-
def show(self):
|
|
95
|
-
self._is_visible = True
|
|
96
|
-
|
|
97
|
-
def exec_(self):
|
|
98
|
-
self._is_visible = True
|
|
99
|
-
|
|
100
|
-
def move(self, pos):
|
|
101
|
-
return False
|
|
102
|
-
|
|
103
|
-
def isVisible(self):
|
|
104
|
-
return self._is_visible
|
|
105
|
-
|
|
106
|
-
def close(self):
|
|
107
|
-
self._is_visible = False
|
|
108
|
-
|
|
109
|
-
def width(self):
|
|
110
|
-
return 100
|
|
111
|
-
|
|
112
|
-
|
|
113
96
|
@pytest.fixture(params=[True, False], ids=["constructor", "no-constructor"])
|
|
114
97
|
def plugin_dialog(
|
|
115
98
|
request,
|
|
@@ -120,6 +103,10 @@ def plugin_dialog(
|
|
|
120
103
|
old_plugins,
|
|
121
104
|
):
|
|
122
105
|
"""Fixture that provides a plugin dialog for a normal napari install."""
|
|
106
|
+
from napari.settings import get_settings
|
|
107
|
+
|
|
108
|
+
original_setting = get_settings().plugins.use_npe2_adaptor
|
|
109
|
+
get_settings().plugins.use_npe2_adaptor = False
|
|
123
110
|
|
|
124
111
|
class PluginManagerMock:
|
|
125
112
|
def instance(self):
|
|
@@ -138,7 +125,7 @@ def plugin_dialog(
|
|
|
138
125
|
def is_disabled(self, name):
|
|
139
126
|
return False
|
|
140
127
|
|
|
141
|
-
def discover(self):
|
|
128
|
+
def discover(self, include_npe1=False):
|
|
142
129
|
return ['plugin']
|
|
143
130
|
|
|
144
131
|
def enable(self, plugin):
|
|
@@ -182,7 +169,6 @@ def plugin_dialog(
|
|
|
182
169
|
"iter_napari_plugin_info",
|
|
183
170
|
_iter_napari_pypi_plugin_info,
|
|
184
171
|
)
|
|
185
|
-
monkeypatch.setattr(qt_plugin_dialog, 'WarnPopup', WarnPopupMock)
|
|
186
172
|
|
|
187
173
|
# This is patching `napari.utils.misc.running_as_constructor_app` function
|
|
188
174
|
# to mock a normal napari install.
|
|
@@ -212,6 +198,7 @@ def plugin_dialog(
|
|
|
212
198
|
widget.hide()
|
|
213
199
|
widget._add_items_timer.stop()
|
|
214
200
|
assert not widget._add_items_timer.isActive()
|
|
201
|
+
get_settings().plugins.use_npe2_adaptor = original_setting
|
|
215
202
|
|
|
216
203
|
|
|
217
204
|
def test_filter_not_available_plugins(request, plugin_dialog, qtbot):
|
|
@@ -275,7 +262,7 @@ def test_visible_widgets(request, plugin_dialog):
|
|
|
275
262
|
"""
|
|
276
263
|
Test that the direct entry button and textbox are visible
|
|
277
264
|
"""
|
|
278
|
-
if "
|
|
265
|
+
if "constructor" in request.node.name:
|
|
279
266
|
pytest.skip(
|
|
280
267
|
reason="Tested functionality not available in constructor-based installs"
|
|
281
268
|
)
|
|
@@ -314,14 +301,6 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
314
301
|
trans._("updating..."), InstallerActions.UPGRADE
|
|
315
302
|
)
|
|
316
303
|
|
|
317
|
-
with patch.object(qt_plugin_dialog.WarnPopup, "exec_") as mock:
|
|
318
|
-
plugin_dialog.installed_list.handle_action(
|
|
319
|
-
item,
|
|
320
|
-
'my-test-old-plugin-1',
|
|
321
|
-
InstallerActions.UNINSTALL,
|
|
322
|
-
)
|
|
323
|
-
assert mock.called
|
|
324
|
-
|
|
325
304
|
plugin_dialog.search("requests")
|
|
326
305
|
qtbot.wait(500)
|
|
327
306
|
item = plugin_dialog.available_list.item(0)
|
|
@@ -334,7 +313,7 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
334
313
|
InstallerActions.INSTALL,
|
|
335
314
|
version='3',
|
|
336
315
|
)
|
|
337
|
-
mock.
|
|
316
|
+
mock.assert_called_once_with(
|
|
338
317
|
trans._("installing..."), InstallerActions.INSTALL
|
|
339
318
|
)
|
|
340
319
|
|
|
@@ -344,11 +323,36 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
344
323
|
InstallerActions.CANCEL,
|
|
345
324
|
version='3',
|
|
346
325
|
)
|
|
347
|
-
mock.
|
|
326
|
+
assert mock.call_count >= 2
|
|
327
|
+
assert mock.call_args_list[1] == call(
|
|
328
|
+
"cancelling...", InstallerActions.CANCEL
|
|
329
|
+
)
|
|
348
330
|
|
|
349
331
|
qtbot.waitUntil(lambda: not plugin_dialog.worker.is_running)
|
|
350
332
|
|
|
351
333
|
|
|
334
|
+
def test_plugin_install_restart_warning(plugin_dialog, monkeypatch):
|
|
335
|
+
dialog_mock = MagicMock()
|
|
336
|
+
monkeypatch.setattr(
|
|
337
|
+
base_qt_plugin_dialog, 'RestartWarningDialog', dialog_mock
|
|
338
|
+
)
|
|
339
|
+
plugin_dialog.exec_()
|
|
340
|
+
plugin_dialog.already_installed.add('brand-new-plugin')
|
|
341
|
+
plugin_dialog.hide()
|
|
342
|
+
dialog_mock.assert_called_once()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_plugin_uninstall_restart_warning(plugin_dialog, monkeypatch):
|
|
346
|
+
dialog_mock = MagicMock()
|
|
347
|
+
monkeypatch.setattr(
|
|
348
|
+
base_qt_plugin_dialog, 'RestartWarningDialog', dialog_mock
|
|
349
|
+
)
|
|
350
|
+
plugin_dialog.exec_()
|
|
351
|
+
plugin_dialog.already_installed.remove('my-plugin')
|
|
352
|
+
plugin_dialog.hide()
|
|
353
|
+
dialog_mock.assert_called_once()
|
|
354
|
+
|
|
355
|
+
|
|
352
356
|
def test_on_enabled_checkbox(plugin_dialog, qtbot, plugins, old_plugins):
|
|
353
357
|
# checks npe2 lines
|
|
354
358
|
item = plugin_dialog.installed_list.item(0)
|
|
@@ -463,10 +467,15 @@ def test_drop_event(plugin_dialog, tmp_path):
|
|
|
463
467
|
assert plugin_dialog.direct_entry_edit.text() == str(path_1)
|
|
464
468
|
|
|
465
469
|
|
|
470
|
+
@pytest.mark.skipif(
|
|
471
|
+
'napari_latest' in os.getenv('TOX_ENV_NAME')
|
|
472
|
+
and 'PySide2' in os.getenv('TOX_ENV_NAME'),
|
|
473
|
+
reason='PySide2 flaky with latest released napari',
|
|
474
|
+
)
|
|
466
475
|
def test_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
467
476
|
if "[constructor]" in request.node.name:
|
|
468
477
|
pytest.skip(
|
|
469
|
-
reason="This test is only relevant for constructor-based installs"
|
|
478
|
+
reason="This test is only relevant for non-constructor-based installs"
|
|
470
479
|
)
|
|
471
480
|
|
|
472
481
|
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
@@ -490,12 +499,23 @@ def test_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
|
490
499
|
[QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Ok],
|
|
491
500
|
)
|
|
492
501
|
def test_install_pypi_constructor(
|
|
493
|
-
qtbot, tmp_virtualenv, plugin_dialog, request, message_return
|
|
502
|
+
qtbot, tmp_virtualenv, plugin_dialog, request, message_return, monkeypatch
|
|
494
503
|
):
|
|
495
504
|
if "no-constructor" in request.node.name:
|
|
496
505
|
pytest.skip(
|
|
497
|
-
reason="This test is
|
|
506
|
+
reason="This test is to test pip in constructor-based installs"
|
|
498
507
|
)
|
|
508
|
+
# ensure pip is the installer tool, so that the warning will trigger
|
|
509
|
+
monkeypatch.setattr(
|
|
510
|
+
qt_plugin_dialog.PluginListItem,
|
|
511
|
+
'get_installer_tool',
|
|
512
|
+
lambda self: InstallerTools.PIP,
|
|
513
|
+
)
|
|
514
|
+
monkeypatch.setattr(
|
|
515
|
+
qt_plugin_dialog.PluginListItem,
|
|
516
|
+
'get_installer_source',
|
|
517
|
+
lambda self: "PIP",
|
|
518
|
+
)
|
|
499
519
|
|
|
500
520
|
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
501
521
|
plugin_dialog.search('requests')
|
|
@@ -518,7 +538,7 @@ def test_install_pypi_constructor(
|
|
|
518
538
|
def test_cancel(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
519
539
|
if "[constructor]" in request.node.name:
|
|
520
540
|
pytest.skip(
|
|
521
|
-
reason="This test is only relevant for constructor-based installs"
|
|
541
|
+
reason="This test is only relevant for non-constructor-based installs"
|
|
522
542
|
)
|
|
523
543
|
|
|
524
544
|
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
@@ -542,7 +562,7 @@ def test_cancel(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
|
542
562
|
def test_cancel_all(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
543
563
|
if "[constructor]" in request.node.name:
|
|
544
564
|
pytest.skip(
|
|
545
|
-
reason="This test is only relevant for constructor-based installs"
|
|
565
|
+
reason="This test is only relevant for non-constructor-based installs"
|
|
546
566
|
)
|
|
547
567
|
|
|
548
568
|
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
@@ -569,7 +589,7 @@ def test_cancel_all(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
|
569
589
|
def test_direct_entry_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
570
590
|
if "[constructor]" in request.node.name:
|
|
571
591
|
pytest.skip(
|
|
572
|
-
reason="
|
|
592
|
+
reason="The tested functionality is not available in constructor-based installs"
|
|
573
593
|
)
|
|
574
594
|
|
|
575
595
|
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
@@ -605,3 +625,46 @@ def test_shortcut_quit(plugin_dialog, qtbot):
|
|
|
605
625
|
)
|
|
606
626
|
qtbot.wait(500)
|
|
607
627
|
assert not plugin_dialog.isVisible()
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@pytest.mark.skipif(
|
|
631
|
+
not sys.platform.startswith('linux'), reason="Test works only on linux"
|
|
632
|
+
)
|
|
633
|
+
def test_export_plugins_button(plugin_dialog):
|
|
634
|
+
def _timer():
|
|
635
|
+
dialog = QApplication.activeModalWidget()
|
|
636
|
+
dialog.reject()
|
|
637
|
+
|
|
638
|
+
timer = QTimer()
|
|
639
|
+
timer.setSingleShot(True)
|
|
640
|
+
timer.timeout.connect(_timer)
|
|
641
|
+
timer.start(4_000)
|
|
642
|
+
plugin_dialog.export_button.click()
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def test_export_plugins(plugin_dialog, tmp_path):
|
|
646
|
+
plugins_file = 'plugins.txt'
|
|
647
|
+
plugin_dialog.export_plugins(str(tmp_path / plugins_file))
|
|
648
|
+
assert (tmp_path / plugins_file).exists()
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@pytest.mark.skipif(
|
|
652
|
+
not sys.platform.startswith('linux'), reason="Test works only on linux"
|
|
653
|
+
)
|
|
654
|
+
def test_import_plugins_button(plugin_dialog):
|
|
655
|
+
def _timer():
|
|
656
|
+
dialog = QApplication.activeModalWidget()
|
|
657
|
+
dialog.reject()
|
|
658
|
+
|
|
659
|
+
timer = QTimer()
|
|
660
|
+
timer.setSingleShot(True)
|
|
661
|
+
timer.timeout.connect(_timer)
|
|
662
|
+
timer.start(4_000)
|
|
663
|
+
plugin_dialog.import_button.click()
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def test_import_plugins(plugin_dialog, tmp_path, qtbot):
|
|
667
|
+
path = tmp_path / 'plugins.txt'
|
|
668
|
+
path.write_text('requests\npyzenhub\n')
|
|
669
|
+
with qtbot.waitSignal(plugin_dialog.installer.allFinished, timeout=60_000):
|
|
670
|
+
plugin_dialog.import_plugins(str(path))
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
3
6
|
TYPE_CHECKING = False
|
|
4
7
|
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
6
11
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
12
|
else:
|
|
8
13
|
VERSION_TUPLE = object
|
|
@@ -12,5 +17,5 @@ __version__: str
|
|
|
12
17
|
__version_tuple__: VERSION_TUPLE
|
|
13
18
|
version_tuple: VERSION_TUPLE
|
|
14
19
|
|
|
15
|
-
__version__ = version = '0.1.
|
|
16
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
20
|
+
__version__ = version = '0.1.5'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 1, 5)
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
"""
|
|
2
|
-
A tool-agnostic installation logic for the plugin manager.
|
|
1
|
+
"""Package tool-agnostic installation logic for the plugin manager.
|
|
3
2
|
|
|
4
3
|
The main object is `InstallerQueue`, a `QProcess` subclass
|
|
5
|
-
with the notion of a job queue.
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
with the notion of a job queue.
|
|
5
|
+
|
|
6
|
+
The queued jobs are represented by a `deque` of `*InstallerTool` dataclasses
|
|
7
|
+
that contain the executable path, arguments and environment modifications.
|
|
8
|
+
|
|
8
9
|
Available actions for each tool are `install`, `uninstall`
|
|
9
10
|
and `cancel`.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
import contextlib
|
|
14
|
+
import logging
|
|
13
15
|
import os
|
|
14
16
|
import sys
|
|
15
17
|
from collections import deque
|
|
18
|
+
from collections.abc import Sequence
|
|
16
19
|
from dataclasses import dataclass
|
|
17
20
|
from enum import auto
|
|
18
21
|
from functools import lru_cache
|
|
@@ -20,7 +23,7 @@ from logging import getLogger
|
|
|
20
23
|
from pathlib import Path
|
|
21
24
|
from subprocess import call
|
|
22
25
|
from tempfile import gettempdir
|
|
23
|
-
from typing import
|
|
26
|
+
from typing import TypedDict
|
|
24
27
|
|
|
25
28
|
from napari.plugins import plugin_manager
|
|
26
29
|
from napari.plugins.npe2api import _user_agent
|
|
@@ -30,12 +33,15 @@ from npe2 import PluginManager
|
|
|
30
33
|
from qtpy.QtCore import QObject, QProcess, QProcessEnvironment, Signal
|
|
31
34
|
from qtpy.QtWidgets import QTextEdit
|
|
32
35
|
|
|
36
|
+
# Alias for int type to represent a job idenfifier in the installer queue
|
|
33
37
|
JobId = int
|
|
38
|
+
|
|
34
39
|
log = getLogger(__name__)
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
class InstallerActions(StringEnum):
|
|
38
43
|
"Available actions for the plugin manager"
|
|
44
|
+
|
|
39
45
|
INSTALL = auto()
|
|
40
46
|
UNINSTALL = auto()
|
|
41
47
|
CANCEL = auto()
|
|
@@ -44,24 +50,29 @@ class InstallerActions(StringEnum):
|
|
|
44
50
|
|
|
45
51
|
|
|
46
52
|
class ProcessFinishedData(TypedDict):
|
|
53
|
+
"""Data about a finished process."""
|
|
54
|
+
|
|
47
55
|
exit_code: int
|
|
48
56
|
exit_status: int
|
|
49
57
|
action: InstallerActions
|
|
50
|
-
pkgs:
|
|
58
|
+
pkgs: tuple[str, ...]
|
|
51
59
|
|
|
52
60
|
|
|
53
61
|
class InstallerTools(StringEnum):
|
|
54
|
-
"
|
|
62
|
+
"Installer tools selectable by InstallerQueue jobs"
|
|
63
|
+
|
|
55
64
|
CONDA = auto()
|
|
56
65
|
PIP = auto()
|
|
57
66
|
|
|
58
67
|
|
|
59
68
|
@dataclass(frozen=True)
|
|
60
69
|
class AbstractInstallerTool:
|
|
70
|
+
"""Abstract base class for installer tools."""
|
|
71
|
+
|
|
61
72
|
action: InstallerActions
|
|
62
|
-
pkgs:
|
|
63
|
-
origins:
|
|
64
|
-
prefix:
|
|
73
|
+
pkgs: tuple[str, ...]
|
|
74
|
+
origins: tuple[str, ...] = ()
|
|
75
|
+
prefix: str | None = None
|
|
65
76
|
process: QProcess = None
|
|
66
77
|
|
|
67
78
|
@property
|
|
@@ -104,16 +115,25 @@ class AbstractInstallerTool:
|
|
|
104
115
|
|
|
105
116
|
|
|
106
117
|
class PipInstallerTool(AbstractInstallerTool):
|
|
118
|
+
"""Pip installer tool for the plugin manager.
|
|
119
|
+
|
|
120
|
+
This class is used to install and uninstall packages using pip.
|
|
121
|
+
"""
|
|
122
|
+
|
|
107
123
|
@classmethod
|
|
108
124
|
def available(cls):
|
|
125
|
+
"""Check if pip is available."""
|
|
109
126
|
return call([cls.executable(), "-m", "pip", "--version"]) == 0
|
|
110
127
|
|
|
111
|
-
def arguments(self) ->
|
|
128
|
+
def arguments(self) -> tuple[str, ...]:
|
|
129
|
+
"""Compose arguments for the pip command."""
|
|
112
130
|
args = ['-m', 'pip']
|
|
131
|
+
|
|
113
132
|
if self.action == InstallerActions.INSTALL:
|
|
114
133
|
args += ['install', '-c', self._constraints_file()]
|
|
115
134
|
for origin in self.origins:
|
|
116
135
|
args += ['--extra-index-url', origin]
|
|
136
|
+
|
|
117
137
|
elif self.action == InstallerActions.UPGRADE:
|
|
118
138
|
args += [
|
|
119
139
|
'install',
|
|
@@ -123,14 +143,19 @@ class PipInstallerTool(AbstractInstallerTool):
|
|
|
123
143
|
]
|
|
124
144
|
for origin in self.origins:
|
|
125
145
|
args += ['--extra-index-url', origin]
|
|
146
|
+
|
|
126
147
|
elif self.action == InstallerActions.UNINSTALL:
|
|
127
148
|
args += ['uninstall', '-y']
|
|
149
|
+
|
|
128
150
|
else:
|
|
129
151
|
raise ValueError(f"Action '{self.action}' not supported!")
|
|
130
|
-
|
|
152
|
+
|
|
153
|
+
if log.getEffectiveLevel() < 30: # DEBUG and INFOlevel
|
|
131
154
|
args.append('-vvv')
|
|
155
|
+
|
|
132
156
|
if self.prefix is not None:
|
|
133
157
|
args.extend(['--prefix', str(self.prefix)])
|
|
158
|
+
|
|
134
159
|
return (*args, *self.pkgs)
|
|
135
160
|
|
|
136
161
|
def environment(
|
|
@@ -148,8 +173,17 @@ class PipInstallerTool(AbstractInstallerTool):
|
|
|
148
173
|
|
|
149
174
|
|
|
150
175
|
class CondaInstallerTool(AbstractInstallerTool):
|
|
176
|
+
"""Conda installer tool for the plugin manager.
|
|
177
|
+
|
|
178
|
+
This class is used to install and uninstall packages using conda or conda-like executable.
|
|
179
|
+
"""
|
|
180
|
+
|
|
151
181
|
@classmethod
|
|
152
182
|
def executable(cls):
|
|
183
|
+
"""Find a path to the executable.
|
|
184
|
+
|
|
185
|
+
This method assumes that if no environment variable is set that conda is available in the PATH.
|
|
186
|
+
"""
|
|
153
187
|
bat = ".bat" if os.name == "nt" else ""
|
|
154
188
|
for path in (
|
|
155
189
|
Path(os.environ.get('MAMBA_EXE', '')),
|
|
@@ -158,26 +192,33 @@ class CondaInstallerTool(AbstractInstallerTool):
|
|
|
158
192
|
Path(os.environ.get('CONDA', '')) / 'condabin' / f'conda{bat}',
|
|
159
193
|
):
|
|
160
194
|
if path.is_file():
|
|
195
|
+
# return the path to the executable
|
|
161
196
|
return str(path)
|
|
162
|
-
|
|
197
|
+
# Otherwise, we assume that conda is available in the PATH
|
|
198
|
+
return f'conda{bat}'
|
|
163
199
|
|
|
164
200
|
@classmethod
|
|
165
201
|
def available(cls):
|
|
202
|
+
"""Check if the executable is available by checking if it can output its version."""
|
|
166
203
|
executable = cls.executable()
|
|
167
204
|
try:
|
|
168
205
|
return call([executable, "--version"]) == 0
|
|
169
206
|
except FileNotFoundError: # pragma: no cover
|
|
170
207
|
return False
|
|
171
208
|
|
|
172
|
-
def arguments(self) ->
|
|
209
|
+
def arguments(self) -> tuple[str, ...]:
|
|
210
|
+
"""Compose arguments for the conda command."""
|
|
173
211
|
prefix = self.prefix or self._default_prefix()
|
|
212
|
+
|
|
174
213
|
if self.action == InstallerActions.UPGRADE:
|
|
175
214
|
args = ['update', '-y', '--prefix', prefix]
|
|
176
215
|
else:
|
|
177
216
|
args = [self.action.value, '-y', '--prefix', prefix]
|
|
217
|
+
|
|
178
218
|
args.append('--override-channels')
|
|
179
219
|
for channel in (*self.origins, *self._default_channels()):
|
|
180
220
|
args.extend(["-c", channel])
|
|
221
|
+
|
|
181
222
|
return (*args, *self.pkgs)
|
|
182
223
|
|
|
183
224
|
def environment(
|
|
@@ -205,6 +246,7 @@ class CondaInstallerTool(AbstractInstallerTool):
|
|
|
205
246
|
def _add_constraints_to_env(
|
|
206
247
|
self, env: QProcessEnvironment
|
|
207
248
|
) -> QProcessEnvironment:
|
|
249
|
+
"""Add constraints to the environment."""
|
|
208
250
|
PINNED = 'CONDA_PINNED_PACKAGES'
|
|
209
251
|
constraints = self.constraints()
|
|
210
252
|
if env.contains(PINNED):
|
|
@@ -213,9 +255,11 @@ class CondaInstallerTool(AbstractInstallerTool):
|
|
|
213
255
|
return env
|
|
214
256
|
|
|
215
257
|
def _default_channels(self):
|
|
258
|
+
"""Default channels for conda installations."""
|
|
216
259
|
return ('conda-forge',)
|
|
217
260
|
|
|
218
261
|
def _default_prefix(self):
|
|
262
|
+
"""Default prefix for conda installations."""
|
|
219
263
|
if (Path(sys.prefix) / "conda-meta").is_dir():
|
|
220
264
|
return sys.prefix
|
|
221
265
|
raise ValueError("Prefix has not been specified!")
|
|
@@ -244,10 +288,10 @@ class InstallerQueue(QObject):
|
|
|
244
288
|
BASE_PACKAGE_NAME = ''
|
|
245
289
|
|
|
246
290
|
def __init__(
|
|
247
|
-
self, parent:
|
|
291
|
+
self, parent: QObject | None = None, prefix: str | None = None
|
|
248
292
|
) -> None:
|
|
249
293
|
super().__init__(parent)
|
|
250
|
-
self._queue:
|
|
294
|
+
self._queue: deque[AbstractInstallerTool] = deque()
|
|
251
295
|
self._current_process: QProcess = None
|
|
252
296
|
self._prefix = prefix
|
|
253
297
|
self._output_widget = None
|
|
@@ -259,11 +303,13 @@ class InstallerQueue(QObject):
|
|
|
259
303
|
tool: InstallerTools,
|
|
260
304
|
pkgs: Sequence[str],
|
|
261
305
|
*,
|
|
262
|
-
prefix:
|
|
306
|
+
prefix: str | None = None,
|
|
263
307
|
origins: Sequence[str] = (),
|
|
264
308
|
**kwargs,
|
|
265
309
|
) -> JobId:
|
|
266
|
-
"""Install packages in
|
|
310
|
+
"""Install packages in the installer queue.
|
|
311
|
+
|
|
312
|
+
This installs packages in `pkgs` into `prefix` using `tool` with additional
|
|
267
313
|
`origins` as source for `pkgs`.
|
|
268
314
|
|
|
269
315
|
Parameters
|
|
@@ -280,7 +326,7 @@ class InstallerQueue(QObject):
|
|
|
280
326
|
Returns
|
|
281
327
|
-------
|
|
282
328
|
JobId : int
|
|
283
|
-
ID
|
|
329
|
+
An ID to reference the job. Use to cancel the process.
|
|
284
330
|
"""
|
|
285
331
|
item = self._build_queue_item(
|
|
286
332
|
tool=tool,
|
|
@@ -298,11 +344,13 @@ class InstallerQueue(QObject):
|
|
|
298
344
|
tool: InstallerTools,
|
|
299
345
|
pkgs: Sequence[str],
|
|
300
346
|
*,
|
|
301
|
-
prefix:
|
|
347
|
+
prefix: str | None = None,
|
|
302
348
|
origins: Sequence[str] = (),
|
|
303
349
|
**kwargs,
|
|
304
350
|
) -> JobId:
|
|
305
|
-
"""Upgrade packages in
|
|
351
|
+
"""Upgrade packages in the installer queue.
|
|
352
|
+
|
|
353
|
+
Upgrade in `pkgs` into `prefix` using `tool` with additional
|
|
306
354
|
`origins` as source for `pkgs`.
|
|
307
355
|
|
|
308
356
|
Parameters
|
|
@@ -319,7 +367,7 @@ class InstallerQueue(QObject):
|
|
|
319
367
|
Returns
|
|
320
368
|
-------
|
|
321
369
|
JobId : int
|
|
322
|
-
ID
|
|
370
|
+
An ID to reference the job. Use to cancel the process.
|
|
323
371
|
"""
|
|
324
372
|
item = self._build_queue_item(
|
|
325
373
|
tool=tool,
|
|
@@ -337,10 +385,12 @@ class InstallerQueue(QObject):
|
|
|
337
385
|
tool: InstallerTools,
|
|
338
386
|
pkgs: Sequence[str],
|
|
339
387
|
*,
|
|
340
|
-
prefix:
|
|
388
|
+
prefix: str | None = None,
|
|
341
389
|
**kwargs,
|
|
342
390
|
) -> JobId:
|
|
343
|
-
"""Uninstall packages in
|
|
391
|
+
"""Uninstall packages in the installer queue.
|
|
392
|
+
|
|
393
|
+
Uninstall packages in `pkgs` from `prefix` using `tool`.
|
|
344
394
|
|
|
345
395
|
Parameters
|
|
346
396
|
----------
|
|
@@ -354,7 +404,7 @@ class InstallerQueue(QObject):
|
|
|
354
404
|
Returns
|
|
355
405
|
-------
|
|
356
406
|
JobId : int
|
|
357
|
-
ID
|
|
407
|
+
An ID to reference the job. Use to cancel the process.
|
|
358
408
|
"""
|
|
359
409
|
item = self._build_queue_item(
|
|
360
410
|
tool=tool,
|
|
@@ -367,8 +417,10 @@ class InstallerQueue(QObject):
|
|
|
367
417
|
return self._queue_item(item)
|
|
368
418
|
|
|
369
419
|
def cancel(self, job_id: JobId):
|
|
370
|
-
"""Cancel
|
|
371
|
-
|
|
420
|
+
"""Cancel a job.
|
|
421
|
+
|
|
422
|
+
Cancel the process, if it is running, referenced by `job_id`.
|
|
423
|
+
If `job_id` does not exist in the queue, a ValueError is raised.
|
|
372
424
|
|
|
373
425
|
Parameters
|
|
374
426
|
----------
|
|
@@ -391,7 +443,7 @@ class InstallerQueue(QObject):
|
|
|
391
443
|
|
|
392
444
|
self._end_process(item.process)
|
|
393
445
|
else:
|
|
394
|
-
# still pending, just remove from queue
|
|
446
|
+
# job is still pending, just remove it from the queue
|
|
395
447
|
self._queue.remove(item)
|
|
396
448
|
|
|
397
449
|
self.processFinished.emit(
|
|
@@ -402,6 +454,7 @@ class InstallerQueue(QObject):
|
|
|
402
454
|
'pkgs': item.pkgs,
|
|
403
455
|
}
|
|
404
456
|
)
|
|
457
|
+
# continue processing the queue
|
|
405
458
|
self._process_queue()
|
|
406
459
|
return
|
|
407
460
|
|
|
@@ -415,7 +468,7 @@ class InstallerQueue(QObject):
|
|
|
415
468
|
raise ValueError(msg)
|
|
416
469
|
|
|
417
470
|
def cancel_all(self):
|
|
418
|
-
"""Terminate all
|
|
471
|
+
"""Terminate all processes in the queue and emit the `processFinished` signal."""
|
|
419
472
|
all_pkgs = []
|
|
420
473
|
for item in deque(self._queue):
|
|
421
474
|
all_pkgs.extend(item.pkgs)
|
|
@@ -462,6 +515,7 @@ class InstallerQueue(QObject):
|
|
|
462
515
|
return len(self._queue)
|
|
463
516
|
|
|
464
517
|
def set_output_widget(self, output_widget: QTextEdit):
|
|
518
|
+
"""Set the output widget for text output."""
|
|
465
519
|
if output_widget:
|
|
466
520
|
self._output_widget = output_widget
|
|
467
521
|
|
|
@@ -492,7 +546,7 @@ class InstallerQueue(QObject):
|
|
|
492
546
|
tool: InstallerTools,
|
|
493
547
|
action: InstallerActions,
|
|
494
548
|
pkgs: Sequence[str],
|
|
495
|
-
prefix:
|
|
549
|
+
prefix: str | None = None,
|
|
496
550
|
origins: Sequence[str] = (),
|
|
497
551
|
**kwargs,
|
|
498
552
|
) -> AbstractInstallerTool:
|
|
@@ -581,9 +635,9 @@ class InstallerQueue(QObject):
|
|
|
581
635
|
|
|
582
636
|
def _on_process_done(
|
|
583
637
|
self,
|
|
584
|
-
exit_code:
|
|
585
|
-
exit_status:
|
|
586
|
-
error:
|
|
638
|
+
exit_code: int | None = None,
|
|
639
|
+
exit_status: QProcess.ExitStatus | None = None,
|
|
640
|
+
error: QProcess.ProcessError | None = None,
|
|
587
641
|
):
|
|
588
642
|
item = None
|
|
589
643
|
with contextlib.suppress(IndexError):
|
|
@@ -616,9 +670,15 @@ class InstallerQueue(QObject):
|
|
|
616
670
|
|
|
617
671
|
def _on_stdout_ready(self):
|
|
618
672
|
if self._current_process is not None:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
673
|
+
try:
|
|
674
|
+
text = (
|
|
675
|
+
self._current_process.readAllStandardOutput()
|
|
676
|
+
.data()
|
|
677
|
+
.decode()
|
|
678
|
+
)
|
|
679
|
+
except UnicodeDecodeError:
|
|
680
|
+
logging.exception("Could not decode stdout")
|
|
681
|
+
return
|
|
622
682
|
if text:
|
|
623
683
|
self._log(text)
|
|
624
684
|
|
|
@@ -2,20 +2,17 @@ import contextlib
|
|
|
2
2
|
import importlib.metadata
|
|
3
3
|
import os
|
|
4
4
|
import webbrowser
|
|
5
|
+
from collections.abc import Sequence
|
|
5
6
|
from functools import partial
|
|
6
7
|
from typing import (
|
|
7
8
|
Any,
|
|
8
|
-
Dict,
|
|
9
|
-
List,
|
|
10
9
|
Literal,
|
|
11
10
|
NamedTuple,
|
|
12
|
-
Optional,
|
|
13
11
|
Protocol,
|
|
14
|
-
Sequence,
|
|
15
|
-
Tuple,
|
|
16
12
|
)
|
|
17
13
|
|
|
18
14
|
from packaging.version import parse as parse_version
|
|
15
|
+
from qtpy.compat import getopenfilename, getsavefilename
|
|
19
16
|
from qtpy.QtCore import QSize, Qt, QTimer, Signal, Slot
|
|
20
17
|
from qtpy.QtGui import (
|
|
21
18
|
QAction,
|
|
@@ -54,6 +51,7 @@ from napari_plugin_manager.base_qt_package_installer import (
|
|
|
54
51
|
InstallerTools,
|
|
55
52
|
ProcessFinishedData,
|
|
56
53
|
)
|
|
54
|
+
from napari_plugin_manager.qt_warning_dialog import RestartWarningDialog
|
|
57
55
|
from napari_plugin_manager.qt_widgets import ClickableLabel
|
|
58
56
|
from napari_plugin_manager.utils import is_conda_package
|
|
59
57
|
|
|
@@ -113,8 +111,8 @@ class BasePackageMetadata(NamedTuple):
|
|
|
113
111
|
class BaseProjectInfoVersions(NamedTuple):
|
|
114
112
|
metadata: BasePackageMetadata
|
|
115
113
|
display_name: str
|
|
116
|
-
pypi_versions:
|
|
117
|
-
conda_versions:
|
|
114
|
+
pypi_versions: list[str]
|
|
115
|
+
conda_versions: list[str]
|
|
118
116
|
|
|
119
117
|
|
|
120
118
|
class BasePluginListItem(QFrame):
|
|
@@ -146,13 +144,13 @@ class BasePluginListItem(QFrame):
|
|
|
146
144
|
author: str = '',
|
|
147
145
|
license: str = "UNKNOWN", # noqa: A002
|
|
148
146
|
*,
|
|
149
|
-
plugin_name:
|
|
147
|
+
plugin_name: str | None = None,
|
|
150
148
|
parent: QWidget = None,
|
|
151
149
|
enabled: bool = True,
|
|
152
150
|
installed: bool = False,
|
|
153
151
|
plugin_api_version=1,
|
|
154
|
-
versions_conda:
|
|
155
|
-
versions_pypi:
|
|
152
|
+
versions_conda: list[str] | None = None,
|
|
153
|
+
versions_pypi: list[str] | None = None,
|
|
156
154
|
prefix=None,
|
|
157
155
|
) -> None:
|
|
158
156
|
super().__init__(parent)
|
|
@@ -308,9 +306,9 @@ class BasePluginListItem(QFrame):
|
|
|
308
306
|
def set_busy(
|
|
309
307
|
self,
|
|
310
308
|
text: str,
|
|
311
|
-
action_name:
|
|
312
|
-
Literal['install', 'uninstall', 'cancel', 'upgrade']
|
|
313
|
-
|
|
309
|
+
action_name: (
|
|
310
|
+
Literal['install', 'uninstall', 'cancel', 'upgrade'] | None
|
|
311
|
+
) = None,
|
|
314
312
|
):
|
|
315
313
|
"""Updates status text and what buttons are visible when any button is pushed.
|
|
316
314
|
|
|
@@ -831,8 +829,8 @@ class BaseQPluginList(QListWidget):
|
|
|
831
829
|
item: QListWidgetItem,
|
|
832
830
|
pkg_name: str,
|
|
833
831
|
action_name: InstallerActions,
|
|
834
|
-
version:
|
|
835
|
-
installer_choice:
|
|
832
|
+
version: str | None = None,
|
|
833
|
+
installer_choice: str | None = None,
|
|
836
834
|
):
|
|
837
835
|
"""Determine which action is called (install, uninstall, update, cancel).
|
|
838
836
|
Update buttons appropriately and run the action."""
|
|
@@ -843,8 +841,6 @@ class BaseQPluginList(QListWidget):
|
|
|
843
841
|
if not item.text().startswith(self._SORT_ORDER_PREFIX):
|
|
844
842
|
item.setText(f"{self._SORT_ORDER_PREFIX}{item.text()}")
|
|
845
843
|
|
|
846
|
-
self._before_handle_action(widget, action_name)
|
|
847
|
-
|
|
848
844
|
if action_name == InstallerActions.INSTALL:
|
|
849
845
|
if version:
|
|
850
846
|
pkg_name += (
|
|
@@ -1258,7 +1254,7 @@ class BaseQtPluginDialog(QDialog):
|
|
|
1258
1254
|
self._add_items_timer.start()
|
|
1259
1255
|
self._update_plugin_count()
|
|
1260
1256
|
|
|
1261
|
-
def _add_installed(self, pkg_name:
|
|
1257
|
+
def _add_installed(self, pkg_name: str | None = None) -> None:
|
|
1262
1258
|
"""
|
|
1263
1259
|
Add plugins that are installed to the dialog.
|
|
1264
1260
|
|
|
@@ -1385,6 +1381,18 @@ class BaseQtPluginDialog(QDialog):
|
|
|
1385
1381
|
self.packages_search.setClearButtonEnabled(True)
|
|
1386
1382
|
self.packages_search.textChanged.connect(self.search)
|
|
1387
1383
|
|
|
1384
|
+
self.import_button = QPushButton(self._trans('Import'), self)
|
|
1385
|
+
self.import_button.setObjectName("import_button")
|
|
1386
|
+
self.import_button.setToolTip(self._trans('Import plugins from file'))
|
|
1387
|
+
self.import_button.clicked.connect(self._import_plugins)
|
|
1388
|
+
|
|
1389
|
+
self.export_button = QPushButton(self._trans('Export'), self)
|
|
1390
|
+
self.export_button.setObjectName("export_button")
|
|
1391
|
+
self.export_button.setToolTip(
|
|
1392
|
+
self._trans('Export installed plugins list')
|
|
1393
|
+
)
|
|
1394
|
+
self.export_button.clicked.connect(self._export_plugins)
|
|
1395
|
+
|
|
1388
1396
|
self.refresh_button = QPushButton(self._trans('Refresh'), self)
|
|
1389
1397
|
self.refresh_button.setObjectName("refresh_button")
|
|
1390
1398
|
self.refresh_button.setToolTip(
|
|
@@ -1398,6 +1406,8 @@ class BaseQtPluginDialog(QDialog):
|
|
|
1398
1406
|
horizontal_mid_layout = QHBoxLayout()
|
|
1399
1407
|
horizontal_mid_layout.addWidget(self.packages_search)
|
|
1400
1408
|
horizontal_mid_layout.addStretch()
|
|
1409
|
+
horizontal_mid_layout.addWidget(self.import_button)
|
|
1410
|
+
horizontal_mid_layout.addWidget(self.export_button)
|
|
1401
1411
|
horizontal_mid_layout.addWidget(self.refresh_button)
|
|
1402
1412
|
mid_layout.addLayout(horizontal_mid_layout)
|
|
1403
1413
|
mid_layout.addWidget(self.installed_label)
|
|
@@ -1657,7 +1667,7 @@ class BaseQtPluginDialog(QDialog):
|
|
|
1657
1667
|
|
|
1658
1668
|
self._update_plugin_count()
|
|
1659
1669
|
|
|
1660
|
-
def _handle_yield(self, data:
|
|
1670
|
+
def _handle_yield(self, data: tuple[PackageMetadataProtocol, bool, dict]):
|
|
1661
1671
|
"""Output from a worker process.
|
|
1662
1672
|
|
|
1663
1673
|
Includes information about the plugin, including available versions on conda and pypi.
|
|
@@ -1687,6 +1697,16 @@ class BaseQtPluginDialog(QDialog):
|
|
|
1687
1697
|
def _refresh_and_clear_cache(self):
|
|
1688
1698
|
self.refresh(clear_cache=True)
|
|
1689
1699
|
|
|
1700
|
+
def _import_plugins(self):
|
|
1701
|
+
fpath, _ = getopenfilename(filters="Text files (*.txt)")
|
|
1702
|
+
if fpath:
|
|
1703
|
+
self.import_plugins(fpath)
|
|
1704
|
+
|
|
1705
|
+
def _export_plugins(self):
|
|
1706
|
+
fpath, _ = getsavefilename(filters="Text files (*.txt)")
|
|
1707
|
+
if fpath:
|
|
1708
|
+
self.export_plugins(fpath)
|
|
1709
|
+
|
|
1690
1710
|
# endregion - Private methods
|
|
1691
1711
|
|
|
1692
1712
|
# region - Qt overrides
|
|
@@ -1721,12 +1741,18 @@ class BaseQtPluginDialog(QDialog):
|
|
|
1721
1741
|
|
|
1722
1742
|
plugin_dialog.setModal(True)
|
|
1723
1743
|
plugin_dialog.show()
|
|
1744
|
+
plugin_dialog._installed_on_show = set(plugin_dialog.already_installed)
|
|
1724
1745
|
|
|
1725
1746
|
if self._first_open:
|
|
1726
1747
|
self._update_theme(None)
|
|
1727
1748
|
self._first_open = False
|
|
1728
1749
|
|
|
1729
1750
|
def hideEvent(self, event):
|
|
1751
|
+
if (
|
|
1752
|
+
hasattr(self, '_installed_on_show')
|
|
1753
|
+
and self._installed_on_show != self.already_installed
|
|
1754
|
+
):
|
|
1755
|
+
RestartWarningDialog(self).exec_()
|
|
1730
1756
|
self.packages_search.clear()
|
|
1731
1757
|
self.toggle_status(False)
|
|
1732
1758
|
super().hideEvent(event)
|
|
@@ -1735,7 +1761,7 @@ class BaseQtPluginDialog(QDialog):
|
|
|
1735
1761
|
|
|
1736
1762
|
# region - Public methods
|
|
1737
1763
|
# ------------------------------------------------------------------------
|
|
1738
|
-
def search(self, text:
|
|
1764
|
+
def search(self, text: str | None = None, skip=False) -> None:
|
|
1739
1765
|
"""Filter by text or set current text as filter."""
|
|
1740
1766
|
if text is None:
|
|
1741
1767
|
text = self.packages_search.text()
|
|
@@ -1813,4 +1839,30 @@ class BaseQtPluginDialog(QDialog):
|
|
|
1813
1839
|
item = self.installed_list.item(idx)
|
|
1814
1840
|
item.widget.prefix = prefix
|
|
1815
1841
|
|
|
1842
|
+
def export_plugins(self, fpath: str) -> list[str]:
|
|
1843
|
+
"""Export installed plugins to a file."""
|
|
1844
|
+
plugins = []
|
|
1845
|
+
if self.installed_list.count():
|
|
1846
|
+
for idx in range(self.installed_list.count()):
|
|
1847
|
+
item = self.installed_list.item(idx)
|
|
1848
|
+
if item:
|
|
1849
|
+
name = item.widget.name
|
|
1850
|
+
version = item.widget._version # Make public attr?
|
|
1851
|
+
plugins.append(f"{name}=={version}\n")
|
|
1852
|
+
|
|
1853
|
+
with open(fpath, 'w') as f:
|
|
1854
|
+
f.writelines(plugins)
|
|
1855
|
+
|
|
1856
|
+
return plugins
|
|
1857
|
+
|
|
1858
|
+
def import_plugins(self, fpath: str) -> None:
|
|
1859
|
+
"""Install plugins from file."""
|
|
1860
|
+
with open(fpath) as f:
|
|
1861
|
+
plugins = f.read().split('\n')
|
|
1862
|
+
|
|
1863
|
+
print(plugins)
|
|
1864
|
+
|
|
1865
|
+
plugins = [p for p in plugins if p]
|
|
1866
|
+
self._install_packages(plugins)
|
|
1867
|
+
|
|
1816
1868
|
# endregion - Public methods
|
napari_plugin_manager/npe2api.py
CHANGED
|
@@ -8,7 +8,6 @@ from collections.abc import Iterator
|
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
9
|
from functools import lru_cache
|
|
10
10
|
from typing import (
|
|
11
|
-
Optional,
|
|
12
11
|
TypedDict,
|
|
13
12
|
cast,
|
|
14
13
|
)
|
|
@@ -75,7 +74,7 @@ def plugin_summaries() -> list[SummaryDict]:
|
|
|
75
74
|
|
|
76
75
|
|
|
77
76
|
@lru_cache
|
|
78
|
-
def conda_map() -> dict[PyPIname,
|
|
77
|
+
def conda_map() -> dict[PyPIname, str | None]:
|
|
79
78
|
"""Return map of PyPI package name to conda_channel/package_name ()."""
|
|
80
79
|
url = 'https://npe2api.vercel.app/api/conda'
|
|
81
80
|
with urlopen(Request(url, headers={'User-Agent': _user_agent()})) as resp:
|
|
@@ -10,10 +10,10 @@ by a `deque` of `*InstallerTool` dataclasses (`NapariPipInstallerTool` and
|
|
|
10
10
|
import atexit
|
|
11
11
|
import os
|
|
12
12
|
import sys
|
|
13
|
+
from collections.abc import Sequence
|
|
13
14
|
from functools import lru_cache
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from tempfile import NamedTemporaryFile
|
|
16
|
-
from typing import Sequence
|
|
17
17
|
|
|
18
18
|
from napari._version import version as _napari_version
|
|
19
19
|
from napari._version import version_tuple as _napari_version_tuple
|
|
@@ -48,7 +48,7 @@ class NapariPipInstallerTool(PipInstallerTool):
|
|
|
48
48
|
"""
|
|
49
49
|
Version constraints to limit unwanted changes in installation.
|
|
50
50
|
"""
|
|
51
|
-
return [f"napari=={_napari_version}"
|
|
51
|
+
return [f"napari=={_napari_version}"]
|
|
52
52
|
|
|
53
53
|
@classmethod
|
|
54
54
|
@lru_cache(maxsize=0)
|
|
@@ -75,7 +75,7 @@ class NapariCondaInstallerTool(CondaInstallerTool):
|
|
|
75
75
|
pin_level = 2 if is_dev else 3
|
|
76
76
|
version = ".".join([str(x) for x in _napari_version_tuple[:pin_level]])
|
|
77
77
|
|
|
78
|
-
return [f"napari={version}"
|
|
78
|
+
return [f"napari={version}"]
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
class NapariInstallerQueue(InstallerQueue):
|
|
@@ -6,7 +6,6 @@ import napari.resources
|
|
|
6
6
|
import npe2
|
|
7
7
|
from napari._qt.qt_resources import QColoredSVGIcon, get_current_stylesheet
|
|
8
8
|
from napari._qt.qthreading import create_worker
|
|
9
|
-
from napari._qt.widgets.qt_message_popup import WarnPopup
|
|
10
9
|
from napari._qt.widgets.qt_tooltip import QtToolTipLabel
|
|
11
10
|
from napari.plugins.utils import normalized_name
|
|
12
11
|
from napari.settings import get_settings
|
|
@@ -15,7 +14,7 @@ from napari.utils.misc import (
|
|
|
15
14
|
)
|
|
16
15
|
from napari.utils.notifications import show_info, show_warning
|
|
17
16
|
from napari.utils.translations import trans
|
|
18
|
-
from qtpy.QtCore import
|
|
17
|
+
from qtpy.QtCore import QSize
|
|
19
18
|
from qtpy.QtGui import (
|
|
20
19
|
QMovie,
|
|
21
20
|
)
|
|
@@ -43,24 +42,6 @@ STYLES_PATH = Path(__file__).parent / 'styles.qss'
|
|
|
43
42
|
DISMISS_WARN_PYPI_INSTALL_DLG = False
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
def _show_message(widget):
|
|
47
|
-
message = trans._(
|
|
48
|
-
'When installing/uninstalling npe2 plugins, '
|
|
49
|
-
'you must restart napari for UI changes to take effect.'
|
|
50
|
-
)
|
|
51
|
-
if widget.isVisible():
|
|
52
|
-
button = widget.action_button
|
|
53
|
-
warn_dialog = WarnPopup(text=message)
|
|
54
|
-
global_point = widget.action_button.mapToGlobal(
|
|
55
|
-
button.rect().topRight()
|
|
56
|
-
)
|
|
57
|
-
global_point = QPoint(
|
|
58
|
-
global_point.x() - button.width(), global_point.y()
|
|
59
|
-
)
|
|
60
|
-
warn_dialog.move(global_point)
|
|
61
|
-
warn_dialog.exec_()
|
|
62
|
-
|
|
63
|
-
|
|
64
45
|
class ProjectInfoVersions(BaseProjectInfoVersions):
|
|
65
46
|
metadata: npe2.PackageMetadata
|
|
66
47
|
|
|
@@ -182,13 +163,6 @@ class QPluginList(BaseQPluginList):
|
|
|
182
163
|
def _trans(self, text, **kwargs):
|
|
183
164
|
return trans._(text, **kwargs)
|
|
184
165
|
|
|
185
|
-
def _before_handle_action(self, widget, action_name):
|
|
186
|
-
if (
|
|
187
|
-
widget.plugin_api_version != 1
|
|
188
|
-
and action_name == InstallerActions.UNINSTALL
|
|
189
|
-
):
|
|
190
|
-
_show_message(widget)
|
|
191
|
-
|
|
192
166
|
|
|
193
167
|
class QtPluginDialog(BaseQtPluginDialog):
|
|
194
168
|
|
|
@@ -206,8 +180,9 @@ class QtPluginDialog(BaseQtPluginDialog):
|
|
|
206
180
|
self.setStyleSheet(stylesheet)
|
|
207
181
|
|
|
208
182
|
def _add_installed(self, pkg_name=None):
|
|
183
|
+
use_npe2_adaptor = get_settings().plugins.use_npe2_adaptor
|
|
209
184
|
pm2 = npe2.PluginManager.instance()
|
|
210
|
-
pm2.discover()
|
|
185
|
+
pm2.discover(include_npe1=use_npe2_adaptor)
|
|
211
186
|
for manifest in pm2.iter_manifests():
|
|
212
187
|
distname = normalized_name(manifest.name or '')
|
|
213
188
|
if distname in self.already_installed or distname == 'napari':
|
|
@@ -220,26 +195,32 @@ class QtPluginDialog(BaseQtPluginDialog):
|
|
|
220
195
|
distname, enabled, distname, plugin_api_version=npev
|
|
221
196
|
)
|
|
222
197
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
distname
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
198
|
+
if not use_npe2_adaptor:
|
|
199
|
+
napari.plugins.plugin_manager.discover() # since they might not be loaded yet
|
|
200
|
+
for (
|
|
201
|
+
plugin_name,
|
|
202
|
+
_,
|
|
203
|
+
distname,
|
|
204
|
+
) in napari.plugins.plugin_manager.iter_available():
|
|
205
|
+
# not showing these in the plugin dialog
|
|
206
|
+
if plugin_name in (
|
|
207
|
+
'napari_plugin_engine',
|
|
208
|
+
'napari_plugin_manager',
|
|
209
|
+
):
|
|
210
|
+
continue
|
|
211
|
+
if normalized_name(distname or '') in self.already_installed:
|
|
212
|
+
continue
|
|
213
|
+
if (
|
|
214
|
+
normalized_name(distname or '') == pkg_name
|
|
215
|
+
or pkg_name is None
|
|
216
|
+
):
|
|
217
|
+
self._add_to_installed(
|
|
218
|
+
distname,
|
|
219
|
+
not napari.plugins.plugin_manager.is_blocked(
|
|
220
|
+
plugin_name
|
|
221
|
+
),
|
|
222
|
+
normalized_name(distname or ''),
|
|
223
|
+
)
|
|
243
224
|
self._update_plugin_count()
|
|
244
225
|
|
|
245
226
|
for i in range(self.installed_list.count()):
|
|
@@ -248,12 +229,11 @@ class QtPluginDialog(BaseQtPluginDialog):
|
|
|
248
229
|
if widget.name == pkg_name:
|
|
249
230
|
self.installed_list.scrollToItem(item)
|
|
250
231
|
self.installed_list.setCurrentItem(item)
|
|
251
|
-
if widget.plugin_api_version != 1:
|
|
252
|
-
_show_message(widget)
|
|
253
232
|
break
|
|
254
233
|
|
|
255
234
|
def _fetch_available_plugins(self, clear_cache: bool = False):
|
|
256
|
-
get_settings()
|
|
235
|
+
settings = get_settings()
|
|
236
|
+
use_npe2_adaptor = settings.plugins.use_npe2_adaptor
|
|
257
237
|
|
|
258
238
|
if clear_cache:
|
|
259
239
|
cache_clear()
|
|
@@ -267,7 +247,7 @@ class QtPluginDialog(BaseQtPluginDialog):
|
|
|
267
247
|
self.worker.start()
|
|
268
248
|
|
|
269
249
|
pm2 = npe2.PluginManager.instance()
|
|
270
|
-
pm2.discover()
|
|
250
|
+
pm2.discover(include_npe1=use_npe2_adaptor)
|
|
271
251
|
|
|
272
252
|
def _loading_gif(self):
|
|
273
253
|
load_gif = str(Path(napari.resources.__file__).parent / "loading.gif")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from qtpy.QtWidgets import QDialog, QLabel, QPushButton, QVBoxLayout, QWidget
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RestartWarningDialog(QDialog):
|
|
5
|
+
def __init__(self, parent: QWidget) -> None:
|
|
6
|
+
super().__init__(parent)
|
|
7
|
+
self.setWindowTitle('Restart napari')
|
|
8
|
+
okay_btn = QPushButton('Okay')
|
|
9
|
+
self.restart_warning_text = """
|
|
10
|
+
Plugins have been installed or uninstalled. If you notice any
|
|
11
|
+
issues with plugin functionality, you may need to restart napari.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
okay_btn.clicked.connect(self.accept)
|
|
15
|
+
|
|
16
|
+
layout = QVBoxLayout()
|
|
17
|
+
layout.addWidget(QLabel(self.restart_warning_text))
|
|
18
|
+
layout.addWidget(okay_btn)
|
|
19
|
+
self.setLayout(layout)
|
napari_plugin_manager/utils.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import sys
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
def is_conda_package(pkg: str, prefix:
|
|
6
|
+
def is_conda_package(pkg: str, prefix: str | None = None) -> bool:
|
|
8
7
|
"""Determines if plugin was installed through conda.
|
|
9
8
|
|
|
10
9
|
Returns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: napari-plugin-manager
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Install plugins for napari, in napari.
|
|
5
5
|
Author-email: napari team <napari-steering-council@googlegroups.com>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -43,9 +43,10 @@ Classifier: License :: OSI Approved :: BSD License
|
|
|
43
43
|
Classifier: Programming Language :: C
|
|
44
44
|
Classifier: Programming Language :: Python
|
|
45
45
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
46
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
47
46
|
Classifier: Programming Language :: Python :: 3.10
|
|
48
47
|
Classifier: Programming Language :: Python :: 3.11
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
49
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
49
50
|
Classifier: Topic :: Scientific/Engineering
|
|
50
51
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
51
52
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
@@ -55,7 +56,7 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
55
56
|
Classifier: Operating System :: POSIX
|
|
56
57
|
Classifier: Operating System :: Unix
|
|
57
58
|
Classifier: Operating System :: MacOS
|
|
58
|
-
Requires-Python: >=3.
|
|
59
|
+
Requires-Python: >=3.10
|
|
59
60
|
Description-Content-Type: text/markdown
|
|
60
61
|
License-File: LICENSE
|
|
61
62
|
Requires-Dist: npe2
|
|
@@ -64,12 +65,12 @@ Requires-Dist: superqt
|
|
|
64
65
|
Requires-Dist: pip
|
|
65
66
|
Requires-Dist: packaging
|
|
66
67
|
Provides-Extra: dev
|
|
67
|
-
Requires-Dist:
|
|
68
|
+
Requires-Dist: PyQt6; extra == "dev"
|
|
68
69
|
Requires-Dist: pre-commit; extra == "dev"
|
|
69
70
|
Provides-Extra: testing
|
|
71
|
+
Requires-Dist: coverage; extra == "testing"
|
|
70
72
|
Requires-Dist: flaky; extra == "testing"
|
|
71
73
|
Requires-Dist: pytest; extra == "testing"
|
|
72
|
-
Requires-Dist: pytest-cov; extra == "testing"
|
|
73
74
|
Requires-Dist: pytest-qt; extra == "testing"
|
|
74
75
|
Requires-Dist: virtualenv; extra == "testing"
|
|
75
76
|
Provides-Extra: docs
|
|
@@ -80,6 +81,7 @@ Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
|
80
81
|
Requires-Dist: sphinx-favicon; extra == "docs"
|
|
81
82
|
Requires-Dist: myst-nb; extra == "docs"
|
|
82
83
|
Requires-Dist: napari-sphinx-theme>=0.3.0; extra == "docs"
|
|
84
|
+
Dynamic: license-file
|
|
83
85
|
|
|
84
86
|
# napari-plugin-manager
|
|
85
87
|
|
|
@@ -230,6 +232,24 @@ You can cancel the process at any time by clicking the `Cancel` button of each p
|
|
|
230
232
|
|
|
231
233
|

|
|
232
234
|
|
|
235
|
+
### Export/Import plugins
|
|
236
|
+
|
|
237
|
+
You can export the list of install plugins by clicking on the `Export` button located on the top right
|
|
238
|
+
corner of the UI. This will prompt a dialog to select the location and name of the text file where
|
|
239
|
+
the installed plugin list will be saved.
|
|
240
|
+
|
|
241
|
+
The file is plain text and plugins are listed in the [requirements.txt](https://pip.pypa.io/en/stable/reference/requirements-file-format/) format:
|
|
242
|
+
```
|
|
243
|
+
plugin_name==0.1.2
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
This file can be shared and then imported by clicking on the `Import` button located on the top right
|
|
247
|
+
corner of the UI. This will prompt a dialog to select the location of the text file to import.
|
|
248
|
+
|
|
249
|
+
After selecting the file, the plugin manager will attempt to install all the listed plugins using the auto-detected default installer.
|
|
250
|
+
|
|
251
|
+

|
|
252
|
+
|
|
233
253
|
### Batch actions
|
|
234
254
|
|
|
235
255
|
You don't need wait for one action to finish before you can start another one. You can add more
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
napari_plugin_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
napari_plugin_manager/_version.py,sha256=Y4jy4bEMmwl_qNPCmiMFnlQ2ofMoqyG37hp8uwI3m10,511
|
|
3
|
+
napari_plugin_manager/base_qt_package_installer.py,sha256=Tp2dt5BBT-5CsXXlQPIPxuXSDMvUgKy2kSgw2Y2G9Dk,21825
|
|
4
|
+
napari_plugin_manager/base_qt_plugin_dialog.py,sha256=gkrEkVf_WGJHjZFORMoCiw-gxCwFuTZ04Q0PGFK8gLk,65978
|
|
5
|
+
napari_plugin_manager/npe2api.py,sha256=bXmhwFkwKw_1DfnGLhWhEGaEAA3oYPFaw4eR4SX2Nyg,4075
|
|
6
|
+
napari_plugin_manager/qt_package_installer.py,sha256=j-pacW6wHVq3iJaZXsj6D-_VH25Fz-55r7clcd_CQvE,2804
|
|
7
|
+
napari_plugin_manager/qt_plugin_dialog.py,sha256=Ig4TtjCh9Z2Dx_QpiOsfLlgolPJHmJ5_7VuTU2DE0Ag,9607
|
|
8
|
+
napari_plugin_manager/qt_warning_dialog.py,sha256=ue4CeMptlBBkBctPg7qCayamrkm75iLqASSFUvwo_Bc,669
|
|
9
|
+
napari_plugin_manager/qt_widgets.py,sha256=O8t5CbN8r_16cQzshyjvhTEYdUcj7OX0-bfYIiN2uSs,356
|
|
10
|
+
napari_plugin_manager/styles.qss,sha256=9ODPba2IorJybWObWoEO9VGq4AO0IYlAa8brN14tgZU,7379
|
|
11
|
+
napari_plugin_manager/utils.py,sha256=V0QmCQNP2OwszoQ2n9Gnau9jH81rLKwfskL4ebex7EE,722
|
|
12
|
+
napari_plugin_manager/_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
napari_plugin_manager/_tests/conftest.py,sha256=OvzenfBP2oIS6x8ksr9FhPXdsLV3Q_3Kzr6PRJe45Uc,1885
|
|
14
|
+
napari_plugin_manager/_tests/test_base_installer_process.py,sha256=Cv-nBnUeNAX6pYUE1zs38I9vGtCE-ahBN4q-xcBH-pw,561
|
|
15
|
+
napari_plugin_manager/_tests/test_installer_process.py,sha256=qPSDcYWPQ08gzM38av2tcE9XCtruHv-Mo6duQ0sZp-8,11614
|
|
16
|
+
napari_plugin_manager/_tests/test_npe2api.py,sha256=GRXucH7kWHt6thgueppHHWaToTvQG1PXH6UECFeVxcM,1225
|
|
17
|
+
napari_plugin_manager/_tests/test_qt_plugin_dialog.py,sha256=IRXodt3IrE7rXHZO0R69lhz4_2mMW3MOE9LLQ5tCxWY,21727
|
|
18
|
+
napari_plugin_manager/_tests/test_utils.py,sha256=7EilxmDkRjU6UO2AnaqyYovdAs18D0ZA5GCVGN62-3M,720
|
|
19
|
+
napari_plugin_manager-0.1.5.dist-info/licenses/LICENSE,sha256=8dAlKbOqTMYe9L-gL_kEx5Xr1Sd0AbaWQDUkpiOp3vI,1506
|
|
20
|
+
napari_plugin_manager-0.1.5.dist-info/METADATA,sha256=zjeelaebx95UsROBhCGhzqJ4JG_syOGxg7RMSgTbmGI,13810
|
|
21
|
+
napari_plugin_manager-0.1.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
22
|
+
napari_plugin_manager-0.1.5.dist-info/top_level.txt,sha256=pmCqLetuumhY1CKSuTZ5eQsitzazrSvc7V_mkugEPTY,22
|
|
23
|
+
napari_plugin_manager-0.1.5.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
napari_plugin_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
napari_plugin_manager/_version.py,sha256=9GTNkADgEYZ6fEjCvZZUdKyqxiPIgtskLFZNJz7nq_U,411
|
|
3
|
-
napari_plugin_manager/base_qt_package_installer.py,sha256=XjfI_sqiCIkOLop4scpDio2sf7zIG-LwjrvPIuB4R7Q,20166
|
|
4
|
-
napari_plugin_manager/base_qt_plugin_dialog.py,sha256=RtvkAnPGDkivjihlSAE2zi3rNP-4DjorFh9tSl9t5_k,63834
|
|
5
|
-
napari_plugin_manager/npe2api.py,sha256=vEBKo1JBnPxRoVzMWedODXYNnD5OIfYJM-XeNWm6DsQ,4092
|
|
6
|
-
napari_plugin_manager/qt_package_installer.py,sha256=AcEu3htlXK1FsYdicqtdTCkKF9TN2XPGAIYgEgVZTHM,2821
|
|
7
|
-
napari_plugin_manager/qt_plugin_dialog.py,sha256=oWkaB06oW5SNDtg-XohnC5Aln7CBhzgeG-p6K5DfC8g,10124
|
|
8
|
-
napari_plugin_manager/qt_widgets.py,sha256=O8t5CbN8r_16cQzshyjvhTEYdUcj7OX0-bfYIiN2uSs,356
|
|
9
|
-
napari_plugin_manager/styles.qss,sha256=9ODPba2IorJybWObWoEO9VGq4AO0IYlAa8brN14tgZU,7379
|
|
10
|
-
napari_plugin_manager/utils.py,sha256=wG_lGPaMmbfyH-q7oTWDYSI2iAKiZ3cqxyjlRlbvFJo,753
|
|
11
|
-
napari_plugin_manager/_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
napari_plugin_manager/_tests/conftest.py,sha256=OvzenfBP2oIS6x8ksr9FhPXdsLV3Q_3Kzr6PRJe45Uc,1885
|
|
13
|
-
napari_plugin_manager/_tests/test_base_installer_process.py,sha256=Cv-nBnUeNAX6pYUE1zs38I9vGtCE-ahBN4q-xcBH-pw,561
|
|
14
|
-
napari_plugin_manager/_tests/test_installer_process.py,sha256=-q3u6UqCccWbHXjtWROUikFFONNCW3PRVqXVJUno9pg,11600
|
|
15
|
-
napari_plugin_manager/_tests/test_npe2api.py,sha256=i0W1CrWcRzjL1Rk2WELlelOiBiHbOS37QKpmyaRn1QA,1031
|
|
16
|
-
napari_plugin_manager/_tests/test_qt_plugin_dialog.py,sha256=q6Cj19069KwYwivB-0_sB_64e4ICYoFR_PcjvDwvLts,19520
|
|
17
|
-
napari_plugin_manager/_tests/test_utils.py,sha256=7EilxmDkRjU6UO2AnaqyYovdAs18D0ZA5GCVGN62-3M,720
|
|
18
|
-
napari_plugin_manager-0.1.4.dist-info/LICENSE,sha256=8dAlKbOqTMYe9L-gL_kEx5Xr1Sd0AbaWQDUkpiOp3vI,1506
|
|
19
|
-
napari_plugin_manager-0.1.4.dist-info/METADATA,sha256=Ld1YHwF15gQgkLpOfbbgqqdi19GbF4Oyjgb0cVpCWW0,12782
|
|
20
|
-
napari_plugin_manager-0.1.4.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
|
21
|
-
napari_plugin_manager-0.1.4.dist-info/top_level.txt,sha256=pmCqLetuumhY1CKSuTZ5eQsitzazrSvc7V_mkugEPTY,22
|
|
22
|
-
napari_plugin_manager-0.1.4.dist-info/RECORD,,
|
{napari_plugin_manager-0.1.4.dist-info → napari_plugin_manager-0.1.5.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|
{napari_plugin_manager-0.1.4.dist-info → napari_plugin_manager-0.1.5.dist-info}/top_level.txt
RENAMED
|
File without changes
|