From 04ec41e9e9e2133ab9574953ea3d57b5a8c4105f Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 18 Jun 2026 16:35:05 +0200 Subject: [PATCH 1/6] Improve import handling for QtPy Modified import statements to handle potential ImportError for QtPy. Close # 654 --- src/fabio/app/viewer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/fabio/app/viewer.py b/src/fabio/app/viewer.py index c8c3ef1d..f8f5f2c9 100644 --- a/src/fabio/app/viewer.py +++ b/src/fabio/app/viewer.py @@ -39,9 +39,10 @@ __author__ = "Gaël Goret, Jérôme Kieffer" __copyright__ = "2015-2026 ESRF" __licence__ = "MIT" -__date__ = "12/03/2026" +__date__ = "18/06/2026" +import os import sys from argparse import ArgumentParser import numpy @@ -50,9 +51,16 @@ # ---------------------------------------------------------------------- # Qt imports via QtPy – this works with PyQt5, PySide2, PySide6, etc. # ---------------------------------------------------------------------- -from qtpy.QtWidgets import QApplication, QStyleFactory -from ..qt.viewer import AppForm - +try: + from qtpy.QtWidgets import QApplication, QStyleFactory + from ..qt.viewer import AppForm +except ImportError as err: + if os.environ.get("WITH_QT_TEST") == "False": + print("QtPy is not available but it does not matter as `WITH_QT_TEST=False`") + else: + raise err + + # Matplotlib imports (unchanged) # ---------------------------------------------------------------------- # Global configuration From ca4177c337cffffe4a02216bbe106b3c414a3cc9 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 18 Jun 2026 16:37:39 +0200 Subject: [PATCH 2/6] Handle ImportError for QtPy in dialogs.py Add error handling for QtPy import failure --- src/fabio/qt/dialogs.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/fabio/qt/dialogs.py b/src/fabio/qt/dialogs.py index 5c94ded8..6e988162 100644 --- a/src/fabio/qt/dialogs.py +++ b/src/fabio/qt/dialogs.py @@ -36,14 +36,22 @@ # Imports (QtPy, FabIO, NumPy, Matplotlib, …) # ---------------------------------------------------------------------- import numpy -from qtpy import QtWidgets as qt -from qtpy.QtWidgets import ( - QMessageBox, - QLabel, - QDialog, - QDialogButtonBox, - QGroupBox, -) +import os +try: + from qtpy import QtWidgets as qt + from qtpy.QtWidgets import ( + QMessageBox, + QLabel, + QDialog, + QDialogButtonBox, + QGroupBox, + ) +except ImportError as err: + if os.environ.get("WITH_QT_TEST") == "False": + print("QtPy is not available but it does not matter as `WITH_QT_TEST=False`") + else: + raise err + numpy.seterr(divide="ignore") output_format = [ From 2d2a63cfc42f64dc77abea122b9d2903c3ee84f4 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 18 Jun 2026 16:41:20 +0200 Subject: [PATCH 3/6] Add error handling for qtpy import Handle ImportError for qtpy and manage environment variable for testing. --- src/fabio/qt/matplotlib.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/fabio/qt/matplotlib.py b/src/fabio/qt/matplotlib.py index 7c3f7eda..70d9283a 100644 --- a/src/fabio/qt/matplotlib.py +++ b/src/fabio/qt/matplotlib.py @@ -38,12 +38,19 @@ __date__ = "12/03/2026" +import os import io import matplotlib import numpy -from qtpy import API as BINDING -from qtpy.QtGui import QFont +try: + from qtpy import API as BINDING + from qtpy.QtGui import QFont +except ImportError as err: + if os.environ["WITH_QT_TEST"] == "False": + print("QtPy not present but since `WITH_QT_TEST` is False, silence it") + else: + raise err # This must be performed before any import from matplotlib if BINDING in ("pyside6", "pyqt6"): From 79ea868004a0e3d6a0c918b071b9718d06be6fff Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 18 Jun 2026 16:44:07 +0200 Subject: [PATCH 4/6] Refactor Qt imports with error handling Wrap Qt imports in a try-except block to handle ImportError and check environment variable. --- src/fabio/qt/viewer.py | 59 +++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/fabio/qt/viewer.py b/src/fabio/qt/viewer.py index ea442b22..62c9c8f7 100644 --- a/src/fabio/qt/viewer.py +++ b/src/fabio/qt/viewer.py @@ -44,32 +44,39 @@ import numpy import fabio from fabio.nexus import Nexus -# ---------------------------------------------------------------------- -# Qt imports via QtPy – this works with PyQt5, PySide2, PySide6, etc. -# ---------------------------------------------------------------------- -from qtpy import QtWidgets as qt -from qtpy import QtCore as qtc -from qtpy.QtGui import QIcon -from qtpy.QtWidgets import ( - QSizePolicy, - QFileDialog, - QMessageBox, - QAction, - QComboBox, - QPlainTextEdit, - QLabel, - QSplitter, - QTabWidget, - QWidget, - QProgressBar, - QGroupBox, - QVBoxLayout, - QHBoxLayout, - QPushButton, - QListWidget, - QCheckBox, - QButtonGroup -) +try: + # ---------------------------------------------------------------------- + # Qt imports via QtPy – this works with PyQt5, PySide2, PySide6, etc. + # ---------------------------------------------------------------------- + from qtpy import QtWidgets as qt + from qtpy import QtCore as qtc + from qtpy.QtGui import QIcon + from qtpy.QtWidgets import ( + QSizePolicy, + QFileDialog, + QMessageBox, + QAction, + QComboBox, + QPlainTextEdit, + QLabel, + QSplitter, + QTabWidget, + QWidget, + QProgressBar, + QGroupBox, + QVBoxLayout, + QHBoxLayout, + QPushButton, + QListWidget, + QCheckBox, + QButtonGroup + ) +except ImportError as err: + if os.environ.get("WITH_QT_TEST") == "False": + print("QtPy does not import but it does not matter as `WITH_QT_TEST` is False.") + else: + raise err + # Matplotlib imports (unchanged) from matplotlib.figure import Figure from .matplotlib import FigureCanvasQTAgg From 3644cddd7cc89fbf1c1c3d83a7c74dd7c8f8a024 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 21 Aug 2026 09:35:23 +0200 Subject: [PATCH 5/6] conditionally skip QT-imports Skip certain imports based on the WITH_QT_TEST environment variable. --- src/fabio/test/test_import.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/fabio/test/test_import.py b/src/fabio/test/test_import.py index 6998e027..da552a93 100644 --- a/src/fabio/test/test_import.py +++ b/src/fabio/test/test_import.py @@ -14,17 +14,22 @@ class TestImport(unittest.TestCase): def test_import_all(self): import fabio - + skip_without_qt = { + "fabio.app.viewer", + "fabio.qt.dialogs", + "fabio.qt.matplotlib", + } + with_qt = os.environ.get("WITH_QT_TEST", "True").lower() not in ("0", "false", "no") + base = os.path.split(fabio.__path__[0])[0] + "/" for root, dirs, files in os.walk(fabio.__path__[0]): for f in files: if f.endswith(".py"): - module = os.path.join(root, f[:-3])[len(base) :].replace( - os.sep, "." - ) + module = os.path.join(root, f[:-3])[len(base):].replace(os.sep, ".") + if not with_qt and module in skip_without_qt: + continue __import__(module) - def suite(): loadTests = unittest.defaultTestLoader.loadTestsFromTestCase testsuite = unittest.TestSuite() From 5fd3b06cee9943c9e87b9b14ae9573d029fa9211 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 21 Aug 2026 09:54:57 +0200 Subject: [PATCH 6/6] implement Thomas' solution --- src/fabio/app/viewer.py | 16 +++------- src/fabio/qt/dialogs.py | 11 ++----- src/fabio/qt/matplotlib.py | 18 ++++------- src/fabio/qt/viewer.py | 61 +++++++++++++++++--------------------- 4 files changed, 38 insertions(+), 68 deletions(-) diff --git a/src/fabio/app/viewer.py b/src/fabio/app/viewer.py index f8f5f2c9..9df853f1 100644 --- a/src/fabio/app/viewer.py +++ b/src/fabio/app/viewer.py @@ -39,10 +39,9 @@ __author__ = "Gaël Goret, Jérôme Kieffer" __copyright__ = "2015-2026 ESRF" __licence__ = "MIT" -__date__ = "18/06/2026" +__date__ = "20/08/2026" -import os import sys from argparse import ArgumentParser import numpy @@ -51,16 +50,9 @@ # ---------------------------------------------------------------------- # Qt imports via QtPy – this works with PyQt5, PySide2, PySide6, etc. # ---------------------------------------------------------------------- -try: - from qtpy.QtWidgets import QApplication, QStyleFactory - from ..qt.viewer import AppForm -except ImportError as err: - if os.environ.get("WITH_QT_TEST") == "False": - print("QtPy is not available but it does not matter as `WITH_QT_TEST=False`") - else: - raise err - - +from qtpy.QtWidgets import QApplication, QStyleFactory +from ..qt.viewer import AppForm + # Matplotlib imports (unchanged) # ---------------------------------------------------------------------- # Global configuration diff --git a/src/fabio/qt/dialogs.py b/src/fabio/qt/dialogs.py index 6e988162..e28d2808 100644 --- a/src/fabio/qt/dialogs.py +++ b/src/fabio/qt/dialogs.py @@ -36,21 +36,14 @@ # Imports (QtPy, FabIO, NumPy, Matplotlib, …) # ---------------------------------------------------------------------- import numpy -import os -try: - from qtpy import QtWidgets as qt - from qtpy.QtWidgets import ( +from qtpy import QtWidgets as qt +from qtpy.QtWidgets import ( QMessageBox, QLabel, QDialog, QDialogButtonBox, QGroupBox, ) -except ImportError as err: - if os.environ.get("WITH_QT_TEST") == "False": - print("QtPy is not available but it does not matter as `WITH_QT_TEST=False`") - else: - raise err numpy.seterr(divide="ignore") diff --git a/src/fabio/qt/matplotlib.py b/src/fabio/qt/matplotlib.py index 70d9283a..c4930bf4 100644 --- a/src/fabio/qt/matplotlib.py +++ b/src/fabio/qt/matplotlib.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2016-2024 European Synchrotron Radiation Facility +# Copyright (c) 2016-2026 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -30,27 +30,19 @@ to the used backend. """ -from __future__ import annotations - __authors__ = ["T. Vincent"] __license__ = "MIT" -__date__ = "12/03/2026" +__date__ = "20/08/2026" -import os import io import matplotlib import numpy -try: - from qtpy import API as BINDING - from qtpy.QtGui import QFont -except ImportError as err: - if os.environ["WITH_QT_TEST"] == "False": - print("QtPy not present but since `WITH_QT_TEST` is False, silence it") - else: - raise err +from qtpy import API as BINDING +from qtpy.QtGui import QFont + # This must be performed before any import from matplotlib if BINDING in ("pyside6", "pyqt6"): diff --git a/src/fabio/qt/viewer.py b/src/fabio/qt/viewer.py index 62c9c8f7..c2f90290 100644 --- a/src/fabio/qt/viewer.py +++ b/src/fabio/qt/viewer.py @@ -44,39 +44,32 @@ import numpy import fabio from fabio.nexus import Nexus -try: - # ---------------------------------------------------------------------- - # Qt imports via QtPy – this works with PyQt5, PySide2, PySide6, etc. - # ---------------------------------------------------------------------- - from qtpy import QtWidgets as qt - from qtpy import QtCore as qtc - from qtpy.QtGui import QIcon - from qtpy.QtWidgets import ( - QSizePolicy, - QFileDialog, - QMessageBox, - QAction, - QComboBox, - QPlainTextEdit, - QLabel, - QSplitter, - QTabWidget, - QWidget, - QProgressBar, - QGroupBox, - QVBoxLayout, - QHBoxLayout, - QPushButton, - QListWidget, - QCheckBox, - QButtonGroup - ) -except ImportError as err: - if os.environ.get("WITH_QT_TEST") == "False": - print("QtPy does not import but it does not matter as `WITH_QT_TEST` is False.") - else: - raise err - +# ---------------------------------------------------------------------- +# Qt imports via QtPy – this works with PyQt5, PySide2, PySide6, etc. +# ---------------------------------------------------------------------- +from qtpy import QtWidgets as qt +from qtpy import QtCore as qtc +from qtpy.QtGui import QIcon +from qtpy.QtWidgets import ( + QSizePolicy, + QFileDialog, + QMessageBox, + QAction, + QComboBox, + QPlainTextEdit, + QLabel, + QSplitter, + QTabWidget, + QWidget, + QProgressBar, + QGroupBox, + QVBoxLayout, + QHBoxLayout, + QPushButton, + QListWidget, + QCheckBox, + QButtonGroup + ) # Matplotlib imports (unchanged) from matplotlib.figure import Figure from .matplotlib import FigureCanvasQTAgg @@ -144,7 +137,7 @@ def format_header(self, d): :param d: dict containing headers :return: formatted string - """ + """ keys = sorted(d.keys()) return " \n".join([f"{k}: {d[k]}" for k in keys]) + " \n"