napari-plugin-manager 0.1.3__py3-none-any.whl → 0.1.5rc0__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.
@@ -1,8 +1,8 @@
1
1
  import importlib.metadata
2
2
  import os
3
3
  import sys
4
- from typing import Generator, Optional, Tuple
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,8 +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 (
16
+ QApplication,
17
+ QMessageBox,
18
+ )
15
19
 
16
20
  if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] > (3, 10):
17
21
  pytest.skip(
@@ -20,8 +24,11 @@ if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] > (3, 10):
20
24
  allow_module_level=True,
21
25
  )
22
26
 
23
- from napari_plugin_manager import qt_plugin_dialog
24
- from napari_plugin_manager.qt_package_installer import InstallerActions
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
+ )
25
32
 
26
33
  N_MOCKED_PLUGINS = 2
27
34
 
@@ -29,7 +36,7 @@ N_MOCKED_PLUGINS = 2
29
36
  def _iter_napari_pypi_plugin_info(
30
37
  conda_forge: bool = True,
31
38
  ) -> Generator[
32
- Tuple[Optional[npe2.PackageMetadata], bool], None, None
39
+ tuple[npe2.PackageMetadata | None, bool], None, None
33
40
  ]: # pragma: no cover (this function is used in thread and codecov has a problem with the collection of coverage in such cases)
34
41
  """Mock the pypi method to collect available plugins.
35
42
 
@@ -86,29 +93,6 @@ def plugins(qtbot):
86
93
  return PluginsMock()
87
94
 
88
95
 
89
- class WarnPopupMock:
90
- def __init__(self, text):
91
- self._is_visible = False
92
-
93
- def show(self):
94
- self._is_visible = True
95
-
96
- def exec_(self):
97
- self._is_visible = True
98
-
99
- def move(self, pos):
100
- return False
101
-
102
- def isVisible(self):
103
- return self._is_visible
104
-
105
- def close(self):
106
- self._is_visible = False
107
-
108
- def width(self):
109
- return 100
110
-
111
-
112
96
  @pytest.fixture(params=[True, False], ids=["constructor", "no-constructor"])
113
97
  def plugin_dialog(
114
98
  request,
@@ -119,6 +103,10 @@ def plugin_dialog(
119
103
  old_plugins,
120
104
  ):
121
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
122
110
 
123
111
  class PluginManagerMock:
124
112
  def instance(self):
@@ -137,7 +125,7 @@ def plugin_dialog(
137
125
  def is_disabled(self, name):
138
126
  return False
139
127
 
140
- def discover(self):
128
+ def discover(self, include_npe1=False):
141
129
  return ['plugin']
142
130
 
143
131
  def enable(self, plugin):
@@ -181,17 +169,12 @@ def plugin_dialog(
181
169
  "iter_napari_plugin_info",
182
170
  _iter_napari_pypi_plugin_info,
183
171
  )
184
- monkeypatch.setattr(qt_plugin_dialog, 'WarnPopup', WarnPopupMock)
185
172
 
186
173
  # This is patching `napari.utils.misc.running_as_constructor_app` function
187
174
  # to mock a normal napari install.
188
175
  monkeypatch.setattr(
189
176
  qt_plugin_dialog, "running_as_constructor_app", lambda: request.param
190
177
  )
191
- monkeypatch.setattr(
192
- qt_plugin_dialog, "IS_NAPARI_CONDA_INSTALLED", request.param
193
- )
194
- monkeypatch.setattr(qt_plugin_dialog, "ON_BUNDLE", request.param)
195
178
  monkeypatch.setattr(
196
179
  napari.plugins, 'plugin_manager', OldPluginManagerMock()
197
180
  )
@@ -201,22 +184,24 @@ def plugin_dialog(
201
184
  monkeypatch.setattr(npe2, 'PluginManager', PluginManagerMock())
202
185
 
203
186
  widget = qt_plugin_dialog.QtPluginDialog()
187
+ monkeypatch.setattr(
188
+ widget, '_is_main_app_conda_package', lambda: request.param
189
+ )
204
190
  # monkeypatch.setattr(widget, '_tag_outdated_plugins', lambda: None)
205
191
  widget.show()
206
192
  qtbot.waitUntil(widget.isVisible, timeout=300)
207
193
 
208
- def available_list_populated():
209
- return widget.available_list.count() == N_MOCKED_PLUGINS
210
-
211
- qtbot.waitUntil(available_list_populated, timeout=3000)
194
+ assert widget.available_list.count_visible() == 0
195
+ assert widget.available_list.count() == 0
212
196
  qtbot.add_widget(widget)
213
197
  yield widget
214
198
  widget.hide()
215
199
  widget._add_items_timer.stop()
216
200
  assert not widget._add_items_timer.isActive()
201
+ get_settings().plugins.use_npe2_adaptor = original_setting
217
202
 
218
203
 
219
- def test_filter_not_available_plugins(request, plugin_dialog):
204
+ def test_filter_not_available_plugins(request, plugin_dialog, qtbot):
220
205
  """
