napari-plugin-manager 0.1.0a2__py3-none-any.whl → 0.1.2__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/conftest.py +45 -0
- napari_plugin_manager/_tests/test_installer_process.py +179 -67
- napari_plugin_manager/_tests/test_npe2api.py +54 -0
- napari_plugin_manager/_tests/test_qt_plugin_dialog.py +269 -60
- napari_plugin_manager/_tests/test_utils.py +27 -0
- napari_plugin_manager/_version.py +14 -2
- napari_plugin_manager/npe2api.py +131 -0
- napari_plugin_manager/qt_package_installer.py +166 -60
- napari_plugin_manager/qt_plugin_dialog.py +871 -407
- napari_plugin_manager/qt_widgets.py +14 -0
- napari_plugin_manager/styles.qss +383 -0
- napari_plugin_manager/utils.py +22 -0
- napari_plugin_manager-0.1.2.dist-info/METADATA +257 -0
- napari_plugin_manager-0.1.2.dist-info/RECORD +19 -0
- {napari_plugin_manager-0.1.0a2.dist-info → napari_plugin_manager-0.1.2.dist-info}/WHEEL +1 -1
- napari_plugin_manager-0.1.0a2.dist-info/METADATA +0 -107
- napari_plugin_manager-0.1.0a2.dist-info/RECORD +0 -13
- {napari_plugin_manager-0.1.0a2.dist-info → napari_plugin_manager-0.1.2.dist-info}/LICENSE +0 -0
- {napari_plugin_manager-0.1.0a2.dist-info → napari_plugin_manager-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
2
4
|
import sys
|
|
3
5
|
from typing import Generator, Optional, Tuple
|
|
4
6
|
from unittest.mock import patch
|
|
@@ -9,17 +11,19 @@ import pytest
|
|
|
9
11
|
import qtpy
|
|
10
12
|
from napari.plugins._tests.test_npe2 import mock_pm # noqa
|
|
11
13
|
from napari.utils.translations import trans
|
|
14
|
+
from qtpy.QtCore import QMimeData, QPointF, Qt, QUrl
|
|
15
|
+
from qtpy.QtGui import QDropEvent
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
from napari_plugin_manager.qt_package_installer import InstallerActions
|
|
15
|
-
|
|
16
|
-
if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] == (3, 11):
|
|
17
|
+
if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] > (3, 10):
|
|
17
18
|
pytest.skip(
|
|
18
|
-
"Known PySide2 x Python
|
|
19
|
-
"
|
|
19
|
+
"Known PySide2 x Python incompatibility: "
|
|
20
|
+
"... object cannot be interpreted as an integer",
|
|
20
21
|
allow_module_level=True,
|
|
21
22
|
)
|
|
22
23
|
|
|
24
|
+
from napari_plugin_manager import qt_plugin_dialog
|
|
25
|
+
from napari_plugin_manager.qt_package_installer import InstallerActions
|
|
26
|
+
|
|
23
27
|
N_MOCKED_PLUGINS = 2
|
|
24
28
|
|
|
25
29
|
|
|
@@ -33,11 +37,10 @@ def _iter_napari_pypi_plugin_info(
|
|
|
33
37
|
This will mock napari.plugins.pypi.iter_napari_plugin_info` for pypi.
|
|
34
38
|
|
|
35
39
|
It will return two fake plugins that will populate the available plugins
|
|
36
|
-
list (the bottom one).
|
|
37
|
-
conda-forge so will be greyed out ("test-name-0"). The second plugin will
|
|
38
|
-
be available on conda-forge so will be enabled ("test-name-1").
|
|
40
|
+
list (the bottom one).
|
|
39
41
|
"""
|
|
40
42
|
# This mock `base_data`` will be the same for both fake plugins.
|
|
43
|
+
packages = ['pyzenhub', 'requests', 'my-plugin', 'my-test-old-plugin-1']
|
|
41
44
|
base_data = {
|
|
42
45
|
"metadata_version": "1.0",
|
|
43
46
|
"version": "0.1.0",
|
|
@@ -46,28 +49,31 @@ def _iter_napari_pypi_plugin_info(
|
|
|
46
49
|
"author": "test author",
|
|
47
50
|
"license": "UNKNOWN",
|
|
48
51
|
}
|
|
49
|
-
for i in range(
|
|
50
|
-
yield npe2.PackageMetadata(name=f"
|
|
52
|
+
for i in range(len(packages)):
|
|
53
|
+
yield npe2.PackageMetadata(name=f"{packages[i]}", **base_data), bool(
|
|
51
54
|
i
|
|
52
55
|
), {
|
|
53
56
|
"home_page": 'www.mywebsite.com',
|
|
54
|
-
"pypi_versions": ['
|
|
55
|
-
"conda_versions": ['
|
|
57
|
+
"pypi_versions": ['2.31.0'],
|
|
58
|
+
"conda_versions": ['2.32.1'],
|
|
59
|
+
'display_name': packages[i].upper(),
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
class PluginsMock:
|
|
60
64
|
def __init__(self):
|
|
61
65
|
self.plugins = {
|
|
62
|
-
'
|
|
63
|
-
'
|
|
66
|
+
'requests': True,
|
|
67
|
+
'pyzenhub': True,
|
|
64
68
|
'my-plugin': True,
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
class OldPluginsMock:
|
|
69
73
|
def __init__(self):
|
|
70
|
-
self.plugins = [
|
|
74
|
+
self.plugins = [
|
|
75
|
+
('my-test-old-plugin-1', False, 'my-test-old-plugin-1')
|
|
76
|
+
]
|
|
71
77
|
self.enabled = [True]
|
|
72
78
|
|
|
73
79
|
|
|
@@ -85,6 +91,9 @@ class WarnPopupMock:
|
|
|
85
91
|
def __init__(self, text):
|
|
86
92
|
self._is_visible = False
|
|
87
93
|
|
|
94
|
+
def show(self):
|
|
95
|
+
self._is_visible = True
|
|
96
|
+
|
|
88
97
|
def exec_(self):
|
|
89
98
|
self._is_visible = True
|
|
90
99
|
|
|
@@ -97,6 +106,9 @@ class WarnPopupMock:
|
|
|
97
106
|
def close(self):
|
|
98
107
|
self._is_visible = False
|
|
99
108
|
|
|
109
|
+
def width(self):
|
|
110
|
+
return 100
|
|
111
|
+
|
|
100
112
|
|
|
101
113
|
@pytest.fixture(params=[True, False], ids=["constructor", "no-constructor"])
|
|
102
114
|
def plugin_dialog(
|
|
@@ -177,7 +189,10 @@ def plugin_dialog(
|
|
|
177
189
|
monkeypatch.setattr(
|
|
178
190
|
qt_plugin_dialog, "running_as_constructor_app", lambda: request.param
|
|
179
191
|
)
|
|
180
|
-
|
|
192
|
+
monkeypatch.setattr(
|
|
193
|
+
qt_plugin_dialog, "IS_NAPARI_CONDA_INSTALLED", request.param
|
|
194
|
+
)
|
|
195
|
+
monkeypatch.setattr(qt_plugin_dialog, "ON_BUNDLE", request.param)
|
|
181
196
|
monkeypatch.setattr(
|
|
182
197
|
napari.plugins, 'plugin_manager', OldPluginManagerMock()
|
|
183
198
|
)
|
|
@@ -187,6 +202,7 @@ def plugin_dialog(
|
|
|
187
202
|
monkeypatch.setattr(npe2, 'PluginManager', PluginManagerMock())
|
|
188
203
|
|
|
189
204
|
widget = qt_plugin_dialog.QtPluginDialog()
|
|
205
|
+
# monkeypatch.setattr(widget, '_tag_outdated_plugins', lambda: None)
|
|
190
206
|
widget.show()
|
|
191
207
|
qtbot.waitUntil(widget.isVisible, timeout=300)
|
|
192
208
|
|
|
@@ -201,17 +217,15 @@ def plugin_dialog(
|
|
|
201
217
|
assert not widget._add_items_timer.isActive()
|
|
202
218
|
|
|
203
219
|
|
|
204
|
-
def test_filter_not_available_plugins(plugin_dialog):
|
|
220
|
+
def test_filter_not_available_plugins(request, plugin_dialog):
|
|
205
221
|
"""
|
|
206
222
|
Check that the plugins listed under available plugins are
|
|
207
223
|
enabled and disabled accordingly.
|
|
208
|
-
|
|
209
|
-
The first plugin ("test-name-0") is not available on conda-forge and
|
|
210
|
-
should be disabled, and show a tooltip warning.
|
|
211
|
-
|
|
212
|
-
The second plugin ("test-name-1") is available on conda-forge and
|
|
213
|
-
should be enabled without the tooltip warning.
|
|
214
224
|
"""
|
|
225
|
+
if "no-constructor" in request.node.name:
|
|
226
|
+
pytest.skip(
|
|
227
|
+
reason="This test is only relevant for constructor-based installs"
|
|
228
|
+
)
|
|
215
229
|
item = plugin_dialog.available_list.item(0)
|
|
216
230
|
widget = plugin_dialog.available_list.itemWidget(item)
|
|
217
231
|
if widget:
|
|
@@ -231,14 +245,14 @@ def test_filter_available_plugins(plugin_dialog):
|
|
|
231
245
|
"""
|
|
232
246
|
plugin_dialog.filter("")
|
|
233
247
|
assert plugin_dialog.available_list.count() == 2
|
|
234
|
-
assert plugin_dialog.available_list.
|
|
248
|
+
assert plugin_dialog.available_list.count_visible() == 2
|
|
235
249
|
|
|
236
250
|
plugin_dialog.filter("no-match@123")
|
|
237
|
-
assert plugin_dialog.available_list.
|
|
251
|
+
assert plugin_dialog.available_list.count_visible() == 0
|
|
238
252
|
|
|
239
253
|
plugin_dialog.filter("")
|
|
240
|
-
plugin_dialog.filter("
|
|
241
|
-
assert plugin_dialog.available_list.
|
|
254
|
+
plugin_dialog.filter("requests")
|
|
255
|
+
assert plugin_dialog.available_list.count_visible() == 1
|
|
242
256
|
|
|
243
257
|
|
|
244
258
|
def test_filter_installed_plugins(plugin_dialog):
|
|
@@ -247,10 +261,10 @@ def test_filter_installed_plugins(plugin_dialog):
|
|
|
247
261
|
list (the top one).
|
|
248
262
|
"""
|
|
249
263
|
plugin_dialog.filter("")
|
|
250
|
-
assert plugin_dialog.installed_list.
|
|
264
|
+
assert plugin_dialog.installed_list.count_visible() >= 0
|
|
251
265
|
|
|
252
266
|
plugin_dialog.filter("no-match@123")
|
|
253
|
-
assert plugin_dialog.installed_list.
|
|
267
|
+
assert plugin_dialog.installed_list.count_visible() == 0
|
|
254
268
|
|
|
255
269
|
|
|
256
270
|
def test_visible_widgets(request, plugin_dialog):
|
|
@@ -258,26 +272,29 @@ def test_visible_widgets(request, plugin_dialog):
|
|
|
258
272
|
Test that the direct entry button and textbox are visible
|
|
259
273
|
"""
|
|
260
274
|
if "no-constructor" not in request.node.name:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
275
|
+
pytest.skip(
|
|
276
|
+
reason="Tested functionality not available in constructor-based installs"
|
|
277
|
+
)
|
|
264
278
|
assert plugin_dialog.direct_entry_edit.isVisible()
|
|
265
279
|
assert plugin_dialog.direct_entry_btn.isVisible()
|
|
266
280
|
|
|
267
281
|
|
|
268
|
-
def test_version_dropdown(
|
|
282
|
+
def test_version_dropdown(plugin_dialog, qtbot):
|
|
269
283
|
"""
|
|
270
284
|
Test that when the source drop down is changed, it displays the other versions properly.
|
|
271
285
|
"""
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
286
|
+
# qtbot.wait(10000)
|
|
287
|
+
widget = plugin_dialog.available_list.item(0).widget
|
|
288
|
+
count = widget.version_choice_dropdown.count()
|
|
289
|
+
if count == 2:
|
|
290
|
+
assert widget.version_choice_dropdown.currentText() == "2.31.0"
|
|
291
|
+
# switch from PyPI source to conda one.
|
|
292
|
+
widget.source_choice_dropdown.setCurrentIndex(1)
|
|
293
|
+
assert widget.version_choice_dropdown.currentText() == "2.32.1"
|
|
277
294
|
|
|
278
295
|
|
|
279
296
|
def test_plugin_list_count_items(plugin_dialog):
|
|
280
|
-
assert plugin_dialog.installed_list.
|
|
297
|
+
assert plugin_dialog.installed_list.count_visible() == 2
|
|
281
298
|
|
|
282
299
|
|
|
283
300
|
def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
@@ -285,7 +302,7 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
285
302
|
with patch.object(qt_plugin_dialog.PluginListItem, "set_busy") as mock:
|
|
286
303
|
plugin_dialog.installed_list.handle_action(
|
|
287
304
|
item,
|
|
288
|
-
'test-
|
|
305
|
+
'my-test-old-plugin-1',
|
|
289
306
|
InstallerActions.UPGRADE,
|
|
290
307
|
)
|
|
291
308
|
mock.assert_called_with(
|
|
@@ -295,16 +312,17 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
295
312
|
with patch.object(qt_plugin_dialog.WarnPopup, "exec_") as mock:
|
|
296
313
|
plugin_dialog.installed_list.handle_action(
|
|
297
314
|
item,
|
|
298
|
-
'test-
|
|
315
|
+
'my-test-old-plugin-1',
|
|
299
316
|
InstallerActions.UNINSTALL,
|
|
300
317
|
)
|
|
301
318
|
assert mock.called
|
|
302
319
|
|
|
303
320
|
item = plugin_dialog.available_list.item(0)
|
|
304
321
|
with patch.object(qt_plugin_dialog.PluginListItem, "set_busy") as mock:
|
|
322
|
+
|
|
305
323
|
plugin_dialog.available_list.handle_action(
|
|
306
324
|
item,
|
|
307
|
-
'test-
|
|
325
|
+
'my-test-old-plugin-1',
|
|
308
326
|
InstallerActions.INSTALL,
|
|
309
327
|
version='3',
|
|
310
328
|
)
|
|
@@ -313,17 +331,10 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
313
331
|
)
|
|
314
332
|
|
|
315
333
|
plugin_dialog.available_list.handle_action(
|
|
316
|
-
item, 'test-
|
|
317
|
-
)
|
|
318
|
-
mock.assert_called_with(
|
|
319
|
-
trans._("cancelling..."), InstallerActions.CANCEL
|
|
334
|
+
item, 'my-test-old-plugin-1', InstallerActions.CANCEL, version='3'
|
|
320
335
|
)
|
|
336
|
+
mock.assert_called_with("", InstallerActions.CANCEL)
|
|
321
337
|
|
|
322
|
-
# Wait for refresh timer, state and worker to be done
|
|
323
|
-
qtbot.waitUntil(
|
|
324
|
-
lambda: not plugin_dialog._add_items_timer.isActive()
|
|
325
|
-
and plugin_dialog.refresh_state == qt_plugin_dialog.RefreshState.DONE
|
|
326
|
-
)
|
|
327
338
|
qtbot.waitUntil(lambda: not plugin_dialog.worker.is_running)
|
|
328
339
|
|
|
329
340
|
|
|
@@ -347,8 +358,12 @@ def test_on_enabled_checkbox(plugin_dialog, qtbot, plugins, old_plugins):
|
|
|
347
358
|
assert old_plugins.enabled[0] is False
|
|
348
359
|
|
|
349
360
|
|
|
350
|
-
def
|
|
351
|
-
"""
|
|
361
|
+
def test_add_items_outdated_and_update(plugin_dialog, qtbot):
|
|
362
|
+
"""
|
|
363
|
+
Test that a plugin is tagged as outdated (a newer version is available), the update button becomes visible.
|
|
364
|
+
|
|
365
|
+
Also check that after doing an update the update button gets hidden.
|
|
366
|
+
"""
|
|
352
367
|
|
|
353
368
|
# The plugin is being added to the available plugins list. When the dialog is being built
|
|
354
369
|
# this one will be listed as available, and it will be found as already installed.
|
|
@@ -360,15 +375,209 @@ def test_add_items_outdated(plugin_dialog):
|
|
|
360
375
|
True,
|
|
361
376
|
{
|
|
362
377
|
"home_page": 'www.mywebsite.com',
|
|
363
|
-
"pypi_versions": ['0.4.0'],
|
|
364
|
-
"conda_versions": ['0.4.0'],
|
|
378
|
+
"pypi_versions": ['0.4.0', '0.1.0'],
|
|
379
|
+
"conda_versions": ['0.4.0', '0.1.0'],
|
|
365
380
|
},
|
|
366
381
|
)
|
|
367
|
-
|
|
368
|
-
plugin_dialog.
|
|
369
|
-
|
|
382
|
+
plugin_dialog._plugin_data_map["my-plugin"] = new_plugin
|
|
383
|
+
plugin_dialog._plugin_queue = [new_plugin]
|
|
370
384
|
plugin_dialog._add_items()
|
|
371
385
|
item = plugin_dialog.installed_list.item(0)
|
|
372
386
|
widget = plugin_dialog.installed_list.itemWidget(item)
|
|
373
|
-
|
|
387
|
+
initial_version = "0.1.0"
|
|
388
|
+
mod_initial_version = initial_version.replace('.', '․') # noqa: RUF001
|
|
374
389
|
assert widget.update_btn.isVisible()
|
|
390
|
+
assert widget.version.text() == mod_initial_version
|
|
391
|
+
assert widget.version.toolTip() == initial_version
|
|
392
|
+
|
|
393
|
+
# Trigger process finished handler to simulated that an update was done
|
|
394
|
+
plugin_dialog._on_process_finished(
|
|
395
|
+
{
|
|
396
|
+
'exit_code': 1,
|
|
397
|
+
'exit_status': 0,
|
|
398
|
+
'action': InstallerActions.UPGRADE,
|
|
399
|
+
'pkgs': ['my-plugin==0.4.0'],
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
updated_version = "0.4.0"
|
|
403
|
+
mod_updated_version = updated_version.replace('.', '․') # noqa: RUF001
|
|
404
|
+
assert not widget.update_btn.isVisible()
|
|
405
|
+
assert widget.version.text() == mod_updated_version
|
|
406
|
+
assert widget.version.toolTip() == updated_version
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@pytest.mark.skipif(
|
|
410
|
+
qtpy.API_NAME.lower().startswith('pyside')
|
|
411
|
+
and sys.version_info[:2] > (3, 10)
|
|
412
|
+
and platform.system() == "Darwin",
|
|
413
|
+
reason='pyside specific bug',
|
|
414
|
+
)
|
|
415
|
+
def test_refresh(qtbot, plugin_dialog):
|
|
416
|
+
with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
|
|
417
|
+
plugin_dialog.refresh(clear_cache=False)
|
|
418
|
+
|
|
419
|
+
with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
|
|
420
|
+
plugin_dialog.refresh(clear_cache=True)
|
|
421
|
+
|
|
422
|
+
with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
|
|
423
|
+
plugin_dialog._refresh_and_clear_cache()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@pytest.mark.skipif(
|
|
427
|
+
qtpy.API_NAME.lower().startswith('pyside')
|
|
428
|
+
and sys.version_info[:2] > (3, 10)
|
|
429
|
+
and platform.system() == "Darwin",
|
|
430
|
+
reason='pyside specific bug',
|
|
431
|
+
)
|
|
432
|
+
def test_toggle_status(plugin_dialog):
|
|
433
|
+
plugin_dialog.toggle_status(True)
|
|
434
|
+
assert plugin_dialog.stdout_text.isVisible()
|
|
435
|
+
plugin_dialog.toggle_status(False)
|
|
436
|
+
assert not plugin_dialog.stdout_text.isVisible()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@pytest.mark.skipif(
|
|
440
|
+
qtpy.API_NAME.lower().startswith('pyside')
|
|
441
|
+
and sys.version_info[:2] > (3, 10)
|
|
442
|
+
and platform.system() == "Darwin",
|
|
443
|
+
reason='pyside specific bug',
|
|
444
|
+
)
|
|
445
|
+
def test_exec(plugin_dialog):
|
|
446
|
+
plugin_dialog.exec_()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@pytest.mark.skipif(
|
|
450
|
+
qtpy.API_NAME.lower().startswith('pyside')
|
|
451
|
+
and sys.version_info[:2] > (3, 10)
|
|
452
|
+
and platform.system() == "Darwin",
|
|
453
|
+
reason='pyside specific bug',
|
|
454
|
+
)
|
|
455
|
+
def test_search_in_available(plugin_dialog):
|
|
456
|
+
idxs = plugin_dialog._search_in_available("test")
|
|
457
|
+
assert idxs == [0, 1, 2, 3]
|
|
458
|
+
idxs = plugin_dialog._search_in_available("*&%$")
|
|
459
|
+
assert idxs == []
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_drop_event(plugin_dialog, tmp_path):
|
|
463
|
+
path_1 = tmp_path / "example-1.txt"
|
|
464
|
+
path_2 = tmp_path / "example-2.txt"
|
|
465
|
+
url_prefix = 'file:///' if os.name == 'nt' else 'file://'
|
|
466
|
+
data = QMimeData()
|
|
467
|
+
data.setUrls(
|
|
468
|
+
[QUrl(f'{url_prefix}{path_1}'), QUrl(f'{url_prefix}{path_2}')]
|
|
469
|
+
)
|
|
470
|
+
event = QDropEvent(
|
|
471
|
+
QPointF(5.0, 5.0), Qt.CopyAction, data, Qt.LeftButton, Qt.NoModifier
|
|
472
|
+
)
|
|
473
|
+
plugin_dialog.dropEvent(event)
|
|
474
|
+
assert plugin_dialog.direct_entry_edit.text() == str(path_1)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@pytest.mark.skipif(
|
|
478
|
+
qtpy.API_NAME.lower().startswith('pyside'), reason='pyside specific bug'
|
|
479
|
+
)
|
|
480
|
+
def test_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
481
|
+
if "[constructor]" in request.node.name:
|
|
482
|
+
pytest.skip(
|
|
483
|
+
reason="This test is only relevant for constructor-based installs"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
487
|
+
item = plugin_dialog.available_list.item(1)
|
|
488
|
+
widget = plugin_dialog.available_list.itemWidget(item)
|
|
489
|
+
with qtbot.waitSignal(
|
|
490
|
+
plugin_dialog.installer.processFinished, timeout=60_000
|
|
491
|
+
) as blocker:
|
|
492
|
+
widget.action_button.click()
|
|
493
|
+
|
|
494
|
+
process_finished_data = blocker.args[0]
|
|
495
|
+
assert process_finished_data['action'] == InstallerActions.INSTALL
|
|
496
|
+
assert process_finished_data['pkgs'][0].startswith("requests")
|
|
497
|
+
qtbot.wait(5000)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@pytest.mark.skipif(
|
|
501
|
+
qtpy.API_NAME.lower().startswith('pyside'), reason='pyside specific bug'
|
|
502
|
+
)
|
|
503
|
+
def test_cancel(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
504
|
+
if "[constructor]" in request.node.name:
|
|
505
|
+
pytest.skip(
|
|
506
|
+
reason="This test is only relevant for constructor-based installs"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
510
|
+
item = plugin_dialog.available_list.item(1)
|
|
511
|
+
widget = plugin_dialog.available_list.itemWidget(item)
|
|
512
|
+
with qtbot.waitSignal(
|
|
513
|
+
plugin_dialog.installer.processFinished, timeout=60_000
|
|
514
|
+
) as blocker:
|
|
515
|
+
widget.action_button.click()
|
|
516
|
+
widget.cancel_btn.click()
|
|
517
|
+
|
|
518
|
+
process_finished_data = blocker.args[0]
|
|
519
|
+
assert process_finished_data['action'] == InstallerActions.CANCEL
|
|
520
|
+
assert process_finished_data['pkgs'][0].startswith("requests")
|
|
521
|
+
assert plugin_dialog.available_list.count() == 2
|
|
522
|
+
assert plugin_dialog.installed_list.count() == 2
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@pytest.mark.skipif(
|
|
526
|
+
qtpy.API_NAME.lower().startswith('pyside'), reason='pyside specific bug'
|
|
527
|
+
)
|
|
528
|
+
def test_cancel_all(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
529
|
+
if "[constructor]" in request.node.name:
|
|
530
|
+
pytest.skip(
|
|
531
|
+
reason="This test is only relevant for constructor-based installs"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
535
|
+
item_1 = plugin_dialog.available_list.item(0)
|
|
536
|
+
item_2 = plugin_dialog.available_list.item(1)
|
|
537
|
+
widget_1 = plugin_dialog.available_list.itemWidget(item_1)
|
|
538
|
+
widget_2 = plugin_dialog.available_list.itemWidget(item_2)
|
|
539
|
+
with qtbot.waitSignal(plugin_dialog.installer.allFinished, timeout=60_000):
|
|
540
|
+
widget_1.action_button.click()
|
|
541
|
+
widget_2.action_button.click()
|
|
542
|
+
plugin_dialog.cancel_all_btn.click()
|
|
543
|
+
|
|
544
|
+
assert plugin_dialog.available_list.count() == 2
|
|
545
|
+
assert plugin_dialog.installed_list.count() == 2
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@pytest.mark.skipif(
|
|
549
|
+
qtpy.API_NAME.lower().startswith('pyside'), reason='pyside specific bug'
|
|
550
|
+
)
|
|
551
|
+
def test_direct_entry_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
552
|
+
if "[constructor]" in request.node.name:
|
|
553
|
+
pytest.skip(
|
|
554
|
+
reason="This test is only relevant for constructor-based installs"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
558
|
+
with qtbot.waitSignal(
|
|
559
|
+
plugin_dialog.installer.processFinished, timeout=60_000
|
|
560
|
+
) as blocker:
|
|
561
|
+
plugin_dialog.direct_entry_edit.setText('requests')
|
|
562
|
+
plugin_dialog.direct_entry_btn.click()
|
|
563
|
+
|
|
564
|
+
process_finished_data = blocker.args[0]
|
|
565
|
+
assert process_finished_data['action'] == InstallerActions.INSTALL
|
|
566
|
+
assert process_finished_data['pkgs'][0].startswith("requests")
|
|
567
|
+
qtbot.wait(5000)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def test_shortcut_close(plugin_dialog, qtbot):
|
|
571
|
+
qtbot.keyClicks(
|
|
572
|
+
plugin_dialog, 'W', modifier=Qt.KeyboardModifier.ControlModifier
|
|
573
|
+
)
|
|
574
|
+
qtbot.wait(200)
|
|
575
|
+
assert not plugin_dialog.isVisible()
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def test_shortcut_quit(plugin_dialog, qtbot):
|
|
579
|
+
qtbot.keyClicks(
|
|
580
|
+
plugin_dialog, 'Q', modifier=Qt.KeyboardModifier.ControlModifier
|
|
581
|
+
)
|
|
582
|
+
qtbot.wait(200)
|
|
583
|
+
assert not plugin_dialog.isVisible()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from napari_plugin_manager.utils import is_conda_package
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize(
|
|
10
|
+
"pkg_name,expected",
|
|
11
|
+
[
|
|
12
|
+
("some-package", True),
|
|
13
|
+
("some-other-package", False),
|
|
14
|
+
("some-package-other", False),
|
|
15
|
+
("other-some-package", False),
|
|
16
|
+
("package", False),
|
|
17
|
+
("some", False),
|
|
18
|
+
],
|
|
19
|
+
)
|
|
20
|
+
def test_is_conda_package(pkg_name, expected, tmp_path):
|
|
21
|
+
mocked_conda_meta = tmp_path / 'conda-meta'
|
|
22
|
+
mocked_conda_meta.mkdir()
|
|
23
|
+
mocked_package = mocked_conda_meta / 'some-package-0.1.1-0.json'
|
|
24
|
+
mocked_package.touch()
|
|
25
|
+
|
|
26
|
+
with patch.object(sys, 'prefix', tmp_path):
|
|
27
|
+
assert is_conda_package(pkg_name) is expected
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
# file generated by setuptools_scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.1.2'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
These convenience functions will be useful for searching pypi for packages
|
|
3
|
+
that match the plugin naming convention, and retrieving related metadata.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from typing import (
|
|
11
|
+
Optional,
|
|
12
|
+
TypedDict,
|
|
13
|
+
cast,
|
|
14
|
+
)
|
|
15
|
+
from urllib.error import HTTPError, URLError
|
|
16
|
+
from urllib.request import Request, urlopen
|
|
17
|
+
|
|
18
|
+
from napari.plugins.utils import normalized_name
|
|
19
|
+
from napari.utils.notifications import show_warning
|
|
20
|
+
from npe2 import PackageMetadata
|
|
21
|
+
from typing_extensions import NotRequired
|
|
22
|
+
|
|
23
|
+
PyPIname = str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache
|
|
27
|
+
def _user_agent() -> str:
|
|
28
|
+
"""Return a user agent string for use in http requests."""
|
|
29
|
+
import platform
|
|
30
|
+
|
|
31
|
+
from napari import __version__
|
|
32
|
+
from napari.utils import misc
|
|
33
|
+
|
|
34
|
+
if misc.running_as_constructor_app():
|
|
35
|
+
env = 'constructor'
|
|
36
|
+
elif misc.in_jupyter():
|
|
37
|
+
env = 'jupyter'
|
|
38
|
+
elif misc.in_ipython():
|
|
39
|
+
env = 'ipython'
|
|
40
|
+
else:
|
|
41
|
+
env = 'python'
|
|
42
|
+
|
|
43
|
+
parts = [
|
|
44
|
+
('napari', __version__),
|
|
45
|
+
('runtime', env),
|
|
46
|
+
(platform.python_implementation(), platform.python_version()),
|
|
47
|
+
(platform.system(), platform.release()),
|
|
48
|
+
]
|
|
49
|
+
return ' '.join(f'{k}/{v}' for k, v in parts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _ShortSummaryDict(TypedDict):
|
|
53
|
+
"""Objects returned at https://npe2api.vercel.app/api/extended_summary ."""
|
|
54
|
+
|
|
55
|
+
name: NotRequired[PyPIname]
|
|
56
|
+
version: str
|
|
57
|
+
summary: str
|
|
58
|
+
author: str
|
|
59
|
+
license: str
|
|
60
|
+
home_page: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SummaryDict(_ShortSummaryDict):
|
|
64
|
+
display_name: NotRequired[str]
|
|
65
|
+
pypi_versions: NotRequired[list[str]]
|
|
66
|
+
conda_versions: NotRequired[list[str]]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@lru_cache
|
|
70
|
+
def plugin_summaries() -> list[SummaryDict]:
|
|
71
|
+
"""Return PackageMetadata object for all known napari plugins."""
|
|
72
|
+
url = 'https://npe2api.vercel.app/api/extended_summary'
|
|
73
|
+
with urlopen(Request(url, headers={'User-Agent': _user_agent()})) as resp:
|
|
74
|
+
return json.load(resp)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@lru_cache
|
|
78
|
+
def conda_map() -> dict[PyPIname, Optional[str]]:
|
|
79
|
+
"""Return map of PyPI package name to conda_channel/package_name ()."""
|
|
80
|
+
url = 'https://npe2api.vercel.app/api/conda'
|
|
81
|
+
with urlopen(Request(url, headers={'User-Agent': _user_agent()})) as resp:
|
|
82
|
+
return json.load(resp)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def iter_napari_plugin_info() -> Iterator[tuple[PackageMetadata, bool, dict]]:
|
|
86
|
+
"""Iterator of tuples of ProjectInfo, Conda availability for all napari plugins."""
|
|
87
|
+
try:
|
|
88
|
+
with ThreadPoolExecutor() as executor:
|
|
89
|
+
data = executor.submit(plugin_summaries)
|
|
90
|
+
_conda = executor.submit(conda_map)
|
|
91
|
+
|
|
92
|
+
conda = _conda.result()
|
|
93
|
+
data_set = data.result()
|
|
94
|
+
except (HTTPError, URLError):
|
|
95
|
+
show_warning(
|
|
96
|
+
'Plugin manager: There seems to be an issue with network connectivity. '
|
|
97
|
+
'Remote plugins cannot be installed, only local ones.\n'
|
|
98
|
+
)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
conda_set = {normalized_name(x) for x in conda}
|
|
102
|
+
for info in data_set:
|
|
103
|
+
info_copy = dict(info)
|
|
104
|
+
info_copy.pop('display_name', None)
|
|
105
|
+
pypi_versions = info_copy.pop('pypi_versions')
|
|
106
|
+
conda_versions = info_copy.pop('conda_versions')
|
|
107
|
+
info_ = cast(_ShortSummaryDict, info_copy)
|
|
108
|
+
|
|
109
|
+
# TODO: use this better.
|
|
110
|
+
# this would require changing the api that qt_plugin_dialog expects to
|
|
111
|
+
# receive
|
|
112
|
+
|
|
113
|
+
# TODO: once the new version of npe2 is out, this can be refactored
|
|
114
|
+
# to all the metadata includes the conda and pypi versions.
|
|
115
|
+
extra_info = {
|
|
116
|
+
'home_page': info_.get('home_page', ''),
|
|
117
|
+
'display_name': info.get('display_name', ''),
|
|
118
|
+
'pypi_versions': pypi_versions,
|
|
119
|
+
'conda_versions': conda_versions,
|
|
120
|
+
}
|
|
121
|
+
info_['name'] = normalized_name(info_['name'])
|
|
122
|
+
meta = PackageMetadata(**info_) # type:ignore[call-arg]
|
|
123
|
+
|
|
124
|
+
yield meta, (info_['name'] in conda_set), extra_info
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def cache_clear():
|
|
128
|
+
"""Clear the cache for all cached functions in this module."""
|
|
129
|
+
plugin_summaries.cache_clear()
|
|
130
|
+
conda_map.cache_clear()
|
|
131
|
+
_user_agent.cache_clear()
|