diff --git a/fill_resistance/plots.py b/fill_resistance/plots.py index 39dfe9f..cab0a81 100644 --- a/fill_resistance/plots.py +++ b/fill_resistance/plots.py @@ -26,14 +26,18 @@ def _pick_backend(): ("Cannot load backend 'TkAgg' ... as 'qt' is currently running") - exactly what happened on macOS, whose bundled Python ships tkinter. - The probe must import the binding's native core, not just the - package: on NixOS `import PySide6` succeeds (pure __init__) while - QtCore's .so cannot find the system libraries pip wheels expect - ("libgthread-2.0.so.0: cannot open shared object file") - promising - QtAgg then kills even the error figure at switch_backend time.""" + The probe must import QtWidgets, not just the package or QtCore: + on NixOS `import PySide6` succeeds (pure __init__) while QtCore's + .so cannot find the system libraries pip wheels expect + ("libgthread-2.0.so.0: cannot open shared object file"), and on a + 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"): try: - __import__(qt + ".QtCore") + __import__(qt + ".QtWidgets") return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg" except Exception: continue diff --git a/fill_resistance/report.py b/fill_resistance/report.py index 9d53815..e951c28 100644 --- a/fill_resistance/report.py +++ b/fill_resistance/report.py @@ -68,7 +68,7 @@ def write_summary(outdir: Path, problem: Problem, stack: RasterStack, f"resistivity: {problem.rho_ohm_m:.3e} ohm*m", 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)" if result.freq_hz > 0 else "DC")), 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}" + (" (uniform orthogonal injection; R is the upper contact bound)" if result.contact_model == "uniform" else " (ideal bonded lug)"), - f"terminals:", + "terminals:", f" V+ ({len(problem.electrodes1)} injection area(s)):", *(f" {_electrode_line(e)}" for e in problem.electrodes1), f" V- ({len(problem.electrodes2)} injection area(s)):", diff --git a/tests/test_platform_fallbacks.py b/tests/test_platform_fallbacks.py index e898b13..ba0c546 100644 --- a/tests/test_platform_fallbacks.py +++ b/tests/test_platform_fallbacks.py @@ -20,19 +20,27 @@ def test_backend_prefers_qt_over_tk(): 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 - # QtCore's .so cannot load the FHS system libraries pip wheels - # expect. The probe must import the native core and fall through - - # promising QtAgg kills even the error figure at switch_backend - # time, and the failure report with it. + # the native .so's cannot load the FHS system libraries pip wheels + # expect; on a partially provisioned system even QtCore loads + # (glib, icu present) while QtWidgets/QtGui still miss libGL. The + # 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 types real_import = builtins.__import__ def broken_qt(name, *args, **kwargs): - if name.split(".")[0] in ("PySide6", "PyQt6", "PyQt5", "PySide2"): - raise ImportError("libgthread-2.0.so.0: cannot open shared " - "object file: No such file or directory") + root, _, sub = name.partition(".") + if root in ("PySide6", "PyQt6", "PyQt5", "PySide2"): + 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) monkeypatch.setattr(builtins, "__import__", broken_qt)