221
206
  Check that the plugins listed under available plugins are
222
207
  enabled and disabled accordingly.
@@ -225,6 +210,8 @@ def test_filter_not_available_plugins(request, plugin_dialog):
225
210
  pytest.skip(
226
211
  reason="This test is only relevant for constructor-based installs"
227
212
  )
213
+ plugin_dialog.search("e")
214
+ qtbot.wait(500)
228
215
  item = plugin_dialog.available_list.item(0)
229
216
  widget = plugin_dialog.available_list.itemWidget(item)
230
217
  if widget:
@@ -237,32 +224,37 @@ def test_filter_not_available_plugins(request, plugin_dialog):
237
224
  assert not widget.warning_tooltip.isVisible()
238
225
 
239
226
 
240
- def test_filter_available_plugins(plugin_dialog):
227
+ def test_filter_available_plugins(plugin_dialog, qtbot):
241
228
  """
242
229
  Test the dialog is correctly filtering plugins in the available plugins
243
230
  list (the bottom one).
244
231
  """
245
- plugin_dialog.filter("")
246
- assert plugin_dialog.available_list.count() == 2
247
- assert plugin_dialog.available_list.count_visible() == 2
232
+ plugin_dialog.search("")
233
+ qtbot.wait(500)
234
+ assert plugin_dialog.available_list.count() == 0
235
+ assert plugin_dialog.available_list.count_visible() == 0
248
236
 
249
- plugin_dialog.filter("no-match@123")
237
+ plugin_dialog.search("no-match@123")
238
+ qtbot.wait(500)
250
239
  assert plugin_dialog.available_list.count_visible() == 0
251
240
 
252
- plugin_dialog.filter("")
253
- plugin_dialog.filter("requests")
241
+ plugin_dialog.search("")
242
+ plugin_dialog.search("requests")
243
+ qtbot.wait(500)
254
244
  assert plugin_dialog.available_list.count_visible() == 1
255
245
 
256
246
 
257
- def test_filter_installed_plugins(plugin_dialog):
247
+ def test_filter_installed_plugins(plugin_dialog, qtbot):
258
248
  """
259
249
  Test the dialog is correctly filtering plugins in the installed plugins
260
250
  list (the top one).
261
251
  """
262
- plugin_dialog.filter("")
263
- assert plugin_dialog.installed_list.count_visible() >= 0
252
+ plugin_dialog.search("")
253
+ qtbot.wait(500)
254
+ assert plugin_dialog.installed_list.count_visible() == 2
264
255
 
265
- plugin_dialog.filter("no-match@123")
256
+ plugin_dialog.search("no-match@123")
257
+ qtbot.wait(500)
266
258
  assert plugin_dialog.installed_list.count_visible() == 0
267
259
 
268
260
 
@@ -270,7 +262,7 @@ def test_visible_widgets(request, plugin_dialog):
270
262
  """
271
263
  Test that the direct entry button and textbox are visible
272
264
  """
273
- if "no-constructor" not in request.node.name:
265
+ if "constructor" in request.node.name:
274
266
  pytest.skip(
275
267
  reason="Tested functionality not available in constructor-based installs"
276
268
  )
@@ -282,7 +274,8 @@ def test_version_dropdown(plugin_dialog, qtbot):
282
274
  """
283
275
  Test that when the source drop down is changed, it displays the other versions properly.
284
276
  """
285
- # qtbot.wait(10000)
277
+ plugin_dialog.search("requests")
278
+ qtbot.wait(500)
286
279
  widget = plugin_dialog.available_list.item(0).widget
287
280
  count = widget.version_choice_dropdown.count()
288
281
  if count == 2:
