Skip to content

Commit 37cbe94

Browse files
committed
Addon Manager: Use pip utility function
Also attempts to fix some bugs when dep installation fails. [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci S
1 parent 28c2b13 commit 37cbe94

File tree

5 files changed

+63
-71
lines changed

5 files changed

+63
-71
lines changed

src/Mod/AddonManager/AddonManagerTest/app/test_dependency_installer.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,6 @@ def test_install_python_packages_existing_location(self):
136136
self.assertTrue(ff_required.called)
137137
self.assertTrue(ff_optional.called)
138138

139-
def test_verify_pip_no_python(self):
140-
self.test_object._get_python = lambda: None
141-
should_continue = self.test_object._verify_pip()
142-
self.assertFalse(should_continue)
143-
self.assertEqual(len(self.signals_caught), 0)
144-
145139
def test_verify_pip_no_pip(self):
146140
sm = SubprocessMock()
147141
sm.succeed = False

src/Mod/AddonManager/addonmanager_dependency_installer.py

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
import subprocess
2828
from typing import List
2929

30-
from freecad.utils import get_python_exe
31-
3230
import addonmanager_freecad_interface as fci
3331
from addonmanager_pyside_interface import QObject, Signal, is_interruption_requested
3432

@@ -46,7 +44,7 @@ class DependencyInstaller(QObject):
4644
no_python_exe = Signal()
4745
no_pip = Signal(str) # Attempted command
4846
failure = Signal(str, str) # Short message, detailed message
49-
finished = Signal()
47+
finished = Signal(bool) # True if everything completed normally, otherwise false
5048

5149
def __init__(
5250
self,
@@ -65,17 +63,25 @@ def __init__(
6563
self.python_requires = python_requires
6664
self.python_optional = python_optional
6765
self.location = location
66+
self.required_succeeded = False
67+
self.finished_successfully = False
6868

6969
def run(self):
7070
"""Normally not called directly, but rather connected to the worker thread's started
7171
signal."""
72-
if self._verify_pip():
72+
try:
7373
if self.python_requires or self.python_optional:
74-
if not is_interruption_requested():
75-
self._install_python_packages()
76-
if not is_interruption_requested():
77-
self._install_addons()
78-
self.finished.emit()
74+
if self._verify_pip():
75+
if not is_interruption_requested():
76+
self._install_python_packages()
77+
else:
78+
self.required_succeeded = True
79+
if not is_interruption_requested():
80+
self._install_addons()
81+
self.finished_successfully = self.required_succeeded
82+
except RuntimeError:
83+
pass
84+
self.finished.emit(self.finished_successfully)
7985

8086
def _install_python_packages(self):
8187
"""Install required and optional Python dependencies using pip."""
@@ -87,20 +93,20 @@ def _install_python_packages(self):
8793
if not os.path.exists(vendor_path):
8894
os.makedirs(vendor_path)
8995

90-
self._install_required(vendor_path)
96+
self.required_succeeded = self._install_required(vendor_path)
9197
self._install_optional(vendor_path)
9298

9399
def _verify_pip(self) -> bool:
94100
"""Ensure that pip is working -- returns True if it is, or False if not. Also emits the
95101
no_pip signal if pip cannot execute."""
96-
python_exe = self._get_python()
97-
if not python_exe:
98-
return False
99102
try:
100103
proc = self._run_pip(["--version"])
101104
fci.Console.PrintMessage(proc.stdout + "\n")
105+
if proc.returncode != 0:
106+
return False
102107
except subprocess.CalledProcessError:
103-
self.no_pip.emit(f"{python_exe} -m pip --version")
108+
call = utils.create_pip_call([])
109+
self.no_pip.emit(" ".join(call))
104110
return False
105111
return True
106112

@@ -115,7 +121,6 @@ def _install_required(self, vendor_path: str) -> bool:
115121
proc = self._run_pip(
116122
[
117123
"install",
118-
"--disable-pip-version-check",
119124
"--target",
120125
vendor_path,
121126
pymod,
@@ -144,7 +149,6 @@ def _install_optional(self, vendor_path: str):
144149
proc = self._run_pip(
145150
[
146151
"install",
147-
"--disable-pip-version-check",
148152
"--target",
149153
vendor_path,
150154
pymod,
@@ -160,22 +164,13 @@ def _install_optional(self, vendor_path: str):
160164
)
161165

162166
def _run_pip(self, args):
163-
python_exe = self._get_python()
164-
final_args = [python_exe, "-m", "pip"]
165-
final_args.extend(args)
167+
final_args = utils.create_pip_call(args)
166168
return self._subprocess_wrapper(final_args)
167169

168170
@staticmethod
169171
def _subprocess_wrapper(args) -> subprocess.CompletedProcess:
170172
"""Wrap subprocess call so test code can mock it."""
171-
return utils.run_interruptable_subprocess(args)
172-
173-
def _get_python(self) -> str:
174-
"""Wrap Python access so test code can mock it."""
175-
python_exe = get_python_exe()
176-
if not python_exe:
177-
self.no_python_exe.emit()
178-
return python_exe
173+
return utils.run_interruptable_subprocess(args, timeout_secs=120)
179174

180175
def _install_addons(self):
181176
for addon in self.addons:

src/Mod/AddonManager/addonmanager_installer_gui.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,26 @@ def __init__(self, addon: Addon, addons: List[Addon] = None):
7777
self.installer.failure.connect(self._installation_failed)
7878

7979
def __del__(self):
80-
if self.worker_thread and hasattr(self.worker_thread, "quit"):
81-
self.worker_thread.quit()
82-
self.worker_thread.wait(500)
83-
if self.worker_thread.isRunning():
80+
self._stop_thread(self.worker_thread)
81+
self._stop_thread(self.dependency_worker_thread)
82+
83+
@staticmethod
84+
def _stop_thread(thread: QtCore.QThread):
85+
if thread and hasattr(thread, "quit"):
86+
if thread.isRunning():
87+
FreeCAD.Console.PrintMessage(
88+
"INTERNAL ERROR: a QThread is still running when it " "should have finished"
89+
)
90+
91+
thread.requestInterruption()
92+
thread.wait(100)
93+
thread.quit()
94+
thread.wait(500)
95+
if thread.isRunning():
8496
FreeCAD.Console.PrintError(
8597
"INTERNAL ERROR: Thread did not quit() cleanly, using terminate()\n"
8698
)
87-
self.worker_thread.terminate()
99+
thread.terminate()
88100

89101
def run(self):
90102
"""Instructs this class to begin displaying the necessary dialogs to guide a user through
@@ -300,13 +312,11 @@ def _run_dependency_installer(self, addons, python_requires, python_optional):
300312
self.dependency_installer.no_python_exe.connect(self._report_no_python_exe)
301313
self.dependency_installer.no_pip.connect(self._report_no_pip)
302314
self.dependency_installer.failure.connect(self._report_dependency_failure)
303-
self.dependency_installer.finished.connect(self._cleanup_dependency_worker)
304-
self.dependency_installer.finished.connect(self._report_dependency_success)
315+
self.dependency_installer.finished.connect(self._dependencies_finished)
305316

306317
self.dependency_worker_thread = QtCore.QThread(self)
307318
self.dependency_installer.moveToThread(self.dependency_worker_thread)
308319
self.dependency_worker_thread.started.connect(self.dependency_installer.run)
309-
self.dependency_installer.finished.connect(self.dependency_worker_thread.quit)
310320

311321
self.dependency_installation_dialog = QtWidgets.QMessageBox(
312322
QtWidgets.QMessageBox.Information,
@@ -319,16 +329,6 @@ def _run_dependency_installer(self, addons, python_requires, python_optional):
319329
self.dependency_installation_dialog.show()
320330
self.dependency_worker_thread.start()
321331

322-
def _cleanup_dependency_worker(self) -> None:
323-
return
324-
self.dependency_worker_thread.quit()
325-
self.dependency_worker_thread.wait(500)
326-
if self.dependency_worker_thread.isRunning():
327-
FreeCAD.Console.PrintError(
328-
"INTERNAL ERROR: Thread did not quit() cleanly, using terminate()\n"
329-
)
330-
self.dependency_worker_thread.terminate()
331-
332332
def _report_no_python_exe(self) -> None:
333333
"""Callback for the dependency installer failing to locate a Python executable."""
334334
if self.dependency_installation_dialog is not None:
@@ -409,6 +409,11 @@ def _report_dependency_success(self):
409409
self.dependency_installation_dialog.hide()
410410
self.install()
411411

412+
def _dependencies_finished(self, success: bool):
413+
if success:
414+
self._report_dependency_success()
415+
self.dependency_worker_thread.quit()
416+
412417
def _dependency_dialog_ignore_clicked(self) -> None:
413418
"""Callback for when dependencies are ignored."""
414419
self.install()

src/Mod/AddonManager/addonmanager_python_deps_gui.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import sys
3434
from functools import partial
3535
from typing import Dict, Iterable, List, Tuple, TypedDict
36+
from addonmanager_utilities import create_pip_call
3637

3738
import addonmanager_freecad_interface as fci
3839

@@ -107,26 +108,21 @@ def call_pip(args: List[str]) -> List[str]:
107108
"""Tries to locate the appropriate Python executable and run pip with version checking
108109
disabled. Fails if Python can't be found or if pip is not installed."""
109110

110-
python_exe = get_python_exe()
111-
pip_failed = False
112-
if python_exe:
113-
call_args = [python_exe, "-m", "pip", "--disable-pip-version-check"]
114-
call_args.extend(args)
115-
proc = None
116-
try:
117-
proc = utils.run_interruptable_subprocess(call_args)
118-
except subprocess.CalledProcessError:
119-
pip_failed = True
120-
121-
if not pip_failed:
122-
data = proc.stdout
123-
return data.split("\n")
124-
elif proc:
125-
raise PipFailed(proc.stderr)
126-
else:
127-
raise PipFailed("pip timed out")
128-
else:
129-
raise PipFailed("Could not locate Python executable on this system")
111+
try:
112+
call_args = create_pip_call(args)
113+
except RuntimeError as exception:
114+
raise PipFailed() from exception
115+
116+
try:
117+
proc = utils.run_interruptable_subprocess(call_args)
118+
except subprocess.CalledProcessError as exception:
119+
raise PipFailed("pip timed out") from exception
120+
121+
if proc.returncode != 0:
122+
raise PipFailed(proc.stderr)
123+
124+
data = proc.stdout
125+
return data.split("\n")
130126

131127

132128
def parse_pip_list_output(all_packages, outdated_packages) -> Dict[str, Dict[str, str]]:

src/Mod/AddonManager/addonmanager_utilities.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@
5050
try:
5151
from freecad.utils import get_python_exe
5252
except ImportError:
53+
5354
def get_python_exe():
5455
return shutil.which("python")
5556

57+
5658
if fci.FreeCADGui:
5759

5860
# If the GUI is up, we can use the NetworkManager to handle our downloads. If there is no event

0 commit comments

Comments
 (0)