napari-plugin-manager 0.1.0a1__py3-none-any.whl → 0.1.1__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 +271 -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 +872 -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.1.dist-info/METADATA +257 -0
- napari_plugin_manager-0.1.1.dist-info/RECORD +19 -0
- {napari_plugin_manager-0.1.0a1.dist-info → napari_plugin_manager-0.1.1.dist-info}/WHEEL +1 -1
- napari_plugin_manager-0.1.0a1.dist-info/METADATA +0 -108
- napari_plugin_manager-0.1.0a1.dist-info/RECORD +0 -13
- {napari_plugin_manager-0.1.0a1.dist-info → napari_plugin_manager-0.1.1.dist-info}/LICENSE +0 -0
- {napari_plugin_manager-0.1.0a1.dist-info → napari_plugin_manager-0.1.1.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,21 @@ 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
|
-
|
|
15
|
-
|
|
16
|
-
if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] == (3, 11):
|
|
17
|
+
if (qtpy.API_NAME == 'PySide2') or (
|
|
18
|
+
sys.version_info[:2] > (3, 10) and platform.system() == "Linux"
|
|
19
|
+
):
|
|
17
20
|
pytest.skip(
|
|
18
|
-
"Known PySide2 x Python
|
|
19
|
-
"
|
|
21
|
+
"Known PySide2 x Python incompatibility: "
|
|
22
|
+
"... object cannot be interpreted as an integer",
|
|
20
23
|
allow_module_level=True,
|
|
21
24
|
)
|
|
22
25
|
|
|
26
|
+
from napari_plugin_manager import qt_plugin_dialog
|
|
27
|
+
from napari_plugin_manager.qt_package_installer import InstallerActions
|
|
28
|
+
|
|
23
29
|
N_MOCKED_PLUGINS = 2
|
|
24
30
|
|
|
25
31
|
|
|
@@ -33,11 +39,10 @@ def _iter_napari_pypi_plugin_info(
|
|
|
33
39
|
This will mock napari.plugins.pypi.iter_napari_plugin_info` for pypi.
|
|
34
40
|
|
|
35
41
|
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").
|
|
42
|
+
list (the bottom one).
|
|
39
43
|
"""
|
|
40
44
|
# This mock `base_data`` will be the same for both fake plugins.
|
|
45
|
+
packages = ['pyzenhub', 'requests', 'my-plugin', 'my-test-old-plugin-1']
|
|
41
46
|
base_data = {
|
|
42
47
|
"metadata_version": "1.0",
|
|
43
48
|
"version": "0.1.0",
|
|
@@ -46,28 +51,31 @@ def _iter_napari_pypi_plugin_info(
|
|
|
46
51
|
"author": "test author",
|
|
47
52
|
"license": "UNKNOWN",
|
|
48
53
|
}
|
|
49
|
-
for i in range(
|
|
50
|
-
yield npe2.PackageMetadata(name=f"
|
|
54
|
+
for i in range(len(packages)):
|
|
55
|
+
yield npe2.PackageMetadata(name=f"{packages[i]}", **base_data), bool(
|
|
51
56
|
i
|
|
52
57
|
), {
|
|
53
58
|
"home_page": 'www.mywebsite.com',
|
|
54
|
-
"pypi_versions": ['
|
|
55
|
-
"conda_versions": ['
|
|
59
|
+
"pypi_versions": ['2.31.0'],
|
|
60
|
+
"conda_versions": ['2.32.1'],
|
|
61
|
+
'display_name': packages[i].upper(),
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
class PluginsMock:
|
|
60
66
|
def __init__(self):
|
|
61
67
|
self.plugins = {
|
|
62
|
-
'
|
|
63
|
-
'
|
|
68
|
+
'requests': True,
|
|
69
|
+
'pyzenhub': True,
|
|
64
70
|
'my-plugin': True,
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
|
|
68
74
|
class OldPluginsMock:
|
|
69
75
|
def __init__(self):
|
|
70
|
-
self.plugins = [
|
|
76
|
+
self.plugins = [
|
|
77
|
+
('my-test-old-plugin-1', False, 'my-test-old-plugin-1')
|
|
78
|
+
]
|
|
71
79
|
self.enabled = [True]
|
|
72
80
|
|
|
73
81
|
|
|
@@ -85,6 +93,9 @@ class WarnPopupMock:
|
|
|
85
93
|
def __init__(self, text):
|
|
86
94
|
self._is_visible = False
|
|
87
95
|
|
|
96
|
+
def show(self):
|
|
97
|
+
self._is_visible = True
|
|
98
|
+
|
|
88
99
|
def exec_(self):
|
|
89
100
|
self._is_visible = True
|
|
90
101
|
|
|
@@ -97,6 +108,9 @@ class WarnPopupMock:
|
|
|
97
108
|
def close(self):
|
|
98
109
|
self._is_visible = False
|
|
99
110
|
|
|
111
|
+
def width(self):
|
|
112
|
+
return 100
|
|
113
|
+
|
|
100
114
|
|
|
101
115
|
@pytest.fixture(params=[True, False], ids=["constructor", "no-constructor"])
|
|
102
116
|
def plugin_dialog(
|
|
@@ -177,7 +191,10 @@ def plugin_dialog(
|
|
|
177
191
|
monkeypatch.setattr(
|
|
178
192
|
qt_plugin_dialog, "running_as_constructor_app", lambda: request.param
|
|
179
193
|
)
|
|
180
|
-
|
|
194
|
+
monkeypatch.setattr(
|
|
195
|
+
qt_plugin_dialog, "IS_NAPARI_CONDA_INSTALLED", request.param
|
|
196
|
+
)
|
|
197
|
+
monkeypatch.setattr(qt_plugin_dialog, "ON_BUNDLE", request.param)
|
|
181
198
|
monkeypatch.setattr(
|
|
182
199
|
napari.plugins, 'plugin_manager', OldPluginManagerMock()
|
|
183
200
|
)
|
|
@@ -187,6 +204,7 @@ def plugin_dialog(
|
|
|
187
204
|
monkeypatch.setattr(npe2, 'PluginManager', PluginManagerMock())
|
|
188
205
|
|
|
189
206
|
widget = qt_plugin_dialog.QtPluginDialog()
|
|
207
|
+
# monkeypatch.setattr(widget, '_tag_outdated_plugins', lambda: None)
|
|
190
208
|
widget.show()
|
|
191
209
|
qtbot.waitUntil(widget.isVisible, timeout=300)
|
|
192
210
|
|
|
@@ -201,17 +219,15 @@ def plugin_dialog(
|
|
|
201
219
|
assert not widget._add_items_timer.isActive()
|
|
202
220
|
|
|
203
221
|
|
|
204
|
-
def test_filter_not_available_plugins(plugin_dialog):
|
|
222
|
+
def test_filter_not_available_plugins(request, plugin_dialog):
|
|
205
223
|
"""
|
|
206
224
|
Check that the plugins listed under available plugins are
|
|
207
225
|
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
226
|
"""
|
|
227
|
+
if "no-constructor" in request.node.name:
|
|
228
|
+
pytest.skip(
|
|
229
|
+
reason="This test is only relevant for constructor-based installs"
|
|
230
|
+
)
|
|
215
231
|
item = plugin_dialog.available_list.item(0)
|
|
216
232
|
widget = plugin_dialog.available_list.itemWidget(item)
|
|
217
233
|
if widget:
|
|
@@ -231,14 +247,14 @@ def test_filter_available_plugins(plugin_dialog):
|
|
|
231
247
|
"""
|
|
232
248
|
plugin_dialog.filter("")
|
|
233
249
|
assert plugin_dialog.available_list.count() == 2
|
|
234
|
-
assert plugin_dialog.available_list.
|
|
250
|
+
assert plugin_dialog.available_list.count_visible() == 2
|
|
235
251
|
|
|
236
252
|
plugin_dialog.filter("no-match@123")
|
|
237
|
-
assert plugin_dialog.available_list.
|
|
253
|
+
assert plugin_dialog.available_list.count_visible() == 0
|
|
238
254
|
|
|
239
255
|
plugin_dialog.filter("")
|
|
240
|
-
plugin_dialog.filter("
|
|
241
|
-
assert plugin_dialog.available_list.
|
|
256
|
+
plugin_dialog.filter("requests")
|
|
257
|
+
assert plugin_dialog.available_list.count_visible() == 1
|
|
242
258
|
|
|
243
259
|
|
|
244
260
|
def test_filter_installed_plugins(plugin_dialog):
|
|
@@ -247,10 +263,10 @@ def test_filter_installed_plugins(plugin_dialog):
|
|
|
247
263
|
list (the top one).
|
|
248
264
|
"""
|
|
249
265
|
plugin_dialog.filter("")
|
|
250
|
-
assert plugin_dialog.installed_list.
|
|
266
|
+
assert plugin_dialog.installed_list.count_visible() >= 0
|
|
251
267
|
|
|
252
268
|
plugin_dialog.filter("no-match@123")
|
|
253
|
-
assert plugin_dialog.installed_list.
|
|
269
|
+
assert plugin_dialog.installed_list.count_visible() == 0
|
|
254
270
|
|
|
255
271
|
|
|
256
272
|
def test_visible_widgets(request, plugin_dialog):
|
|
@@ -258,26 +274,29 @@ def test_visible_widgets(request, plugin_dialog):
|
|
|
258
274
|
Test that the direct entry button and textbox are visible
|
|
259
275
|
"""
|
|
260
276
|
if "no-constructor" not in request.node.name:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
277
|
+
pytest.skip(
|
|
278
|
+
reason="Tested functionality not available in constructor-based installs"
|
|
279
|
+
)
|
|
264
280
|
assert plugin_dialog.direct_entry_edit.isVisible()
|
|
265
281
|
assert plugin_dialog.direct_entry_btn.isVisible()
|
|
266
282
|
|
|
267
283
|
|
|
268
|
-
def test_version_dropdown(
|
|
284
|
+
def test_version_dropdown(plugin_dialog, qtbot):
|
|
269
285
|
"""
|
|
270
286
|
Test that when the source drop down is changed, it displays the other versions properly.
|
|
271
287
|
"""
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
288
|
+
# qtbot.wait(10000)
|
|
289
|
+
widget = plugin_dialog.available_list.item(0).widget
|
|
290
|
+
count = widget.version_choice_dropdown.count()
|
|
291
|
+
if count == 2:
|
|
292
|
+
assert widget.version_choice_dropdown.currentText() == "2.31.0"
|
|
293
|
+
# switch from PyPI source to conda one.
|
|
294
|
+
widget.source_choice_dropdown.setCurrentIndex(1)
|
|
295
|
+
assert widget.version_choice_dropdown.currentText() == "2.32.1"
|
|
277
296
|
|
|
278
297
|
|
|
279
298
|
def test_plugin_list_count_items(plugin_dialog):
|
|
280
|
-
assert plugin_dialog.installed_list.
|
|
299
|
+
assert plugin_dialog.installed_list.count_visible() == 2
|
|
281
300
|
|
|
282
301
|
|
|
283
302
|
def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
@@ -285,7 +304,7 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
285
304
|
with patch.object(qt_plugin_dialog.PluginListItem, "set_busy") as mock:
|
|
286
305
|
plugin_dialog.installed_list.handle_action(
|
|
287
306
|
item,
|
|
288
|
-
'test-
|
|
307
|
+
'my-test-old-plugin-1',
|
|
289
308
|
InstallerActions.UPGRADE,
|
|
290
309
|
)
|
|
291
310
|
mock.assert_called_with(
|
|
@@ -295,16 +314,17 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
295
314
|
with patch.object(qt_plugin_dialog.WarnPopup, "exec_") as mock:
|
|
296
315
|
plugin_dialog.installed_list.handle_action(
|
|
297
316
|
item,
|
|
298
|
-
'test-
|
|
317
|
+
'my-test-old-plugin-1',
|
|
299
318
|
InstallerActions.UNINSTALL,
|
|
300
319
|
)
|
|
301
320
|
assert mock.called
|
|
302
321
|
|
|
303
322
|
item = plugin_dialog.available_list.item(0)
|
|
304
323
|
with patch.object(qt_plugin_dialog.PluginListItem, "set_busy") as mock:
|
|
324
|
+
|
|
305
325
|
plugin_dialog.available_list.handle_action(
|
|
306
326
|
item,
|
|
307
|
-
'test-
|
|
327
|
+
'my-test-old-plugin-1',
|
|
308
328
|
InstallerActions.INSTALL,
|
|
309
329
|
version='3',
|
|
310
330
|
)
|
|
@@ -313,17 +333,10 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
|
313
333
|
)
|
|
314
334
|
|
|
315
335
|
plugin_dialog.available_list.handle_action(
|
|
316
|
-
item, 'test-
|
|
317
|
-
)
|
|
318
|
-
mock.assert_called_with(
|
|
319
|
-
trans._("cancelling..."), InstallerActions.CANCEL
|
|
336
|
+
item, 'my-test-old-plugin-1', InstallerActions.CANCEL, version='3'
|
|
320
337
|
)
|
|
338
|
+
mock.assert_called_with("", InstallerActions.CANCEL)
|
|
321
339
|
|
|
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
340
|
qtbot.waitUntil(lambda: not plugin_dialog.worker.is_running)
|
|
328
341
|
|
|
329
342
|
|
|
@@ -347,8 +360,12 @@ def test_on_enabled_checkbox(plugin_dialog, qtbot, plugins, old_plugins):
|
|
|
347
360
|
assert old_plugins.enabled[0] is False
|
|
348
361
|
|
|
349
362
|
|
|
350
|
-
def
|
|
351
|
-
"""
|
|
363
|
+
def test_add_items_outdated_and_update(plugin_dialog, qtbot):
|
|
364
|
+
"""
|
|
365
|
+
Test that a plugin is tagged as outdated (a newer version is available), the update button becomes visible.
|
|
366
|
+
|
|
367
|
+
Also check that after doing an update the update button gets hidden.
|
|
368
|
+
"""
|
|
352
369
|
|
|
353
370
|
# The plugin is being added to the available plugins list. When the dialog is being built
|
|
354
371
|
# this one will be listed as available, and it will be found as already installed.
|
|
@@ -360,15 +377,209 @@ def test_add_items_outdated(plugin_dialog):
|
|
|
360
377
|
True,
|
|
361
378
|
{
|
|
362
379
|
"home_page": 'www.mywebsite.com',
|
|
363
|
-
"pypi_versions": ['0.4.0'],
|
|
364
|
-
"conda_versions": ['0.4.0'],
|
|
380
|
+
"pypi_versions": ['0.4.0', '0.1.0'],
|
|
381
|
+
"conda_versions": ['0.4.0', '0.1.0'],
|
|
365
382
|
},
|
|
366
383
|
)
|
|
367
|
-
|
|
368
|
-
plugin_dialog.
|
|
369
|
-
|
|
384
|
+
plugin_dialog._plugin_data_map["my-plugin"] = new_plugin
|
|
385
|
+
plugin_dialog._plugin_queue = [new_plugin]
|
|
370
386
|
plugin_dialog._add_items()
|
|
371
387
|
item = plugin_dialog.installed_list.item(0)
|
|
372
388
|
widget = plugin_dialog.installed_list.itemWidget(item)
|
|
373
|
-
|
|
389
|
+
initial_version = "0.1.0"
|
|
390
|
+
mod_initial_version = initial_version.replace('.', '․') # noqa: RUF001
|
|
374
391
|
assert widget.update_btn.isVisible()
|
|
392
|
+
assert widget.version.text() == mod_initial_version
|
|
393
|
+
assert widget.version.toolTip() == initial_version
|
|
394
|
+
|
|
395
|
+
# Trigger process finished handler to simulated that an update was done
|
|
396
|
+
plugin_dialog._on_process_finished(
|
|
397
|
+
{
|
|
398
|
+
'exit_code': 1,
|
|
399
|
+
'exit_status': 0,
|
|
400
|
+
'action': InstallerActions.UPGRADE,
|
|
401
|
+
'pkgs': ['my-plugin==0.4.0'],
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
updated_version = "0.4.0"
|
|
405
|
+
mod_updated_version = updated_version.replace('.', '․') # noqa: RUF001
|
|
406
|
+
assert not widget.update_btn.isVisible()
|
|
407
|
+
assert widget.version.text() == mod_updated_version
|
|
408
|
+
assert widget.version.toolTip() == updated_version
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@pytest.mark.skipif(
|
|
412
|
+
qtpy.API_NAME.lower().startswith('pyside')
|
|
413
|
+
and sys.version_info[:2] > (3, 10)
|
|
414
|
+
and platform.system() == "Darwin",
|
|
415
|
+
reason='pyside specific bug',
|
|
416
|
+
)
|
|
417
|
+
def test_refresh(qtbot, plugin_dialog):
|
|
418
|
+
with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
|
|
419
|
+
plugin_dialog.refresh(clear_cache=False)
|
|
420
|
+
|
|
421
|
+
with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
|
|
422
|
+
plugin_dialog.refresh(clear_cache=True)
|
|
423
|
+
|
|
424
|
+
with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
|
|
425
|
+
plugin_dialog._refresh_and_clear_cache()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@pytest.mark.skipif(
|
|
429
|
+
qtpy.API_NAME.lower().startswith('pyside')
|
|
430
|
+
and sys.version_info[:2] > (3, 10)
|
|
431
|
+
and platform.system() == "Darwin",
|
|
432
|
+
reason='pyside specific bug',
|
|
433
|
+
)
|
|
434
|
+
def test_toggle_status(plugin_dialog):
|
|
435
|
+
plugin_dialog.toggle_status(True)
|
|
436
|
+
assert plugin_dialog.stdout_text.isVisible()
|
|
437
|
+
plugin_dialog.toggle_status(False)
|
|
438
|
+
assert not plugin_dialog.stdout_text.isVisible()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@pytest.mark.skipif(
|
|
442
|
+
qtpy.API_NAME.lower().startswith('pyside')
|
|
443
|
+
and sys.version_info[:2] > (3, 10)
|
|
444
|
+
and platform.system() == "Darwin",
|
|
445
|
+
reason='pyside specific bug',
|
|
446
|
+
)
|
|
447
|
+
def test_exec(plugin_dialog):
|
|
448
|
+
plugin_dialog.exec_()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@pytest.mark.skipif(
|
|
452
|
+
qtpy.API_NAME.lower().startswith('pyside')
|
|
453
|
+
and sys.version_info[:2] > (3, 10)
|
|
454
|
+
and platform.system() == "Darwin",
|
|
455
|
+
reason='pyside specific bug',
|
|
456
|
+
)
|
|
457
|
+
def test_search_in_available(plugin_dialog):
|
|
458
|
+
idxs = plugin_dialog._search_in_available("test")
|
|
459
|
+
assert idxs == [0, 1, 2, 3]
|
|
460
|
+
idxs = plugin_dialog._search_in_available("*&%$")
|
|
461
|
+
assert idxs == []
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def test_drop_event(plugin_dialog, tmp_path):
|
|
465
|
+
path_1 = tmp_path / "example-1.txt"
|
|
466
|
+
path_2 = tmp_path / "example-2.txt"
|
|
467
|
+
url_prefix = 'file:///' if os.name == 'nt' else 'file://'
|
|
468
|
+
data = QMimeData()
|
|
469
|
+
data.setUrls(
|
|
470
|
+
[QUrl(f'{url_prefix}{path_1}'), QUrl(f'{url_prefix}{path_2}')]
|
|
471
|
+
)
|
|
472
|
+
event = QDropEvent(
|
|
473
|
+
QPointF(5.0, 5.0), Qt.CopyAction, data, Qt.LeftButton, Qt.NoModifier
|
|
474
|
+
)
|
|
475
|
+
plugin_dialog.dropEvent(event)
|
|
476
|
+
assert plugin_dialog.direct_entry_edit.text() == str(path_1)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@pytest.mark.skipif(
|
|
480
|
+
qtpy.API_NAME.lower().startswith('pyside'), reason='pyside specific bug'
|
|
481
|
+
)
|
|
482
|
+
def test_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
483
|
+
if "[constructor]" in request.node.name:
|
|
484
|
+
pytest.skip(
|
|
485
|
+
reason="This test is only relevant for constructor-based installs"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
489
|
+
item = plugin_dialog.available_list.item(1)
|
|
490
|
+
widget = plugin_dialog.available_list.itemWidget(item)
|
|
491
|
+
with qtbot.waitSignal(
|
|
492
|
+
plugin_dialog.installer.processFinished, timeout=60_000
|
|
493
|
+
) as blocker:
|
|
494
|
+
widget.action_button.click()
|
|
495
|
+
|
|
496
|
+
process_finished_data = blocker.args[0]
|
|
497
|
+
assert process_finished_data['action'] == InstallerActions.INSTALL
|
|
498
|
+
assert process_finished_data['pkgs'][0].startswith("requests")
|
|
499
|
+
qtbot.wait(5000)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@pytest.mark.skipif(
|
|
503
|
+
qtpy.API_NAME.lower().startswith('pyside'), reason='pyside specific bug'
|
|
504
|
+
)
|
|
505
|
+
def test_cancel(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
506
|
+
if "[constructor]" in request.node.name:
|
|
507
|
+
pytest.skip(
|
|
508
|
+
reason="This test is only relevant for constructor-based installs"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
512
|
+
item = plugin_dialog.available_list.item(1)
|
|
513
|
+
widget = plugin_dialog.available_list.itemWidget(item)
|
|
514
|
+
with qtbot.waitSignal(
|
|
515
|
+
plugin_dialog.installer.processFinished, timeout=60_000
|
|
516
|
+
) as blocker:
|
|
517
|
+
widget.action_button.click()
|
|
518
|
+
widget.cancel_btn.click()
|
|
519
|
+
|
|
520
|
+
process_finished_data = blocker.args[0]
|
|
521
|
+
assert process_finished_data['action'] == InstallerActions.CANCEL
|
|
522
|
+
assert process_finished_data['pkgs'][0].startswith("requests")
|
|
523
|
+
assert plugin_dialog.available_list.count() == 2
|
|
524
|
+
assert plugin_dialog.installed_list.count() == 2
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@pytest.mark.skipif(
|
|
528
|
+
qtpy.API_NAME.lower().startswith('pyside'), reason='pyside specific bug'
|
|
529
|
+
)
|
|
530
|
+
def test_cancel_all(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
531
|
+
if "[constructor]" in request.node.name:
|
|
532
|
+
pytest.skip(
|
|
533
|
+
reason="This test is only relevant for constructor-based installs"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
537
|
+
item_1 = plugin_dialog.available_list.item(0)
|
|
538
|
+
item_2 = plugin_dialog.available_list.item(1)
|
|
539
|
+
widget_1 = plugin_dialog.available_list.itemWidget(item_1)
|
|
540
|
+
widget_2 = plugin_dialog.available_list.itemWidget(item_2)
|
|
541
|
+
with qtbot.waitSignal(plugin_dialog.installer.allFinished, timeout=60_000):
|
|
542
|
+
widget_1.action_button.click()
|
|
543
|
+
widget_2.action_button.click()
|
|
544
|
+
plugin_dialog.cancel_all_btn.click()
|
|
545
|
+
|
|
546
|
+
assert plugin_dialog.available_list.count() == 2
|
|
547
|
+
assert plugin_dialog.installed_list.count() == 2
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@pytest.mark.skipif(
|
|
551
|
+
qtpy.API_NAME.lower().startswith('pyside'), reason='pyside specific bug'
|
|
552
|
+
)
|
|
553
|
+
def test_direct_entry_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
|
|
554
|
+
if "[constructor]" in request.node.name:
|
|
555
|
+
pytest.skip(
|
|
556
|
+
reason="This test is only relevant for constructor-based installs"
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
plugin_dialog.set_prefix(str(tmp_virtualenv))
|
|
560
|
+
with qtbot.waitSignal(
|
|
561
|
+
plugin_dialog.installer.processFinished, timeout=60_000
|
|
562
|
+
) as blocker:
|
|
563
|
+
plugin_dialog.direct_entry_edit.setText('requests')
|
|
564
|
+
plugin_dialog.direct_entry_btn.click()
|
|
565
|
+
|
|
566
|
+
process_finished_data = blocker.args[0]
|
|
567
|
+
assert process_finished_data['action'] == InstallerActions.INSTALL
|
|
568
|
+
assert process_finished_data['pkgs'][0].startswith("requests")
|
|
569
|
+
qtbot.wait(5000)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def test_shortcut_close(plugin_dialog, qtbot):
|
|
573
|
+
qtbot.keyClicks(
|
|
574
|
+
plugin_dialog, 'W', modifier=Qt.KeyboardModifier.ControlModifier
|
|
575
|
+
)
|
|
576
|
+
qtbot.wait(200)
|
|
577
|
+
assert not plugin_dialog.isVisible()
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def test_shortcut_quit(plugin_dialog, qtbot):
|
|
581
|
+
qtbot.keyClicks(
|
|
582
|
+
plugin_dialog, 'Q', modifier=Qt.KeyboardModifier.ControlModifier
|
|
583
|
+
)
|
|
584
|
+
qtbot.wait(200)
|
|
585
|
+
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.1'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
@@ -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()
|