@@ -308,33 +301,56 @@ def test_plugin_list_handle_action(plugin_dialog, qtbot):
308
301
  trans._("updating..."), InstallerActions.UPGRADE
309
302
  )
310
303
 
311
- with patch.object(qt_plugin_dialog.WarnPopup, "exec_") as mock:
312
- plugin_dialog.installed_list.handle_action(
313
- item,
314
- 'my-test-old-plugin-1',
315
- InstallerActions.UNINSTALL,
316
- )
317
- assert mock.called
318
-
304
+ plugin_dialog.search("requests")
305
+ qtbot.wait(500)
319
306
  item = plugin_dialog.available_list.item(0)
320
- with patch.object(qt_plugin_dialog.PluginListItem, "set_busy") as mock:
307
+ if item is not None:
308
+ with patch.object(qt_plugin_dialog.PluginListItem, "set_busy") as mock:
309
+
310
+ plugin_dialog.available_list.handle_action(
311
+ item,
312
+ 'my-test-old-plugin-1',
313
+ InstallerActions.INSTALL,
314
+ version='3',
315
+ )
316
+ mock.assert_called_once_with(
317
+ trans._("installing..."), InstallerActions.INSTALL
318
+ )
319
+
320
+ plugin_dialog.available_list.handle_action(
321
+ item,
322
+ 'my-test-old-plugin-1',
323
+ InstallerActions.CANCEL,
324
+ version='3',
325
+ )
326
+ assert mock.call_count >= 2
327
+ assert mock.call_args_list[1] == call(
328
+ "cancelling...", InstallerActions.CANCEL
329
+ )
321
330
 
322
- plugin_dialog.available_list.handle_action(
323
- item,
324
- 'my-test-old-plugin-1',
325
- InstallerActions.INSTALL,
326
- version='3',
327
- )
328
- mock.assert_called_with(
329
- trans._("installing..."), InstallerActions.INSTALL
330
- )
331
+ qtbot.waitUntil(lambda: not plugin_dialog.worker.is_running)
331
332
 
332
- plugin_dialog.available_list.handle_action(
333
- item, 'my-test-old-plugin-1', InstallerActions.CANCEL, version='3'
334
- )
335
- mock.assert_called_with("", InstallerActions.CANCEL)
336
333
 
337
- qtbot.waitUntil(lambda: not plugin_dialog.worker.is_running)
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()
338
354
 
339
355
 
340
356
  def test_on_enabled_checkbox(plugin_dialog, qtbot, plugins, old_plugins):
@@ -406,13 +422,13 @@ def test_add_items_outdated_and_update(plugin_dialog, qtbot):
406
422
 
407
423
 
408
424
  def test_refresh(qtbot, plugin_dialog):
409
- with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
425
+ with qtbot.waitSignal(plugin_dialog.finished, timeout=500):
410
426
  plugin_dialog.refresh(clear_cache=False)
411
427
 
412
- with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
428
+ with qtbot.waitSignal(plugin_dialog.finished, timeout=500):
413
429
  plugin_dialog.refresh(clear_cache=True)
414
430
 
415
- with qtbot.waitSignal(plugin_dialog._add_items_timer.timeout, timeout=500):
431
+ with qtbot.waitSignal(plugin_dialog.finished, timeout=500):
416
432
  plugin_dialog._refresh_and_clear_cache()
417
433
 
418
434
 
@@ -429,7 +445,9 @@ def test_exec(plugin_dialog):
429
445
 
430
446
  def test_search_in_available(plugin_dialog):
431
447
  idxs = plugin_dialog._search_in_available("test")
432
- assert idxs == [0, 1, 2, 3]
448
+ if idxs:
449
+ assert idxs == [0, 1, 2, 3]
450
+
433
451
  idxs = plugin_dialog._search_in_available("*&%$")
434
452
  assert idxs == []
435
453
 
@@ -452,11 +470,13 @@ def test_drop_event(plugin_dialog, tmp_path):
452
470
  def test_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
453
471
  if "[constructor]" in request.node.name:
454
472
  pytest.skip(
455
- reason="This test is only relevant for constructor-based installs"
473
+ reason="This test is only relevant for non-constructor-based installs"
456
474
  )
457
475
 
458
476
  plugin_dialog.set_prefix(str(tmp_virtualenv))
