napari-plugin-manager 0.1.5rc1__py3-none-any.whl → 0.1.7__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 +10 -10
- napari_plugin_manager/_tests/test_installer_process.py +123 -82
- napari_plugin_manager/_tests/test_npe2api.py +12 -10
- napari_plugin_manager/_tests/test_qt_plugin_dialog.py +91 -93
- napari_plugin_manager/_tests/test_utils.py +39 -8
- napari_plugin_manager/_version.py +16 -3
- napari_plugin_manager/base_qt_package_installer.py +141 -65
- napari_plugin_manager/base_qt_plugin_dialog.py +180 -160
- napari_plugin_manager/npe2api.py +11 -6
- napari_plugin_manager/qt_package_installer.py +44 -16
- napari_plugin_manager/qt_plugin_dialog.py +40 -30
- napari_plugin_manager/qt_warning_dialog.py +5 -3
- napari_plugin_manager/qt_widgets.py +3 -3
- napari_plugin_manager/styles.qss +30 -13
- napari_plugin_manager/utils.py +57 -2
- {napari_plugin_manager-0.1.5rc1.dist-info → napari_plugin_manager-0.1.7.dist-info}/METADATA +9 -35
- napari_plugin_manager-0.1.7.dist-info/RECORD +23 -0
- {napari_plugin_manager-0.1.5rc1.dist-info → napari_plugin_manager-0.1.7.dist-info}/WHEEL +1 -1
- napari_plugin_manager-0.1.5rc1.dist-info/RECORD +0 -23
- {napari_plugin_manager-0.1.5rc1.dist-info → napari_plugin_manager-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {napari_plugin_manager-0.1.5rc1.dist-info → napari_plugin_manager-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -11,17 +11,16 @@ and `cancel`.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import contextlib
|
|
14
|
-
import logging
|
|
15
14
|
import os
|
|
16
15
|
import sys
|
|
17
16
|
from collections import deque
|
|
18
|
-
from collections.abc import Sequence
|
|
17
|
+
from collections.abc import Iterable, Sequence
|
|
19
18
|
from dataclasses import dataclass
|
|
20
19
|
from enum import auto
|
|
21
20
|
from functools import lru_cache
|
|
22
21
|
from logging import getLogger
|
|
23
22
|
from pathlib import Path
|
|
24
|
-
from subprocess import
|
|
23
|
+
from subprocess import run
|
|
25
24
|
from tempfile import gettempdir
|
|
26
25
|
from typing import TypedDict
|
|
27
26
|
|
|
@@ -62,7 +61,7 @@ class InstallerTools(StringEnum):
|
|
|
62
61
|
"Installer tools selectable by InstallerQueue jobs"
|
|
63
62
|
|
|
64
63
|
CONDA = auto()
|
|
65
|
-
|
|
64
|
+
PYPI = auto()
|
|
66
65
|
|
|
67
66
|
|
|
68
67
|
@dataclass(frozen=True)
|
|
@@ -76,19 +75,19 @@ class AbstractInstallerTool:
|
|
|
76
75
|
process: QProcess = None
|
|
77
76
|
|
|
78
77
|
@property
|
|
79
|
-
def ident(self):
|
|
78
|
+
def ident(self) -> JobId:
|
|
80
79
|
return hash(
|
|
81
80
|
(self.action, *self.pkgs, *self.origins, self.prefix, self.process)
|
|
82
81
|
)
|
|
83
82
|
|
|
84
83
|
# abstract method
|
|
85
84
|
@classmethod
|
|
86
|
-
def executable(cls):
|
|
85
|
+
def executable(cls) -> str:
|
|
87
86
|
"Path to the executable that will run the task"
|
|
88
87
|
raise NotImplementedError
|
|
89
88
|
|
|
90
89
|
# abstract method
|
|
91
|
-
def arguments(self):
|
|
90
|
+
def arguments(self) -> list[str]:
|
|
92
91
|
"Arguments supplied to the executable"
|
|
93
92
|
raise NotImplementedError
|
|
94
93
|
|
|
@@ -100,7 +99,7 @@ class AbstractInstallerTool:
|
|
|
100
99
|
raise NotImplementedError
|
|
101
100
|
|
|
102
101
|
@staticmethod
|
|
103
|
-
def constraints() ->
|
|
102
|
+
def constraints() -> list[str]:
|
|
104
103
|
"""
|
|
105
104
|
Version constraints to limit unwanted changes in installation.
|
|
106
105
|
"""
|
|
@@ -121,11 +120,14 @@ class PipInstallerTool(AbstractInstallerTool):
|
|
|
121
120
|
"""
|
|
122
121
|
|
|
123
122
|
@classmethod
|
|
124
|
-
def available(cls):
|
|
123
|
+
def available(cls) -> bool:
|
|
125
124
|
"""Check if pip is available."""
|
|
126
|
-
|
|
125
|
+
process = run(
|
|
126
|
+
[cls.executable(), '-m', 'pip', '--version'], capture_output=True
|
|
127
|
+
)
|
|
128
|
+
return process.returncode == 0
|
|
127
129
|
|
|
128
|
-
def arguments(self) ->
|
|
130
|
+
def arguments(self) -> list[str]:
|
|
129
131
|
"""Compose arguments for the pip command."""
|
|
130
132
|
args = ['-m', 'pip']
|
|
131
133
|
|
|
@@ -150,20 +152,20 @@ class PipInstallerTool(AbstractInstallerTool):
|
|
|
150
152
|
else:
|
|
151
153
|
raise ValueError(f"Action '{self.action}' not supported!")
|
|
152
154
|
|
|
153
|
-
if log.getEffectiveLevel() < 30: # DEBUG and
|
|
155
|
+
if log.getEffectiveLevel() < 30: # DEBUG and INFO level
|
|
154
156
|
args.append('-vvv')
|
|
155
157
|
|
|
156
158
|
if self.prefix is not None:
|
|
157
159
|
args.extend(['--prefix', str(self.prefix)])
|
|
158
160
|
|
|
159
|
-
return
|
|
161
|
+
return [*args, *self.pkgs]
|
|
160
162
|
|
|
161
163
|
def environment(
|
|
162
164
|
self, env: QProcessEnvironment = None
|
|
163
165
|
) -> QProcessEnvironment:
|
|
164
166
|
if env is None:
|
|
165
167
|
env = QProcessEnvironment.systemEnvironment()
|
|
166
|
-
env.insert(
|
|
168
|
+
env.insert('PIP_USER_AGENT_USER_DATA', _user_agent())
|
|
167
169
|
return env
|
|
168
170
|
|
|
169
171
|
@classmethod
|
|
@@ -172,6 +174,79 @@ class PipInstallerTool(AbstractInstallerTool):
|
|
|
172
174
|
raise NotImplementedError
|
|
173
175
|
|
|
174
176
|
|
|
177
|
+
class UvInstallerTool(AbstractInstallerTool):
|
|
178
|
+
"""Uv installer tool for the plugin manager.
|
|
179
|
+
|
|
180
|
+
This class is used to install and uninstall packages using uv.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def executable(cls) -> str:
|
|
185
|
+
"Path to the executable that will run the task"
|
|
186
|
+
if sys.platform == 'win32':
|
|
187
|
+
path = os.path.join(sys.prefix, 'Scripts', 'uv.exe')
|
|
188
|
+
else:
|
|
189
|
+
path = os.path.join(sys.prefix, 'bin', 'uv')
|
|
190
|
+
if os.path.isfile(path):
|
|
191
|
+
return path
|
|
192
|
+
return 'uv'
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def available(cls) -> bool:
|
|
196
|
+
"""Check if uv is available."""
|
|
197
|
+
try:
|
|
198
|
+
process = run([cls.executable(), '--version'], capture_output=True)
|
|
199
|
+
except FileNotFoundError: # pragma: no cover
|
|
200
|
+
return False
|
|
201
|
+
else:
|
|
202
|
+
return process.returncode == 0
|
|
203
|
+
|
|
204
|
+
def arguments(self) -> list[str]:
|
|
205
|
+
"""Compose arguments for the uv pip command."""
|
|
206
|
+
args = ['pip']
|
|
207
|
+
|
|
208
|
+
if self.action == InstallerActions.INSTALL:
|
|
209
|
+
args += ['install', '-c', self._constraints_file()]
|
|
210
|
+
for origin in self.origins:
|
|
211
|
+
args += ['--extra-index-url', origin]
|
|
212
|
+
|
|
213
|
+
elif self.action == InstallerActions.UPGRADE:
|
|
214
|
+
args += ['install', '-c', self._constraints_file()]
|
|
215
|
+
for origin in self.origins:
|
|
216
|
+
args += ['--extra-index-url', origin]
|
|
217
|
+
for pkg in self.pkgs:
|
|
218
|
+
args.append(f'--upgrade-package={pkg}')
|
|
219
|
+
elif self.action == InstallerActions.UNINSTALL:
|
|
220
|
+
args += ['uninstall']
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
raise ValueError(f"Action '{self.action}' not supported!")
|
|
224
|
+
|
|
225
|
+
if log.getEffectiveLevel() < 30: # DEBUG and INFO level
|
|
226
|
+
args.append('-vvv')
|
|
227
|
+
|
|
228
|
+
if self.prefix is not None:
|
|
229
|
+
args.extend(['--prefix', str(self.prefix)])
|
|
230
|
+
args.extend(['--python', self._python_executable()])
|
|
231
|
+
|
|
232
|
+
return [*args, *self.pkgs]
|
|
233
|
+
|
|
234
|
+
def environment(
|
|
235
|
+
self, env: QProcessEnvironment = None
|
|
236
|
+
) -> QProcessEnvironment:
|
|
237
|
+
if env is None:
|
|
238
|
+
env = QProcessEnvironment.systemEnvironment()
|
|
239
|
+
return env
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
@lru_cache(maxsize=0)
|
|
243
|
+
def _constraints_file(cls) -> str:
|
|
244
|
+
raise NotImplementedError
|
|
245
|
+
|
|
246
|
+
def _python_executable(self) -> str:
|
|
247
|
+
raise NotImplementedError
|
|
248
|
+
|
|
249
|
+
|
|
175
250
|
class CondaInstallerTool(AbstractInstallerTool):
|
|
176
251
|
"""Conda installer tool for the plugin manager.
|
|
177
252
|
|
|
@@ -179,12 +254,12 @@ class CondaInstallerTool(AbstractInstallerTool):
|
|
|
179
254
|
"""
|
|
180
255
|
|
|
181
256
|
@classmethod
|
|
182
|
-
def executable(cls):
|
|
257
|
+
def executable(cls) -> str:
|
|
183
258
|
"""Find a path to the executable.
|
|
184
259
|
|
|
185
260
|
This method assumes that if no environment variable is set that conda is available in the PATH.
|
|
186
261
|
"""
|
|
187
|
-
bat =
|
|
262
|
+
bat = '.bat' if os.name == 'nt' else ''
|
|
188
263
|
for path in (
|
|
189
264
|
Path(os.environ.get('MAMBA_EXE', '')),
|
|
190
265
|
Path(os.environ.get('CONDA_EXE', '')),
|
|
@@ -198,15 +273,16 @@ class CondaInstallerTool(AbstractInstallerTool):
|
|
|
198
273
|
return f'conda{bat}'
|
|
199
274
|
|
|
200
275
|
@classmethod
|
|
201
|
-
def available(cls):
|
|
276
|
+
def available(cls) -> bool:
|
|
202
277
|
"""Check if the executable is available by checking if it can output its version."""
|
|
203
|
-
executable = cls.executable()
|
|
204
278
|
try:
|
|
205
|
-
|
|
279
|
+
process = run([cls.executable(), '--version'], capture_output=True)
|
|
206
280
|
except FileNotFoundError: # pragma: no cover
|
|
207
281
|
return False
|
|
282
|
+
else:
|
|
283
|
+
return process.returncode == 0
|
|
208
284
|
|
|
209
|
-
def arguments(self) ->
|
|
285
|
+
def arguments(self) -> list[str]:
|
|
210
286
|
"""Compose arguments for the conda command."""
|
|
211
287
|
prefix = self.prefix or self._default_prefix()
|
|
212
288
|
|
|
@@ -217,9 +293,9 @@ class CondaInstallerTool(AbstractInstallerTool):
|
|
|
217
293
|
|
|
218
294
|
args.append('--override-channels')
|
|
219
295
|
for channel in (*self.origins, *self._default_channels()):
|
|
220
|
-
args.extend([
|
|
296
|
+
args.extend(['-c', channel])
|
|
221
297
|
|
|
222
|
-
return
|
|
298
|
+
return [*args, *self.pkgs]
|
|
223
299
|
|
|
224
300
|
def environment(
|
|
225
301
|
self, env: QProcessEnvironment = None
|
|
@@ -229,18 +305,18 @@ class CondaInstallerTool(AbstractInstallerTool):
|
|
|
229
305
|
self._add_constraints_to_env(env)
|
|
230
306
|
if 10 <= log.getEffectiveLevel() < 30: # DEBUG level
|
|
231
307
|
env.insert('CONDA_VERBOSITY', '3')
|
|
232
|
-
if os.name ==
|
|
233
|
-
if not env.contains(
|
|
308
|
+
if os.name == 'nt':
|
|
309
|
+
if not env.contains('TEMP'):
|
|
234
310
|
temp = gettempdir()
|
|
235
|
-
env.insert(
|
|
236
|
-
env.insert(
|
|
237
|
-
if not env.contains(
|
|
238
|
-
env.insert(
|
|
239
|
-
env.insert(
|
|
311
|
+
env.insert('TMP', temp)
|
|
312
|
+
env.insert('TEMP', temp)
|
|
313
|
+
if not env.contains('USERPROFILE'):
|
|
314
|
+
env.insert('HOME', os.path.expanduser('~'))
|
|
315
|
+
env.insert('USERPROFILE', os.path.expanduser('~'))
|
|
240
316
|
if sys.platform == 'darwin' and env.contains('PYTHONEXECUTABLE'):
|
|
241
317
|
# Fix for macOS when napari launched from terminal
|
|
242
318
|
# related to https://github.com/napari/napari/pull/5531
|
|
243
|
-
env.remove(
|
|
319
|
+
env.remove('PYTHONEXECUTABLE')
|
|
244
320
|
return env
|
|
245
321
|
|
|
246
322
|
def _add_constraints_to_env(
|
|
@@ -251,18 +327,18 @@ class CondaInstallerTool(AbstractInstallerTool):
|
|
|
251
327
|
constraints = self.constraints()
|
|
252
328
|
if env.contains(PINNED):
|
|
253
329
|
constraints.append(env.value(PINNED))
|
|
254
|
-
env.insert(PINNED,
|
|
330
|
+
env.insert(PINNED, '&'.join(constraints))
|
|
255
331
|
return env
|
|
256
332
|
|
|
257
|
-
def _default_channels(self):
|
|
333
|
+
def _default_channels(self) -> list[str]:
|
|
258
334
|
"""Default channels for conda installations."""
|
|
259
|
-
return
|
|
335
|
+
return ['conda-forge']
|
|
260
336
|
|
|
261
|
-
def _default_prefix(self):
|
|
337
|
+
def _default_prefix(self) -> str:
|
|
262
338
|
"""Default prefix for conda installations."""
|
|
263
|
-
if (Path(sys.prefix) /
|
|
339
|
+
if (Path(sys.prefix) / 'conda-meta').is_dir():
|
|
264
340
|
return sys.prefix
|
|
265
|
-
raise ValueError(
|
|
341
|
+
raise ValueError('Prefix has not been specified!')
|
|
266
342
|
|
|
267
343
|
|
|
268
344
|
class InstallerQueue(QObject):
|
|
@@ -281,7 +357,7 @@ class InstallerQueue(QObject):
|
|
|
281
357
|
started = Signal()
|
|
282
358
|
|
|
283
359
|
# classes to manage pip and conda installations
|
|
284
|
-
|
|
360
|
+
PYPI_INSTALLER_TOOL_CLASS = PipInstallerTool
|
|
285
361
|
CONDA_INSTALLER_TOOL_CLASS = CondaInstallerTool
|
|
286
362
|
# This should be set to the name of package that handles plugins
|
|
287
363
|
# e.g `napari` for napari
|
|
@@ -295,7 +371,7 @@ class InstallerQueue(QObject):
|
|
|
295
371
|
self._current_process: QProcess = None
|
|
296
372
|
self._prefix = prefix
|
|
297
373
|
self._output_widget = None
|
|
298
|
-
self._exit_codes = []
|
|
374
|
+
self._exit_codes: list[int] = []
|
|
299
375
|
|
|
300
376
|
# -------------------------- Public API ------------------------------
|
|
301
377
|
def install(
|
|
@@ -416,7 +492,7 @@ class InstallerQueue(QObject):
|
|
|
416
492
|
)
|
|
417
493
|
return self._queue_item(item)
|
|
418
494
|
|
|
419
|
-
def cancel(self, job_id: JobId):
|
|
495
|
+
def cancel(self, job_id: JobId) -> None:
|
|
420
496
|
"""Cancel a job.
|
|
421
497
|
|
|
422
498
|
Cancel the process, if it is running, referenced by `job_id`.
|
|
@@ -458,18 +534,18 @@ class InstallerQueue(QObject):
|
|
|
458
534
|
self._process_queue()
|
|
459
535
|
return
|
|
460
536
|
|
|
461
|
-
msg = f
|
|
462
|
-
msg +=
|
|
537
|
+
msg = f'No job with id {job_id}. Current queue:\n - '
|
|
538
|
+
msg += '\n - '.join(
|
|
463
539
|
[
|
|
464
|
-
f
|
|
540
|
+
f'{item.ident} -> {item.executable()} {item.arguments()}'
|
|
465
541
|
for item in self._queue
|
|
466
542
|
]
|
|
467
543
|
)
|
|
468
544
|
raise ValueError(msg)
|
|
469
545
|
|
|
470
|
-
def cancel_all(self):
|
|
546
|
+
def cancel_all(self) -> None:
|
|
471
547
|
"""Terminate all processes in the queue and emit the `processFinished` signal."""
|
|
472
|
-
all_pkgs = []
|
|
548
|
+
all_pkgs: list[str] = []
|
|
473
549
|
for item in deque(self._queue):
|
|
474
550
|
all_pkgs.extend(item.pkgs)
|
|
475
551
|
process = item.process
|
|
@@ -514,7 +590,7 @@ class InstallerQueue(QObject):
|
|
|
514
590
|
"""Return the number of running jobs in the queue."""
|
|
515
591
|
return len(self._queue)
|
|
516
592
|
|
|
517
|
-
def set_output_widget(self, output_widget: QTextEdit):
|
|
593
|
+
def set_output_widget(self, output_widget: QTextEdit) -> None:
|
|
518
594
|
"""Set the output widget for text output."""
|
|
519
595
|
if output_widget:
|
|
520
596
|
self._output_widget = output_widget
|
|
@@ -529,31 +605,31 @@ class InstallerQueue(QObject):
|
|
|
529
605
|
process.errorOccurred.connect(self._on_error_occurred)
|
|
530
606
|
return process
|
|
531
607
|
|
|
532
|
-
def _log(self, msg: str):
|
|
608
|
+
def _log(self, msg: str) -> None:
|
|
533
609
|
log.debug(msg)
|
|
534
610
|
if self._output_widget:
|
|
535
611
|
self._output_widget.append(msg)
|
|
536
612
|
|
|
537
|
-
def _get_tool(self, tool: InstallerTools):
|
|
538
|
-
if tool == InstallerTools.
|
|
539
|
-
return self.
|
|
613
|
+
def _get_tool(self, tool: InstallerTools) -> type[AbstractInstallerTool]:
|
|
614
|
+
if tool == InstallerTools.PYPI:
|
|
615
|
+
return self.PYPI_INSTALLER_TOOL_CLASS
|
|
540
616
|
if tool == InstallerTools.CONDA:
|
|
541
617
|
return self.CONDA_INSTALLER_TOOL_CLASS
|
|
542
|
-
raise ValueError(f
|
|
618
|
+
raise ValueError(f'InstallerTool {tool} not recognized!')
|
|
543
619
|
|
|
544
620
|
def _build_queue_item(
|
|
545
621
|
self,
|
|
546
622
|
tool: InstallerTools,
|
|
547
623
|
action: InstallerActions,
|
|
548
|
-
pkgs:
|
|
624
|
+
pkgs: Iterable[str],
|
|
549
625
|
prefix: str | None = None,
|
|
550
|
-
origins:
|
|
626
|
+
origins: Iterable[str] = (),
|
|
551
627
|
**kwargs,
|
|
552
628
|
) -> AbstractInstallerTool:
|
|
553
629
|
return self._get_tool(tool)(
|
|
554
|
-
pkgs=pkgs,
|
|
630
|
+
pkgs=tuple(pkgs),
|
|
555
631
|
action=action,
|
|
556
|
-
origins=origins,
|
|
632
|
+
origins=tuple(origins),
|
|
557
633
|
prefix=prefix or self._prefix,
|
|
558
634
|
**kwargs,
|
|
559
635
|
)
|
|
@@ -563,7 +639,7 @@ class InstallerQueue(QObject):
|
|
|
563
639
|
self._process_queue()
|
|
564
640
|
return item.ident
|
|
565
641
|
|
|
566
|
-
def _process_queue(self):
|
|
642
|
+
def _process_queue(self) -> None:
|
|
567
643
|
if not self._queue:
|
|
568
644
|
self.allFinished.emit(tuple(self._exit_codes))
|
|
569
645
|
self._exit_codes = []
|
|
@@ -589,7 +665,7 @@ class InstallerQueue(QObject):
|
|
|
589
665
|
process.start()
|
|
590
666
|
self._current_process = process
|
|
591
667
|
|
|
592
|
-
def _end_process(self, process: QProcess):
|
|
668
|
+
def _end_process(self, process: QProcess) -> None:
|
|
593
669
|
if os.name == 'nt':
|
|
594
670
|
# TODO: this might be too agressive and won't allow rollbacks!
|
|
595
671
|
# investigate whether we can also do .terminate()
|
|
@@ -599,12 +675,12 @@ class InstallerQueue(QObject):
|
|
|
599
675
|
|
|
600
676
|
if self._output_widget:
|
|
601
677
|
self._output_widget.append(
|
|
602
|
-
trans._(
|
|
678
|
+
trans._('\nTask was cancelled by the user.')
|
|
603
679
|
)
|
|
604
680
|
|
|
605
681
|
def _on_process_finished(
|
|
606
682
|
self, exit_code: int, exit_status: QProcess.ExitStatus
|
|
607
|
-
):
|
|
683
|
+
) -> None:
|
|
608
684
|
try:
|
|
609
685
|
current = self._queue[0]
|
|
610
686
|
except IndexError:
|
|
@@ -630,7 +706,7 @@ class InstallerQueue(QObject):
|
|
|
630
706
|
)
|
|
631
707
|
self._on_process_done(exit_code=exit_code, exit_status=exit_status)
|
|
632
708
|
|
|
633
|
-
def _on_error_occurred(self, error: QProcess.ProcessError):
|
|
709
|
+
def _on_error_occurred(self, error: QProcess.ProcessError) -> None:
|
|
634
710
|
self._on_process_done(error=error)
|
|
635
711
|
|
|
636
712
|
def _on_process_done(
|
|
@@ -638,18 +714,18 @@ class InstallerQueue(QObject):
|
|
|
638
714
|
exit_code: int | None = None,
|
|
639
715
|
exit_status: QProcess.ExitStatus | None = None,
|
|
640
716
|
error: QProcess.ProcessError | None = None,
|
|
641
|
-
):
|
|
717
|
+
) -> None:
|
|
642
718
|
item = None
|
|
643
719
|
with contextlib.suppress(IndexError):
|
|
644
720
|
item = self._queue.popleft()
|
|
645
721
|
|
|
646
722
|
if error:
|
|
647
723
|
msg = trans._(
|
|
648
|
-
|
|
724
|
+
'Task finished with errors! Error: {error}.', error=error
|
|
649
725
|
)
|
|
650
726
|
else:
|
|
651
727
|
msg = trans._(
|
|
652
|
-
|
|
728
|
+
'Task finished with exit code {exit_code} with status {exit_status}.',
|
|
653
729
|
exit_code=exit_code,
|
|
654
730
|
exit_status=exit_status,
|
|
655
731
|
)
|
|
@@ -668,7 +744,7 @@ class InstallerQueue(QObject):
|
|
|
668
744
|
self._log(msg)
|
|
669
745
|
self._process_queue()
|
|
670
746
|
|
|
671
|
-
def _on_stdout_ready(self):
|
|
747
|
+
def _on_stdout_ready(self) -> None:
|
|
672
748
|
if self._current_process is not None:
|
|
673
749
|
try:
|
|
674
750
|
text = (
|
|
@@ -677,12 +753,12 @@ class InstallerQueue(QObject):
|
|
|
677
753
|
.decode()
|
|
678
754
|
)
|
|
679
755
|
except UnicodeDecodeError:
|
|
680
|
-
|
|
756
|
+
log.exception('Could not decode stdout')
|
|
681
757
|
return
|
|
682
758
|
if text:
|
|
683
759
|
self._log(text)
|
|
684
760
|
|
|
685
|
-
def _on_stderr_ready(self):
|
|
761
|
+
def _on_stderr_ready(self) -> None:
|
|
686
762
|
if self._current_process is not None:
|
|
687
763
|
text = self._current_process.readAllStandardError().data().decode()
|
|
688
764
|
if text:
|