napari-plugin-manager 0.1.0a0__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/__init__.py +0 -0
- napari_plugin_manager/_tests/__init__.py +0 -0
- napari_plugin_manager/_tests/conftest.py +18 -0
- napari_plugin_manager/_tests/test_installer_process.py +226 -0
- napari_plugin_manager/_tests/test_qt_plugin_dialog.py +374 -0
- napari_plugin_manager/_version.py +4 -0
- napari_plugin_manager/qt_package_installer.py +570 -0
- napari_plugin_manager/qt_plugin_dialog.py +1086 -0
- napari_plugin_manager-0.1.0a0.dist-info/LICENSE +29 -0
- napari_plugin_manager-0.1.0a0.dist-info/METADATA +108 -0
- napari_plugin_manager-0.1.0a0.dist-info/RECORD +13 -0
- napari_plugin_manager-0.1.0a0.dist-info/WHEEL +5 -0
- napari_plugin_manager-0.1.0a0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from qtpy.QtWidgets import QDialog, QInputDialog, QMessageBox
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.fixture(autouse=True)
|
|
6
|
+
def _block_message_box(monkeypatch, request):
|
|
7
|
+
def raise_on_call(*_, **__):
|
|
8
|
+
raise RuntimeError("exec_ call") # pragma: no cover
|
|
9
|
+
|
|
10
|
+
monkeypatch.setattr(QMessageBox, "exec_", raise_on_call)
|
|
11
|
+
monkeypatch.setattr(QMessageBox, "critical", raise_on_call)
|
|
12
|
+
monkeypatch.setattr(QMessageBox, "information", raise_on_call)
|
|
13
|
+
monkeypatch.setattr(QMessageBox, "question", raise_on_call)
|
|
14
|
+
monkeypatch.setattr(QMessageBox, "warning", raise_on_call)
|
|
15
|
+
monkeypatch.setattr(QInputDialog, "getText", raise_on_call)
|
|
16
|
+
# QDialogs can be allowed via a marker; only raise if not decorated
|
|
17
|
+
if "enabledialog" not in request.keywords:
|
|
18
|
+
monkeypatch.setattr(QDialog, "exec_", raise_on_call)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import MethodType
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from qtpy.QtCore import QProcessEnvironment
|
|
10
|
+
|
|
11
|
+
from napari_plugin_manager.qt_package_installer import (
|
|
12
|
+
AbstractInstallerTool,
|
|
13
|
+
CondaInstallerTool,
|
|
14
|
+
InstallerQueue,
|
|
15
|
+
InstallerTools,
|
|
16
|
+
PipInstallerTool,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from virtualenv.run import Session
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def tmp_virtualenv(tmp_path) -> 'Session':
|
|
25
|
+
virtualenv = pytest.importorskip('virtualenv')
|
|
26
|
+
|
|
27
|
+
cmd = [str(tmp_path), '--no-setuptools', '--no-wheel', '--activators', '']
|
|
28
|
+
return virtualenv.cli_run(cmd)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def tmp_conda_env(tmp_path):
|
|
33
|
+
import subprocess
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
subprocess.check_output(
|
|
37
|
+
[
|
|
38
|
+
CondaInstallerTool.executable(),
|
|
39
|
+
'create',
|
|
40
|
+
'-yq',
|
|
41
|
+
'-p',
|
|
42
|
+
str(tmp_path),
|
|
43
|
+
'--override-channels',
|
|
44
|
+
'-c',
|
|
45
|
+
'conda-forge',
|
|
46
|
+
f'python={sys.version_info.major}.{sys.version_info.minor}',
|
|
47
|
+
],
|
|
48
|
+
stderr=subprocess.STDOUT,
|
|
49
|
+
text=True,
|
|
50
|
+
timeout=300,
|
|
51
|
+
)
|
|
52
|
+
except subprocess.CalledProcessError as exc:
|
|
53
|
+
print(exc.output)
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
return tmp_path
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_pip_installer_tasks(qtbot, tmp_virtualenv: 'Session', monkeypatch):
|
|
60
|
+
installer = InstallerQueue()
|
|
61
|
+
monkeypatch.setattr(
|
|
62
|
+
PipInstallerTool, "executable", lambda *a: tmp_virtualenv.creator.exe
|
|
63
|
+
)
|
|
64
|
+
with qtbot.waitSignal(installer.allFinished, timeout=20000):
|
|
65
|
+
installer.install(
|
|
66
|
+
tool=InstallerTools.PIP,
|
|
67
|
+
pkgs=['pip-install-test'],
|
|
68
|
+
)
|
|
69
|
+
installer.install(
|
|
70
|
+
tool=InstallerTools.PIP,
|
|
71
|
+
pkgs=['typing-extensions'],
|
|
72
|
+
)
|
|
73
|
+
job_id = installer.install(
|
|
74
|
+
tool=InstallerTools.PIP,
|
|
75
|
+
pkgs=['requests'],
|
|
76
|
+
)
|
|
77
|
+
assert isinstance(job_id, int)
|
|
78
|
+
installer.cancel(job_id)
|
|
79
|
+
|
|
80
|
+
assert not installer.hasJobs()
|
|
81
|
+
|
|
82
|
+
pkgs = 0
|
|
83
|
+
for pth in tmp_virtualenv.creator.libs:
|
|
84
|
+
if (pth / 'pip_install_test').exists():
|
|
85
|
+
pkgs += 1
|
|
86
|
+
if (pth / 'typing_extensions.py').exists():
|
|
87
|
+
pkgs += 1
|
|
88
|
+
if (pth / 'requests').exists():
|
|
89
|
+
raise AssertionError('requests got installed')
|
|
90
|
+
|
|
91
|
+
assert pkgs >= 2, 'package was not installed'
|
|
92
|
+
|
|
93
|
+
with qtbot.waitSignal(installer.allFinished, timeout=10000):
|
|
94
|
+
job_id = installer.uninstall(
|
|
95
|
+
tool=InstallerTools.PIP,
|
|
96
|
+
pkgs=['pip-install-test'],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
for pth in tmp_virtualenv.creator.libs:
|
|
100
|
+
assert not (
|
|
101
|
+
pth / 'pip_install_test'
|
|
102
|
+
).exists(), 'pip_install_test still installed'
|
|
103
|
+
|
|
104
|
+
assert not installer.hasJobs()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _assert_exit_code_not_zero(
|
|
108
|
+
self, exit_code=None, exit_status=None, error=None
|
|
109
|
+
):
|
|
110
|
+
errors = []
|
|
111
|
+
if exit_code == 0:
|
|
112
|
+
errors.append("- 'exit_code' should have been non-zero!")
|
|
113
|
+
if error is not None:
|
|
114
|
+
errors.append("- 'error' should have been None!")
|
|
115
|
+
if errors:
|
|
116
|
+
raise AssertionError("\n".join(errors))
|
|
117
|
+
return self._on_process_done_original(exit_code, exit_status, error)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _NonExistingTool(AbstractInstallerTool):
|
|
121
|
+
def executable(self):
|
|
122
|
+
return f"this-tool-does-not-exist-{hash(time.time())}"
|
|
123
|
+
|
|
124
|
+
def arguments(self):
|
|
125
|
+
return ()
|
|
126
|
+
|
|
127
|
+
def environment(self, env=None):
|
|
128
|
+
return QProcessEnvironment.systemEnvironment()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _assert_error_used(self, exit_code=None, exit_status=None, error=None):
|
|
132
|
+
errors = []
|
|
133
|
+
if error is None:
|
|
134
|
+
errors.append("- 'error' should have been populated!")
|
|
135
|
+
if exit_code is not None:
|
|
136
|
+
errors.append("- 'exit_code' should not have been populated!")
|
|
137
|
+
if errors:
|
|
138
|
+
raise AssertionError("\n".join(errors))
|
|
139
|
+
return self._on_process_done_original(exit_code, exit_status, error)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_installer_failures(qtbot, tmp_virtualenv: 'Session', monkeypatch):
|
|
143
|
+
installer = InstallerQueue()
|
|
144
|
+
monkeypatch.setattr(
|
|
145
|
+
PipInstallerTool, "executable", lambda *a: tmp_virtualenv.creator.exe
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# CHECK 1) Errors should trigger finished and allFinished too
|
|
149
|
+
with qtbot.waitSignal(installer.allFinished, timeout=10000):
|
|
150
|
+
installer.install(
|
|
151
|
+
tool=InstallerTools.PIP,
|
|
152
|
+
pkgs=[f'this-package-does-not-exist-{hash(time.time())}'],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Keep a reference before we monkey patch stuff
|
|
156
|
+
installer._on_process_done_original = installer._on_process_done
|
|
157
|
+
|
|
158
|
+
# CHECK 2) Non-existing packages should return non-zero
|
|
159
|
+
monkeypatch.setattr(
|
|
160
|
+
installer,
|
|
161
|
+
"_on_process_done",
|
|
162
|
+
MethodType(_assert_exit_code_not_zero, installer),
|
|
163
|
+
)
|
|
164
|
+
with qtbot.waitSignal(installer.allFinished, timeout=10000):
|
|
165
|
+
installer.install(
|
|
166
|
+
tool=InstallerTools.PIP,
|
|
167
|
+
pkgs=[f'this-package-does-not-exist-{hash(time.time())}'],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# CHECK 3) Non-existing tools should fail to start
|
|
171
|
+
monkeypatch.setattr(
|
|
172
|
+
installer,
|
|
173
|
+
"_on_process_done",
|
|
174
|
+
MethodType(_assert_error_used, installer),
|
|
175
|
+
)
|
|
176
|
+
monkeypatch.setattr(installer, "_get_tool", lambda *a: _NonExistingTool)
|
|
177
|
+
with qtbot.waitSignal(installer.allFinished, timeout=10000):
|
|
178
|
+
installer.install(
|
|
179
|
+
tool=_NonExistingTool,
|
|
180
|
+
pkgs=[f'this-package-does-not-exist-{hash(time.time())}'],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.mark.skipif(
|
|
185
|
+
not CondaInstallerTool.available(), reason="Conda is not available."
|
|
186
|
+
)
|
|
187
|
+
def test_conda_installer(qtbot, tmp_conda_env: Path):
|
|
188
|
+
installer = InstallerQueue()
|
|
189
|
+
|
|
190
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
191
|
+
installer.install(
|
|
192
|
+
tool=InstallerTools.CONDA,
|
|
193
|
+
pkgs=['typing-extensions'],
|
|
194
|
+
prefix=tmp_conda_env,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
conda_meta = tmp_conda_env / "conda-meta"
|
|
198
|
+
glob_pat = "typing-extensions-*.json"
|
|
199
|
+
|
|
200
|
+
assert not installer.hasJobs()
|
|
201
|
+
assert list(conda_meta.glob(glob_pat))
|
|
202
|
+
|
|
203
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
204
|
+
installer.uninstall(
|
|
205
|
+
tool=InstallerTools.CONDA,
|
|
206
|
+
pkgs=['typing-extensions'],
|
|
207
|
+
prefix=tmp_conda_env,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert not installer.hasJobs()
|
|
211
|
+
assert not list(conda_meta.glob(glob_pat))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_constraints_are_in_sync():
|
|
215
|
+
conda_constraints = sorted(CondaInstallerTool.constraints())
|
|
216
|
+
pip_constraints = sorted(PipInstallerTool.constraints())
|
|
217
|
+
|
|
218
|
+
assert len(conda_constraints) == len(pip_constraints)
|
|
219
|
+
|
|
220
|
+
name_re = re.compile(r"([a-z0-9_\-]+).*")
|
|
221
|
+
for conda_constraint, pip_constraint in zip(
|
|
222
|
+
conda_constraints, pip_constraints
|
|
223
|
+
):
|
|
224
|
+
conda_name = name_re.match(conda_constraint).group(1)
|
|
225
|
+
pip_name = name_re.match(pip_constraint).group(1)
|
|
226
|
+
assert conda_name == pip_name
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Generator, Optional, Tuple
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import napari.plugins
|
|
7
|
+
import npe2
|
|
8
|
+
import pytest
|
|
9
|
+
import qtpy
|
|
10
|
+
from napari.plugins._tests.test_npe2 import mock_pm # noqa
|
|
11
|
+
from napari.utils.translations import trans
|
|
12
|
+
|
|
13
|
+
from napari_plugin_manager import qt_plugin_dialog
|
|
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
|
+
pytest.skip(
|
|
18
|
+
"Known PySide2 x Python 3.11 incompatibility: "
|
|
19
|
+
"TypeError: 'PySide2.QtCore.Qt.Alignment' object cannot be interpreted as an integer",
|
|
20
|
+
allow_module_level=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
N_MOCKED_PLUGINS = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _iter_napari_pypi_plugin_info(
|
|
27
|
+
conda_forge: bool = True,
|
|
28
|
+
) -> Generator[
|
|
29
|
+
Tuple[Optional[npe2.PackageMetadata], bool], None, None
|
|
30
|
+
]: # pragma: no cover (this function is used in thread and codecov has a problem with the collection of coverage in such cases)
|
|
31
|
+
"""Mock the pypi method to collect available plugins.
|
|
32
|
+
|
|
33
|
+
This will mock napari.plugins.pypi.iter_napari_plugin_info` for pypi.
|
|
34
|
+
|
|
35
|
+
It will return two fake plugins that will populate the available plugins
|
|
36
|
+
list (the bottom one). The first plugin will not be available on
|
|
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").
|
|
39
|
+
"""
|
|
40
|
+
# This mock `base_data`` will be the same for both fake plugins.
|
|
41
|
+
base_data = {
|
|
42
|
+
"metadata_version": "1.0",
|
|
43
|
+
"version": "0.1.0",
|
|
44
|
+
"summary": "some test package",
|
|
45
|
+
"home_page": "http://napari.org",
|
|
46
|
+
"author": "test author",
|
|
47
|
+
"license": "UNKNOWN",
|
|
48
|
+
}
|
|
49
|
+
for i in range(N_MOCKED_PLUGINS):
|
|
50
|
+
yield npe2.PackageMetadata(name=f"test-name-{i}", **base_data), bool(
|
|
51
|
+
i
|
|
52
|
+
), {
|
|
53
|
+
"home_page": 'www.mywebsite.com',
|
|
54
|
+
"pypi_versions": ['3'],
|
|
55
|
+
"conda_versions": ['4.5'],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PluginsMock:
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.plugins = {
|
|
62
|
+
'test-name-0': True,
|
|
63
|
+
'test-name-1': True,
|
|
64
|
+
'my-plugin': True,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class OldPluginsMock:
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.plugins = [('test-1', False, 'test-1')]
|
|
71
|
+
self.enabled = [True]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.fixture
|
|
75
|
+
def old_plugins(qtbot):
|
|
76
|
+
return OldPluginsMock()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.fixture
|
|
80
|
+
def plugins(qtbot):
|
|
81
|
+
return PluginsMock()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class WarnPopupMock:
|
|
85
|
+
def __init__(self, text):
|
|
86
|
+
self._is_visible = False
|
|
87
|
+
|
|
88
|
+
def exec_(self):
|
|
89
|
+
self._is_visible = True
|
|
90
|
+
|
|
91
|
+
def move(self, pos):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def isVisible(self):
|
|
95
|
+
return self._is_visible
|
|
96
|
+
|
|
97
|
+
def close(self):
|
|
98
|
+
self._is_visible = False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.fixture(params=[True, False], ids=["constructor", "no-constructor"])
|
|
102
|
+
def plugin_dialog(
|
|
103
|
+
request,
|
|
104
|
+
qtbot,
|
|
105
|
+
monkeypatch,
|
|
106
|
+
mock_pm, # noqa
|
|
107
|
+
plugins,
|
|
108
|
+
old_plugins,
|
|
109
|
+
):
|
|
110
|
+
"""Fixture that provides a plugin dialog for a normal napari install."""
|
|
111
|
+
|
|
112
|
+
class PluginManagerMock:
|
|
113
|
+
def instance(self):
|
|
114
|
+
return PluginManagerInstanceMock(plugins)
|
|
115
|
+
|
|
116
|
+
class PluginManagerInstanceMock:
|
|
117
|
+
def __init__(self, plugins):
|
|
118
|
+
self.plugins = plugins.plugins
|
|
119
|
+
|
|
120
|
+
def __iter__(self):
|
|
121
|
+
yield from self.plugins
|
|
122
|
+
|
|
123
|
+
def iter_manifests(self):
|
|
124
|
+
yield from [mock_pm.get_manifest('my-plugin')]
|
|
125
|
+
|
|
126
|
+
def is_disabled(self, name):
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def discover(self):
|
|
130
|
+
return ['plugin']
|
|
131
|
+
|
|
132
|
+
def enable(self, plugin):
|
|
133
|
+
self.plugins[plugin] = True
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
def disable(self, plugin):
|
|
137
|
+
self.plugins[plugin] = False
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
def mock_metadata(name):
|
|
141
|
+
meta = {
|
|
142
|
+
'version': '0.1.0',
|
|
143
|
+
'summary': '',
|
|
144
|
+
'Home-page': '',
|
|
145
|
+
'author': '',
|
|
146
|
+
'license': '',
|
|
147
|
+
}
|
|
148
|
+
return meta
|
|
149
|
+
|
|
150
|
+
class OldPluginManagerMock:
|
|
151
|
+
def __init__(self):
|
|
152
|
+
self.plugins = old_plugins.plugins
|
|
153
|
+
self.enabled = old_plugins.enabled
|
|
154
|
+
|
|
155
|
+
def iter_available(self):
|
|
156
|
+
return self.plugins
|
|
157
|
+
|
|
158
|
+
def discover(self):
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def is_blocked(self, plugin):
|
|
162
|
+
return self.plugins[0][1]
|
|
163
|
+
|
|
164
|
+
def set_blocked(self, plugin, blocked):
|
|
165
|
+
self.enabled[0] = not blocked
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
monkeypatch.setattr(
|
|
169
|
+
qt_plugin_dialog,
|
|
170
|
+
"iter_napari_plugin_info",
|
|
171
|
+
_iter_napari_pypi_plugin_info,
|
|
172
|
+
)
|
|
173
|
+
monkeypatch.setattr(qt_plugin_dialog, 'WarnPopup', WarnPopupMock)
|
|
174
|
+
|
|
175
|
+
# This is patching `napari.utils.misc.running_as_constructor_app` function
|
|
176
|
+
# to mock a normal napari install.
|
|
177
|
+
monkeypatch.setattr(
|
|
178
|
+
qt_plugin_dialog, "running_as_constructor_app", lambda: request.param
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
monkeypatch.setattr(
|
|
182
|
+
napari.plugins, 'plugin_manager', OldPluginManagerMock()
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
monkeypatch.setattr(importlib.metadata, 'metadata', mock_metadata)
|
|
186
|
+
|
|
187
|
+
monkeypatch.setattr(npe2, 'PluginManager', PluginManagerMock())
|
|
188
|
+
|
|
189
|
+
widget = qt_plugin_dialog.QtPluginDialog()
|
|
190
|
+
widget.show()
|
|
191
|
+
qtbot.waitUntil(widget.isVisible, timeout=300)
|
|
192
|
+
|
|
193
|
+
def available_list_populated():
|
|
194
|
+
return widget.available_list.count() == N_MOCKED_PLUGINS
|
|
195
|
+
|
|
196
|
+
qtbot.waitUntil(available_list_populated, timeout=3000)
|
|
197
|
+
qtbot.add_widget(widget)
|
|
198
|
+
yield widget
|
|
199
|
+
widget.hide()
|
|
200
|
+
widget._add_items_timer.stop()
|
|
201
|
+
assert not widget._add_items_timer.isActive()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_filter_not_available_plugins(plugin_dialog):
|
|
205
|
+
"""
|
|
206
|
+
Check that the plugins listed under available plugins are
|
|
207
|
+
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
|
+
"""
|
|
215
|
+
item = plugin_dialog.available_list.item(0)
|
|
216
|
+
widget = plugin_dialog.available_list.itemWidget(item)
|
|
217
|
+
if widget:
|
|
218
|
+
assert not widget.action_button.isEnabled()
|
|
219
|
+
assert widget.warning_tooltip.isVisible()
|
|
220
|
+
|
|
221
|
+
item = plugin_dialog.available_list.item(1)
|
|
222
|
+
widget = plugin_dialog.available_list.itemWidget(item)
|
|
223
|
+
assert widget.action_button.isEnabled()
|
|
224
|
+
assert not widget.warning_tooltip.isVisible()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_filter_available_plugins(plugin_dialog):
|
|
228
|
+
"""
|
|
229
|
+
Test the dialog is correctly filtering plugins in the available plugins
|
|
230
|
+
list (the bottom one).
|
|
231
|
+
"""
|
|
232
|
+
plugin_dialog.filter("")
|
|
233
|
+
assert plugin_dialog.available_list.count() == 2
|
|
234
|
+
assert plugin_dialog.available_list._count_visible() == 2
|
|
235
|
+
|
|
236
|
+
plugin_dialog.filter("no-match@123")
|
|
237
|
+
assert plugin_dialog.available_list._count_visible() == 0
|
|
238
|
+
|
|
239
|
+
plugin_dialog.filter("")
|
|
240
|
+
plugin_dialog.filter("test-name-0")
|
|
241
|
+
assert plugin_dialog.available_list._count_visible() == 1
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_filter_installed_plugins(plugin_dialog):
|
|
245
|
+
"""
|
|
246
|
+
Test the dialog is correctly filtering plugins in the installed plugins
|
|
247
|
+
list (the top one).
|
|
248
|
+
"""
|
|
249
|
+
plugin_dialog.filter("")
|
|
250
|
+
assert plugin_dialog.installed_list._count_visible() >= 0
|
|
251
|
+
|
|
252
|
+
plugin_dialog.filter("no-match@123")
|
|
253
|
+
assert plugin_dialog.installed_list._count_visible() == 0
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_visible_widgets(request, plugin_dialog):
|
|
257
|
+
"""
|
|
258
|
+
Test that the direct entry button and textbox are visible
|
|
259
|
+
"""
|
|
260
|
+
if "no-constructor" not in request.node.name:
|
|
261
|
+
# the plugin_dialog fixture has this id
|
|
262
|
+
# skip for 'constructor' variant
|
|
263
|
+
pytest.skip()
|
|
264
|
+
assert plugin_dialog.direct_entry_edit.isVisible()
|
|
265
|
+
assert plugin_dialog.direct_entry_btn.isVisible()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_version_dropdown(qtbot, plugin_dialog):
|
|
269
|
+
"""
|
|
270
|
+
Test that when the source drop down is changed, it displays the other versions properly.
|
|
271
|
+
"""
|
|
272
|
+
widget = plugin_dialog.available_list.item(1).widget
|
|
273
|
+
assert widget.version_choice_dropdown.currentText() == "3"
|
|
274
|
+
# switch from PyPI source to conda one.
|
|
275
|
+
widget.source_choice_dropdown.setCurrentIndex(1)
|
|
276
|
+
assert widget.version_choice_dropdown.currentText() == "4.5"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_plugin_list_count_items(plugin_dialog):
|
|
280
|
+
assert plugin_dialog.installed_list._count_visible() == 2
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_plugin_list_handle_action(plugin_dialog, qtbot):
|
|
284
|
+
item = plugin_dialog.installed_list.item(0)
|
|
285
|
+
with patch.object(qt_plugin_dialog.PluginListItem, "set_busy") as mock:
|
|
286
|
+
plugin_dialog.installed_list.handle_action(
|
|
287
|
+
item,
|
|
288
|
+
'test-name-1',
|
|
289
|
+
InstallerActions.UPGRADE,
|
|
290
|
+
)
|
|
291
|
+
mock.assert_called_with(
|
|
292
|
+
trans._("updating..."), InstallerActions.UPGRADE
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
with patch.object(qt_plugin_dialog.WarnPopup, "exec_") as mock:
|
|
296
|
+
plugin_dialog.installed_list.handle_action(
|
|
297
|
+
item,
|
|
298
|
+
'test-name-1',
|
|
299
|
+
InstallerActions.UNINSTALL,
|
|
300
|
+
)
|
|
301
|
+
assert mock.called
|
|
302
|
+
|
|
303
|
+
item = plugin_dialog.available_list.item(0)
|
|
304
|
+
with patch.object(qt_plugin_dialog.PluginListItem, "set_busy") as mock:
|
|
305
|
+
plugin_dialog.available_list.handle_action(
|
|
306
|
+
item,
|
|
307
|
+
'test-name-1',
|
|
308
|
+
InstallerActions.INSTALL,
|
|
309
|
+
version='3',
|
|
310
|
+
)
|
|
311
|
+
mock.assert_called_with(
|
|
312
|
+
trans._("installing..."), InstallerActions.INSTALL
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
plugin_dialog.available_list.handle_action(
|
|
316
|
+
item, 'test-name-1', InstallerActions.CANCEL, version='3'
|
|
317
|
+
)
|
|
318
|
+
mock.assert_called_with(
|
|
319
|
+
trans._("cancelling..."), InstallerActions.CANCEL
|
|
320
|
+
)
|
|
321
|
+
|
|
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
|
+
qtbot.waitUntil(lambda: not plugin_dialog.worker.is_running)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_on_enabled_checkbox(plugin_dialog, qtbot, plugins, old_plugins):
|
|
331
|
+
# checks npe2 lines
|
|
332
|
+
item = plugin_dialog.installed_list.item(0)
|
|
333
|
+
widget = plugin_dialog.installed_list.itemWidget(item)
|
|
334
|
+
|
|
335
|
+
assert plugins.plugins['my-plugin'] is True
|
|
336
|
+
with qtbot.waitSignal(widget.enabled_checkbox.stateChanged, timeout=500):
|
|
337
|
+
widget.enabled_checkbox.setChecked(False)
|
|
338
|
+
assert plugins.plugins['my-plugin'] is False
|
|
339
|
+
|
|
340
|
+
# checks npe1 lines
|
|
341
|
+
item = plugin_dialog.installed_list.item(1)
|
|
342
|
+
widget = plugin_dialog.installed_list.itemWidget(item)
|
|
343
|
+
|
|
344
|
+
assert old_plugins.enabled[0] is True
|
|
345
|
+
with qtbot.waitSignal(widget.enabled_checkbox.stateChanged, timeout=500):
|
|
346
|
+
widget.enabled_checkbox.setChecked(False)
|
|
347
|
+
assert old_plugins.enabled[0] is False
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_add_items_outdated(plugin_dialog):
|
|
351
|
+
"""Test that a plugin is tagged as outdated (a newer version is available), the update button becomes visible."""
|
|
352
|
+
|
|
353
|
+
# The plugin is being added to the available plugins list. When the dialog is being built
|
|
354
|
+
# this one will be listed as available, and it will be found as already installed.
|
|
355
|
+
# Then, it will check if the installed version is a lower version than the one available.
|
|
356
|
+
# In this case, my-plugin is installed with version 0.1.0, so the one we are trying to install
|
|
357
|
+
# is newer, so the update button should pop up.
|
|
358
|
+
new_plugin = (
|
|
359
|
+
npe2.PackageMetadata(name="my-plugin", version="0.4.0"),
|
|
360
|
+
True,
|
|
361
|
+
{
|
|
362
|
+
"home_page": 'www.mywebsite.com',
|
|
363
|
+
"pypi_versions": ['0.4.0'],
|
|
364
|
+
"conda_versions": ['0.4.0'],
|
|
365
|
+
},
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
plugin_dialog._plugin_data = [new_plugin]
|
|
369
|
+
|
|
370
|
+
plugin_dialog._add_items()
|
|
371
|
+
item = plugin_dialog.installed_list.item(0)
|
|
372
|
+
widget = plugin_dialog.installed_list.itemWidget(item)
|
|
373
|
+
|
|
374
|
+
assert widget.update_btn.isVisible()
|