napari-plugin-manager 0.1.0__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 +63 -0
- napari_plugin_manager/_tests/test_installer_process.py +338 -0
- napari_plugin_manager/_tests/test_npe2api.py +51 -0
- napari_plugin_manager/_tests/test_qt_plugin_dialog.py +563 -0
- napari_plugin_manager/_tests/test_utils.py +27 -0
- napari_plugin_manager/_version.py +16 -0
- napari_plugin_manager/npe2api.py +131 -0
- napari_plugin_manager/qt_package_installer.py +660 -0
- napari_plugin_manager/qt_plugin_dialog.py +1495 -0
- napari_plugin_manager/qt_widgets.py +14 -0
- napari_plugin_manager/styles.qss +352 -0
- napari_plugin_manager/utils.py +22 -0
- napari_plugin_manager-0.1.0.dist-info/LICENSE +29 -0
- napari_plugin_manager-0.1.0.dist-info/METADATA +255 -0
- napari_plugin_manager-0.1.0.dist-info/RECORD +19 -0
- napari_plugin_manager-0.1.0.dist-info/WHEEL +5 -0
- napari_plugin_manager-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from qtpy.QtWidgets import QDialog, QInputDialog, QMessageBox
|
|
6
|
+
|
|
7
|
+
from napari_plugin_manager.qt_package_installer import CondaInstallerTool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(autouse=True)
|
|
11
|
+
def _block_message_box(monkeypatch, request):
|
|
12
|
+
def raise_on_call(*_, **__):
|
|
13
|
+
raise RuntimeError("exec_ call") # pragma: no cover
|
|
14
|
+
|
|
15
|
+
monkeypatch.setattr(QMessageBox, "exec_", raise_on_call)
|
|
16
|
+
monkeypatch.setattr(QMessageBox, "critical", raise_on_call)
|
|
17
|
+
monkeypatch.setattr(QMessageBox, "information", raise_on_call)
|
|
18
|
+
monkeypatch.setattr(QMessageBox, "question", raise_on_call)
|
|
19
|
+
monkeypatch.setattr(QMessageBox, "warning", raise_on_call)
|
|
20
|
+
monkeypatch.setattr(QInputDialog, "getText", raise_on_call)
|
|
21
|
+
# QDialogs can be allowed via a marker; only raise if not decorated
|
|
22
|
+
if "enabledialog" not in request.keywords:
|
|
23
|
+
monkeypatch.setattr(QDialog, "exec_", raise_on_call)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from virtualenv.run import Session
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def tmp_virtualenv(tmp_path) -> 'Session':
|
|
32
|
+
virtualenv = pytest.importorskip('virtualenv')
|
|
33
|
+
|
|
34
|
+
cmd = [str(tmp_path), '--no-setuptools', '--no-wheel', '--activators', '']
|
|
35
|
+
return virtualenv.cli_run(cmd)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def tmp_conda_env(tmp_path):
|
|
40
|
+
import subprocess
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
subprocess.check_output(
|
|
44
|
+
[
|
|
45
|
+
CondaInstallerTool.executable(),
|
|
46
|
+
'create',
|
|
47
|
+
'-yq',
|
|
48
|
+
'-p',
|
|
49
|
+
str(tmp_path),
|
|
50
|
+
'--override-channels',
|
|
51
|
+
'-c',
|
|
52
|
+
'conda-forge',
|
|
53
|
+
f'python={sys.version_info.major}.{sys.version_info.minor}',
|
|
54
|
+
],
|
|
55
|
+
stderr=subprocess.STDOUT,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=300,
|
|
58
|
+
)
|
|
59
|
+
except subprocess.CalledProcessError as exc:
|
|
60
|
+
print(exc.output)
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
return tmp_path
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import MethodType
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from qtpy.QtCore import QProcessEnvironment
|
|
9
|
+
|
|
10
|
+
from napari_plugin_manager.qt_package_installer import (
|
|
11
|
+
AbstractInstallerTool,
|
|
12
|
+
CondaInstallerTool,
|
|
13
|
+
InstallerActions,
|
|
14
|
+
InstallerQueue,
|
|
15
|
+
InstallerTools,
|
|
16
|
+
PipInstallerTool,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from virtualenv.run import Session
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _assert_exit_code_not_zero(
|
|
24
|
+
self, exit_code=None, exit_status=None, error=None
|
|
25
|
+
):
|
|
26
|
+
errors = []
|
|
27
|
+
if exit_code == 0:
|
|
28
|
+
errors.append("- 'exit_code' should have been non-zero!")
|
|
29
|
+
if error is not None:
|
|
30
|
+
errors.append("- 'error' should have been None!")
|
|
31
|
+
if errors:
|
|
32
|
+
raise AssertionError("\n".join(errors))
|
|
33
|
+
return self._on_process_done_original(exit_code, exit_status, error)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _assert_error_used(self, exit_code=None, exit_status=None, error=None):
|
|
37
|
+
errors = []
|
|
38
|
+
if error is None:
|
|
39
|
+
errors.append("- 'error' should have been populated!")
|
|
40
|
+
if exit_code is not None:
|
|
41
|
+
errors.append("- 'exit_code' should not have been populated!")
|
|
42
|
+
if errors:
|
|
43
|
+
raise AssertionError("\n".join(errors))
|
|
44
|
+
return self._on_process_done_original(exit_code, exit_status, error)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _NonExistingTool(AbstractInstallerTool):
|
|
48
|
+
def executable(self):
|
|
49
|
+
return f"this-tool-does-not-exist-{hash(time.time())}"
|
|
50
|
+
|
|
51
|
+
def arguments(self):
|
|
52
|
+
return ()
|
|
53
|
+
|
|
54
|
+
def environment(self, env=None):
|
|
55
|
+
return QProcessEnvironment.systemEnvironment()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_not_implemented_methods():
|
|
59
|
+
tool = AbstractInstallerTool('install', ['requests'])
|
|
60
|
+
with pytest.raises(NotImplementedError):
|
|
61
|
+
tool.executable()
|
|
62
|
+
|
|
63
|
+
with pytest.raises(NotImplementedError):
|
|
64
|
+
tool.arguments()
|
|
65
|
+
|
|
66
|
+
with pytest.raises(NotImplementedError):
|
|
67
|
+
tool.environment()
|
|
68
|
+
|
|
69
|
+
with pytest.raises(NotImplementedError):
|
|
70
|
+
tool.available()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_pip_installer_tasks(qtbot, tmp_virtualenv: 'Session', monkeypatch):
|
|
74
|
+
installer = InstallerQueue()
|
|
75
|
+
monkeypatch.setattr(
|
|
76
|
+
PipInstallerTool, "executable", lambda *a: tmp_virtualenv.creator.exe
|
|
77
|
+
)
|
|
78
|
+
with qtbot.waitSignal(installer.allFinished, timeout=20000):
|
|
79
|
+
installer.install(
|
|
80
|
+
tool=InstallerTools.PIP,
|
|
81
|
+
pkgs=['pip-install-test'],
|
|
82
|
+
)
|
|
83
|
+
installer.install(
|
|
84
|
+
tool=InstallerTools.PIP,
|
|
85
|
+
pkgs=['typing-extensions'],
|
|
86
|
+
)
|
|
87
|
+
job_id = installer.install(
|
|
88
|
+
tool=InstallerTools.PIP,
|
|
89
|
+
pkgs=['requests'],
|
|
90
|
+
)
|
|
91
|
+
assert isinstance(job_id, int)
|
|
92
|
+
installer.cancel(job_id)
|
|
93
|
+
|
|
94
|
+
assert not installer.hasJobs()
|
|
95
|
+
|
|
96
|
+
pkgs = 0
|
|
97
|
+
for pth in tmp_virtualenv.creator.libs:
|
|
98
|
+
if (pth / 'pip_install_test').exists():
|
|
99
|
+
pkgs += 1
|
|
100
|
+
if (pth / 'typing_extensions.py').exists():
|
|
101
|
+
pkgs += 1
|
|
102
|
+
if (pth / 'requests').exists():
|
|
103
|
+
raise AssertionError('requests got installed')
|
|
104
|
+
|
|
105
|
+
assert pkgs >= 2, 'package was not installed'
|
|
106
|
+
|
|
107
|
+
with qtbot.waitSignal(installer.allFinished, timeout=10000):
|
|
108
|
+
job_id = installer.uninstall(
|
|
109
|
+
tool=InstallerTools.PIP,
|
|
110
|
+
pkgs=['pip-install-test'],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
for pth in tmp_virtualenv.creator.libs:
|
|
114
|
+
assert not (
|
|
115
|
+
pth / 'pip_install_test'
|
|
116
|
+
).exists(), 'pip_install_test still installed'
|
|
117
|
+
assert not installer.hasJobs()
|
|
118
|
+
|
|
119
|
+
# Test new signals
|
|
120
|
+
with qtbot.waitSignal(installer.processFinished, timeout=20000) as blocker:
|
|
121
|
+
installer.install(
|
|
122
|
+
tool=InstallerTools.PIP,
|
|
123
|
+
pkgs=['pydantic'],
|
|
124
|
+
)
|
|
125
|
+
process_finished_data = blocker.args[0]
|
|
126
|
+
assert process_finished_data['action'] == InstallerActions.INSTALL
|
|
127
|
+
assert process_finished_data['pkgs'] == ["pydantic"]
|
|
128
|
+
|
|
129
|
+
# Test upgrade
|
|
130
|
+
with qtbot.waitSignal(installer.allFinished, timeout=20000):
|
|
131
|
+
installer.install(
|
|
132
|
+
tool=InstallerTools.PIP,
|
|
133
|
+
pkgs=['requests==2.30.0'],
|
|
134
|
+
)
|
|
135
|
+
installer.upgrade(
|
|
136
|
+
tool=InstallerTools.PIP,
|
|
137
|
+
pkgs=['requests'],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_installer_failures(qtbot, tmp_virtualenv: 'Session', monkeypatch):
|
|
142
|
+
installer = InstallerQueue()
|
|
143
|
+
monkeypatch.setattr(
|
|
144
|
+
PipInstallerTool, "executable", lambda *a: tmp_virtualenv.creator.exe
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# CHECK 1) Errors should trigger finished and allFinished too
|
|
148
|
+
with qtbot.waitSignal(installer.allFinished, timeout=10000):
|
|
149
|
+
installer.install(
|
|
150
|
+
tool=InstallerTools.PIP,
|
|
151
|
+
pkgs=[f'this-package-does-not-exist-{hash(time.time())}'],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Keep a reference before we monkey patch stuff
|
|
155
|
+
installer._on_process_done_original = installer._on_process_done
|
|
156
|
+
|
|
157
|
+
# CHECK 2) Non-existing packages should return non-zero
|
|
158
|
+
monkeypatch.setattr(
|
|
159
|
+
installer,
|
|
160
|
+
"_on_process_done",
|
|
161
|
+
MethodType(_assert_exit_code_not_zero, installer),
|
|
162
|
+
)
|
|
163
|
+
with qtbot.waitSignal(installer.allFinished, timeout=10000):
|
|
164
|
+
installer.install(
|
|
165
|
+
tool=InstallerTools.PIP,
|
|
166
|
+
pkgs=[f'this-package-does-not-exist-{hash(time.time())}'],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# CHECK 3) Non-existing tools should fail to start
|
|
170
|
+
monkeypatch.setattr(
|
|
171
|
+
installer,
|
|
172
|
+
"_on_process_done",
|
|
173
|
+
MethodType(_assert_error_used, installer),
|
|
174
|
+
)
|
|
175
|
+
monkeypatch.setattr(installer, "_get_tool", lambda *a: _NonExistingTool)
|
|
176
|
+
with qtbot.waitSignal(installer.allFinished, timeout=10000):
|
|
177
|
+
installer.install(
|
|
178
|
+
tool=_NonExistingTool,
|
|
179
|
+
pkgs=[f'this-package-does-not-exist-{hash(time.time())}'],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_cancel_incorrect_job_id(qtbot, tmp_virtualenv: 'Session'):
|
|
184
|
+
installer = InstallerQueue()
|
|
185
|
+
with qtbot.waitSignal(installer.allFinished, timeout=20000):
|
|
186
|
+
job_id = installer.install(
|
|
187
|
+
tool=InstallerTools.PIP,
|
|
188
|
+
pkgs=['requests'],
|
|
189
|
+
)
|
|
190
|
+
with pytest.raises(ValueError):
|
|
191
|
+
installer.cancel(job_id + 1)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@pytest.mark.skipif(
|
|
195
|
+
not CondaInstallerTool.available(), reason="Conda is not available."
|
|
196
|
+
)
|
|
197
|
+
def test_conda_installer(qtbot, tmp_conda_env: Path):
|
|
198
|
+
conda_meta = tmp_conda_env / "conda-meta"
|
|
199
|
+
glob_pat = "typing-extensions-*.json"
|
|
200
|
+
glob_pat_2 = "pyzenhub-*.json"
|
|
201
|
+
installer = InstallerQueue()
|
|
202
|
+
|
|
203
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
204
|
+
installer.install(
|
|
205
|
+
tool=InstallerTools.CONDA,
|
|
206
|
+
pkgs=['typing-extensions'],
|
|
207
|
+
prefix=tmp_conda_env,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert not installer.hasJobs()
|
|
211
|
+
assert list(conda_meta.glob(glob_pat))
|
|
212
|
+
|
|
213
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
214
|
+
installer.uninstall(
|
|
215
|
+
tool=InstallerTools.CONDA,
|
|
216
|
+
pkgs=['typing-extensions'],
|
|
217
|
+
prefix=tmp_conda_env,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
assert not installer.hasJobs()
|
|
221
|
+
assert not list(conda_meta.glob(glob_pat))
|
|
222
|
+
|
|
223
|
+
# Check canceling all works
|
|
224
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
225
|
+
installer.install(
|
|
226
|
+
tool=InstallerTools.CONDA,
|
|
227
|
+
pkgs=['typing-extensions'],
|
|
228
|
+
prefix=tmp_conda_env,
|
|
229
|
+
)
|
|
230
|
+
installer.install(
|
|
231
|
+
tool=InstallerTools.CONDA,
|
|
232
|
+
pkgs=['pyzenhub'],
|
|
233
|
+
prefix=tmp_conda_env,
|
|
234
|
+
)
|
|
235
|
+
assert installer.currentJobs() == 2
|
|
236
|
+
installer.cancel_all()
|
|
237
|
+
|
|
238
|
+
assert not installer.hasJobs()
|
|
239
|
+
assert not list(conda_meta.glob(glob_pat))
|
|
240
|
+
assert not list(conda_meta.glob(glob_pat_2))
|
|
241
|
+
|
|
242
|
+
# Check canceling current job works (1st in queue)
|
|
243
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
244
|
+
job_id_1 = installer.install(
|
|
245
|
+
tool=InstallerTools.CONDA,
|
|
246
|
+
pkgs=['typing-extensions'],
|
|
247
|
+
prefix=tmp_conda_env,
|
|
248
|
+
)
|
|
249
|
+
job_id_2 = installer.install(
|
|
250
|
+
tool=InstallerTools.CONDA,
|
|
251
|
+
pkgs=['pyzenhub'],
|
|
252
|
+
prefix=tmp_conda_env,
|
|
253
|
+
)
|
|
254
|
+
assert installer.currentJobs() == 2
|
|
255
|
+
installer.cancel(job_id_1)
|
|
256
|
+
assert installer.currentJobs() == 1
|
|
257
|
+
|
|
258
|
+
assert not installer.hasJobs()
|
|
259
|
+
|
|
260
|
+
# Check canceling queued job works (somewhere besides 1st position in queue)
|
|
261
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
262
|
+
job_id_1 = installer.install(
|
|
263
|
+
tool=InstallerTools.CONDA,
|
|
264
|
+
pkgs=['typing-extensions'],
|
|
265
|
+
prefix=tmp_conda_env,
|
|
266
|
+
)
|
|
267
|
+
job_id_2 = installer.install(
|
|
268
|
+
tool=InstallerTools.CONDA,
|
|
269
|
+
pkgs=['pyzenhub'],
|
|
270
|
+
prefix=tmp_conda_env,
|
|
271
|
+
)
|
|
272
|
+
assert installer.currentJobs() == 2
|
|
273
|
+
installer.cancel(job_id_2)
|
|
274
|
+
assert installer.currentJobs() == 1
|
|
275
|
+
|
|
276
|
+
assert not installer.hasJobs()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_installer_error(qtbot, tmp_virtualenv: 'Session', monkeypatch):
|
|
280
|
+
installer = InstallerQueue()
|
|
281
|
+
monkeypatch.setattr(
|
|
282
|
+
PipInstallerTool, "executable", lambda *a: 'not-a-real-executable'
|
|
283
|
+
)
|
|
284
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
285
|
+
installer.install(
|
|
286
|
+
tool=InstallerTools.PIP,
|
|
287
|
+
pkgs=['some-package-that-does-not-exist'],
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@pytest.mark.skipif(
|
|
292
|
+
not CondaInstallerTool.available(), reason="Conda is not available."
|
|
293
|
+
)
|
|
294
|
+
def test_conda_installer_wait_for_finished(qtbot, tmp_conda_env: Path):
|
|
295
|
+
installer = InstallerQueue()
|
|
296
|
+
|
|
297
|
+
with qtbot.waitSignal(installer.allFinished, timeout=600_000):
|
|
298
|
+
installer.install(
|
|
299
|
+
tool=InstallerTools.CONDA,
|
|
300
|
+
pkgs=['requests'],
|
|
301
|
+
prefix=tmp_conda_env,
|
|
302
|
+
)
|
|
303
|
+
installer.install(
|
|
304
|
+
tool=InstallerTools.CONDA,
|
|
305
|
+
pkgs=['pyzenhub'],
|
|
306
|
+
prefix=tmp_conda_env,
|
|
307
|
+
)
|
|
308
|
+
installer.waitForFinished(20000)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_constraints_are_in_sync():
|
|
312
|
+
conda_constraints = sorted(CondaInstallerTool.constraints())
|
|
313
|
+
pip_constraints = sorted(PipInstallerTool.constraints())
|
|
314
|
+
|
|
315
|
+
assert len(conda_constraints) == len(pip_constraints)
|
|
316
|
+
|
|
317
|
+
name_re = re.compile(r"([a-z0-9_\-]+).*")
|
|
318
|
+
for conda_constraint, pip_constraint in zip(
|
|
319
|
+
conda_constraints, pip_constraints
|
|
320
|
+
):
|
|
321
|
+
conda_name = name_re.match(conda_constraint).group(1)
|
|
322
|
+
pip_name = name_re.match(pip_constraint).group(1)
|
|
323
|
+
assert conda_name == pip_name
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_executables():
|
|
327
|
+
assert CondaInstallerTool.executable()
|
|
328
|
+
assert PipInstallerTool.executable()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_available():
|
|
332
|
+
assert str(CondaInstallerTool.available())
|
|
333
|
+
assert PipInstallerTool.available()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_unrecognized_tool():
|
|
337
|
+
with pytest.raises(ValueError):
|
|
338
|
+
InstallerQueue().install(tool='shrug', pkgs=[])
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from napari_plugin_manager.npe2api import (
|
|
2
|
+
_user_agent,
|
|
3
|
+
cache_clear,
|
|
4
|
+
conda_map,
|
|
5
|
+
iter_napari_plugin_info,
|
|
6
|
+
plugin_summaries,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_user_agent():
|
|
11
|
+
assert _user_agent()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_plugin_summaries():
|
|
15
|
+
keys = [
|
|
16
|
+
"name",
|
|
17
|
+
"version",
|
|
18
|
+
"display_name",
|
|
19
|
+
"summary",
|
|
20
|
+
"author",
|
|
21
|
+
"license",
|
|
22
|
+
"home_page",
|
|
23
|
+
"pypi_versions",
|
|
24
|
+
"conda_versions",
|
|
25
|
+
]
|
|
26
|
+
data = plugin_summaries()
|
|
27
|
+
test_data = dict(data[0])
|
|
28
|
+
for key in keys:
|
|
29
|
+
assert key in test_data
|
|
30
|
+
test_data.pop(key)
|
|
31
|
+
|
|
32
|
+
assert not test_data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_conda_map():
|
|
36
|
+
pkgs = ["napari-svg"]
|
|
37
|
+
data = conda_map()
|
|
38
|
+
for pkg in pkgs:
|
|
39
|
+
assert pkg in data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_iter_napari_plugin_info():
|
|
43
|
+
data = iter_napari_plugin_info()
|
|
44
|
+
for item in data:
|
|
45
|
+
assert item
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_clear_cache():
|
|
49
|
+
assert _user_agent.cache_info().hits >= 1
|
|
50
|
+
cache_clear()
|
|
51
|
+
assert _user_agent.cache_info().hits == 0
|