459
- item = plugin_dialog.available_list.item(1)
477
+ plugin_dialog.search('requests')
478
+ qtbot.wait(500)
479
+ item = plugin_dialog.available_list.item(0)
460
480
  widget = plugin_dialog.available_list.itemWidget(item)
461
481
  with qtbot.waitSignal(
462
482
  plugin_dialog.installer.processFinished, timeout=60_000
@@ -469,14 +489,57 @@ def test_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
469
489
  qtbot.wait(5000)
470
490
 
471
491
 
492
+ @pytest.mark.parametrize(
493
+ "message_return",
494
+ [QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Ok],
495
+ )
496
+ def test_install_pypi_constructor(
497
+ qtbot, tmp_virtualenv, plugin_dialog, request, message_return, monkeypatch
498
+ ):
499
+ if "no-constructor" in request.node.name:
500
+ pytest.skip(
501
+ reason="This test is to test pip in constructor-based installs"
502
+ )
503
+ # ensure pip is the installer tool, so that the warning will trigger
504
+ monkeypatch.setattr(
505
+ qt_plugin_dialog.PluginListItem,
506
+ 'get_installer_tool',
507
+ lambda self: InstallerTools.PIP,
508
+ )
509
+ monkeypatch.setattr(
510
+ qt_plugin_dialog.PluginListItem,
511
+ 'get_installer_source',
512
+ lambda self: "PIP",
513
+ )
514
+
515
+ plugin_dialog.set_prefix(str(tmp_virtualenv))
516
+ plugin_dialog.search('requests')
517
+ qtbot.wait(500)
518
+ item = plugin_dialog.available_list.item(0)
519
+ widget = plugin_dialog.available_list.itemWidget(item)
520
+ with patch.object(qt_plugin_dialog.QMessageBox, "exec_") as mock:
521
+ mock.return_value = message_return
522
+ if message_return == QMessageBox.StandardButton.Ok:
523
+ with qtbot.waitSignal(
524
+ plugin_dialog.installer.processFinished, timeout=60_000
525
+ ):
526
+ widget.action_button.click()
527
+ qtbot.wait(5000)
528
+ else:
529
+ widget.action_button.click()
530
+ assert mock.called
531
+
532
+
472
533
  def test_cancel(qtbot, tmp_virtualenv, plugin_dialog, request):
473
534
  if "[constructor]" in request.node.name:
474
535
  pytest.skip(
475
- reason="This test is only relevant for constructor-based installs"
536
+ reason="This test is only relevant for non-constructor-based installs"
476
537
  )
477
538
 
478
539
  plugin_dialog.set_prefix(str(tmp_virtualenv))
479
- item = plugin_dialog.available_list.item(1)
540
+ plugin_dialog.search('requests')
541
+ qtbot.wait(500)
542
+ item = plugin_dialog.available_list.item(0)
480
543
  widget = plugin_dialog.available_list.itemWidget(item)
481
544
  with qtbot.waitSignal(
482
545
  plugin_dialog.installer.processFinished, timeout=60_000
@@ -487,19 +550,23 @@ def test_cancel(qtbot, tmp_virtualenv, plugin_dialog, request):
487
550
  process_finished_data = blocker.args[0]
488
551
  assert process_finished_data['action'] == InstallerActions.CANCEL
489
552
  assert process_finished_data['pkgs'][0].startswith("requests")
490
- assert plugin_dialog.available_list.count() == 2
553
+ assert plugin_dialog.available_list.count() == 1
491
554
  assert plugin_dialog.installed_list.count() == 2
492
555
 
493
556
 
494
557
  def test_cancel_all(qtbot, tmp_virtualenv, plugin_dialog, request):
495
558
  if "[constructor]" in request.node.name:
496
559
  pytest.skip(
497
- reason="This test is only relevant for constructor-based installs"
560
+ reason="This test is only relevant for non-constructor-based installs"
498
561
  )
499
562
 
500
563
  plugin_dialog.set_prefix(str(tmp_virtualenv))
564
+ plugin_dialog.search('requests')
565
+ qtbot.wait(500)
501
566
  item_1 = plugin_dialog.available_list.item(0)
502
- item_2 = plugin_dialog.available_list.item(1)
567
+ plugin_dialog.search('pyzenhub')
568
+ qtbot.wait(500)
569
+ item_2 = plugin_dialog.available_list.item(0)
503
570
  widget_1 = plugin_dialog.available_list.itemWidget(item_1)
504
571
  widget_2 = plugin_dialog.available_list.itemWidget(item_2)
505
572
  with qtbot.waitSignal(plugin_dialog.installer.allFinished, timeout=60_000):
@@ -507,6 +574,9 @@ def test_cancel_all(qtbot, tmp_virtualenv, plugin_dialog, request):
507
574
  widget_2.action_button.click()
508
575
  plugin_dialog.cancel_all_btn.click()
509
576
 
577
+ plugin_dialog.search('')
578
+ qtbot.wait(500)
579
+
510
580
  assert plugin_dialog.available_list.count() == 2
511
581
  assert plugin_dialog.installed_list.count() == 2
512
582
 
@@ -514,7 +584,7 @@ def test_cancel_all(qtbot, tmp_virtualenv, plugin_dialog, request):
514
584
  def test_direct_entry_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
515
585
  if "[constructor]" in request.node.name:
516
586
  pytest.skip(
517
- reason="This test is only relevant for constructor-based installs"
587
+ reason="The tested functionality is not available in constructor-based installs"
518
588
  )
519
589
 
520
590
  plugin_dialog.set_prefix(str(tmp_virtualenv))
@@ -530,17 +600,66 @@ def test_direct_entry_installs(qtbot, tmp_virtualenv, plugin_dialog, request):
530
600
  qtbot.wait(5000)
531
601
 
532
602
 
603
+ @pytest.mark.skipif(
604
+ sys.platform.startswith('linux'), reason="Test fails on linux randomly"
605
+ )
533
606
  def test_shortcut_close(plugin_dialog, qtbot):
534
607
  qtbot.keyClicks(
535
608
  plugin_dialog, 'W', modifier=Qt.KeyboardModifier.ControlModifier
536
609
  )
537
- qtbot.wait(200)
610
+ qtbot.wait(500)
538
611
  assert not plugin_dialog.isVisible()
539
612
 
540
613
 
614
+ @pytest.mark.skipif(
615
+ sys.platform.startswith('linux'), reason="Test fails on linux randomly"
616
+ )
541
617
  def test_shortcut_quit(plugin_dialog, qtbot):
542
618
  qtbot.keyClicks(
543
619
  plugin_dialog, 'Q', modifier=Qt.KeyboardModifier.ControlModifier
544
620
  )
545
- qtbot.wait(200)
621
+ qtbot.wait(500)
546
622
  assert not plugin_dialog.isVisible()
623
+
624
+
625
+ @pytest.mark.skipif(
626
+ not sys.platform.startswith('linux'), reason="Test works only on linux"
627
+ )
628
+ def test_export_plugins_button(plugin_dialog):
629
+ def _timer():
630
+ dialog = QApplication.activeModalWidget()
631
+ dialog.reject()
632
+
633
+ timer = QTimer()
634
+ timer.setSingleShot(True)
635
+ timer.timeout.connect(_timer)
636
+ timer.start(4_000)
637
+ plugin_dialog.export_button.click()
638
+
639
+
640
+ def test_export_plugins(plugin_dialog, tmp_path):
641
+ plugins_file = 'plugins.txt'
642
+ plugin_dialog.export_plugins(str(tmp_path / plugins_file))
643
+ assert (tmp_path / plugins_file).exists()
644
+
645
+
646
+ @pytest.mark.skipif(
647
+ not sys.platform.startswith('linux'), reason="Test works only on linux"
648
+ )
649
+ def test_import_plugins_button(plugin_dialog):
650
+ def _timer():
651
+ dialog = QApplication.activeModalWidget()
652
+ dialog.reject()
653
+
654
+ timer = QTimer()
655
+ timer.setSingleShot(True)
656
+ timer.timeout.connect(_timer)
657
+ timer.start(4_000)
658
+ plugin_dialog.import_button.click()
659
+
660
+
661
+ def test_import_plugins(plugin_dialog, tmp_path, qtbot):
662
+ path = tmp_path / 'plugins.txt'
663
+ path.write_text('requests\npyzenhub\n')
664
+ with qtbot.waitSignal(plugin_dialog.installer.allFinished, timeout=60_000):
665
+ plugin_dialog.import_plugins(str(path))
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
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, Union
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.3'
16
- __version_tuple__ = version_tuple = (0, 1, 3)
20
+ __version__ = version = '0.1.5rc0'
21
+ __version_tuple__ = version_tuple = (0, 1, 5)