Probe QtWidgets, not QtCore: the qt backend needs the full GUI stack
QtCore's libraries (glib, icu) can load while QtWidgets/QtGui still miss libGL - matplotlib's qt backend imports all three, so the old probe could promise QtAgg and lose even the error figure at switch_backend, one library layer deeper than the NixOS fix. The regression test's mock now mirrors the real failure (bare package and QtCore import fine, GUI modules raise): both the bare-import and the QtCore-probe variants now fail it, verified by mutation. Also drop two redundant f-prefixes on placeholder-less strings in report.py.
This commit is contained in:
@@ -26,14 +26,18 @@ def _pick_backend():
|
|||||||
("Cannot load backend 'TkAgg' ... as 'qt' is currently running") -
|
("Cannot load backend 'TkAgg' ... as 'qt' is currently running") -
|
||||||
exactly what happened on macOS, whose bundled Python ships tkinter.
|
exactly what happened on macOS, whose bundled Python ships tkinter.
|
||||||
|
|
||||||
The probe must import the binding's native core, not just the
|
The probe must import QtWidgets, not just the package or QtCore:
|
||||||
package: on NixOS `import PySide6` succeeds (pure __init__) while
|
on NixOS `import PySide6` succeeds (pure __init__) while QtCore's
|
||||||
QtCore's .so cannot find the system libraries pip wheels expect
|
.so cannot find the system libraries pip wheels expect
|
||||||
("libgthread-2.0.so.0: cannot open shared object file") - promising
|
("libgthread-2.0.so.0: cannot open shared object file"), and on a
|
||||||
QtAgg then kills even the error figure at switch_backend time."""
|
partially provisioned system QtCore's deps (glib, icu) can be
|
||||||
|
present while QtWidgets/QtGui still miss libGL/libEGL. matplotlib's
|
||||||
|
qt backend imports QtCore, QtGui and QtWidgets, so probe the widest
|
||||||
|
one - promising QtAgg then kills even the error figure at
|
||||||
|
switch_backend time."""
|
||||||
for qt in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
|
for qt in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
|
||||||
try:
|
try:
|
||||||
__import__(qt + ".QtCore")
|
__import__(qt + ".QtWidgets")
|
||||||
return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg"
|
return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg"
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def write_summary(outdir: Path, problem: Problem, stack: RasterStack,
|
|||||||
f"resistivity: {problem.rho_ohm_m:.3e} ohm*m",
|
f"resistivity: {problem.rho_ohm_m:.3e} ohm*m",
|
||||||
f"via plating: {problem.plating_nm / 1000:.0f} um",
|
f"via plating: {problem.plating_nm / 1000:.0f} um",
|
||||||
"",
|
"",
|
||||||
(f"frequency: "
|
("frequency: "
|
||||||
+ (f"{result.freq_hz:g} Hz (skin depth {result.skin_depth_um:.0f} um)"
|
+ (f"{result.freq_hz:g} Hz (skin depth {result.skin_depth_um:.0f} um)"
|
||||||
if result.freq_hz > 0 else "DC")),
|
if result.freq_hz > 0 else "DC")),
|
||||||
f"RESISTANCE: {result.R_ohm * 1000:.6g} mOhm"
|
f"RESISTANCE: {result.R_ohm * 1000:.6g} mOhm"
|
||||||
@@ -127,7 +127,7 @@ def write_summary(outdir: Path, problem: Problem, stack: RasterStack,
|
|||||||
f"contact model: {result.contact_model}"
|
f"contact model: {result.contact_model}"
|
||||||
+ (" (uniform orthogonal injection; R is the upper contact bound)"
|
+ (" (uniform orthogonal injection; R is the upper contact bound)"
|
||||||
if result.contact_model == "uniform" else " (ideal bonded lug)"),
|
if result.contact_model == "uniform" else " (ideal bonded lug)"),
|
||||||
f"terminals:",
|
"terminals:",
|
||||||
f" V+ ({len(problem.electrodes1)} injection area(s)):",
|
f" V+ ({len(problem.electrodes1)} injection area(s)):",
|
||||||
*(f" {_electrode_line(e)}" for e in problem.electrodes1),
|
*(f" {_electrode_line(e)}" for e in problem.electrodes1),
|
||||||
f" V- ({len(problem.electrodes2)} injection area(s)):",
|
f" V- ({len(problem.electrodes2)} injection area(s)):",
|
||||||
|
|||||||
@@ -20,19 +20,27 @@ def test_backend_prefers_qt_over_tk():
|
|||||||
assert plots._pick_backend() in ("QtAgg", "Qt5Agg")
|
assert plots._pick_backend() in ("QtAgg", "Qt5Agg")
|
||||||
|
|
||||||
|
|
||||||
def test_backend_probe_requires_working_qtcore(monkeypatch):
|
def test_backend_probe_requires_working_qt_gui_stack(monkeypatch):
|
||||||
# NixOS: `import PySide6` succeeds (a pure-Python __init__) while
|
# NixOS: `import PySide6` succeeds (a pure-Python __init__) while
|
||||||
# QtCore's .so cannot load the FHS system libraries pip wheels
|
# the native .so's cannot load the FHS system libraries pip wheels
|
||||||
# expect. The probe must import the native core and fall through -
|
# expect; on a partially provisioned system even QtCore loads
|
||||||
# promising QtAgg kills even the error figure at switch_backend
|
# (glib, icu present) while QtWidgets/QtGui still miss libGL. The
|
||||||
# time, and the failure report with it.
|
# probe must import QtWidgets and fall through - promising QtAgg
|
||||||
|
# kills even the error figure at switch_backend time, and the
|
||||||
|
# failure report with it. The mock mirrors that faithfully (bare
|
||||||
|
# package and QtCore succeed, GUI modules fail) so a probe reverted
|
||||||
|
# to `__import__(qt)` or `.QtCore` would wrongly return QtAgg here.
|
||||||
import builtins
|
import builtins
|
||||||
|
import types
|
||||||
real_import = builtins.__import__
|
real_import = builtins.__import__
|
||||||
|
|
||||||
def broken_qt(name, *args, **kwargs):
|
def broken_qt(name, *args, **kwargs):
|
||||||
if name.split(".")[0] in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
|
root, _, sub = name.partition(".")
|
||||||
raise ImportError("libgthread-2.0.so.0: cannot open shared "
|
if root in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
|
||||||
"object file: No such file or directory")
|
if sub in ("", "QtCore"):
|
||||||
|
return types.ModuleType(name)
|
||||||
|
raise ImportError("libGL.so.1: cannot open shared object "
|
||||||
|
"file: No such file or directory")
|
||||||
return real_import(name, *args, **kwargs)
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
monkeypatch.setattr(builtins, "__import__", broken_qt)
|
monkeypatch.setattr(builtins, "__import__", broken_qt)
|
||||||
|
|||||||
Reference in New Issue
Block a user