From eba63a77fc83f93e72e2042e07af13350f2e97af Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Mon, 15 Jun 2026 15:09:02 +0200 Subject: [PATCH 1/4] [fix] Hdf5TreeModel 'destroyed' signal can segfault for python>=3.13 --- src/silx/_utils.py | 35 ++++++++++++++++++++++++++++++ src/silx/gui/hdf5/Hdf5TreeModel.py | 11 +++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/silx/_utils.py b/src/silx/_utils.py index 54ee545262..01c6c31b1e 100644 --- a/src/silx/_utils.py +++ b/src/silx/_utils.py @@ -37,3 +37,38 @@ def nfs_cache_refresh(dirname: str) -> None: _ = entry.stat() except Exception: pass + + +class Partial: + """ + Alternative implementation of ``functools.partial``. + + Stores a callable together with positional and keyword arguments and + invokes it later when called. + + This class was introduced as a workaround for segfaults observed with + ``functools.partial`` in Qt/Python object destruction and garbage + collection scenarios since Python 3.13. + + This is possible caused by the partial keeping a reference to the + callstack in which the partial funcion was created. + """ + + __slots__ = ("_func", "_args", "_kwargs") + + def __init__(self, func, *args, **kwargs): + self._func = func + self._args = args + self._kwargs = kwargs + + def __call__(self, *args, **kwargs): + kwargs = {**self._kwargs, **kwargs} + return self._func(*self._args, *args, **kwargs) + + def __repr__(self): + return ( + f"{type(self).__name__}(" + f"{self._func!r}, " + f"args={self._args!r}, " + f"kwargs={self._kwargs!r})" + ) diff --git a/src/silx/gui/hdf5/Hdf5TreeModel.py b/src/silx/gui/hdf5/Hdf5TreeModel.py index fc0f468eb4..a989a8ba53 100644 --- a/src/silx/gui/hdf5/Hdf5TreeModel.py +++ b/src/silx/gui/hdf5/Hdf5TreeModel.py @@ -29,7 +29,6 @@ import os import logging -import functools from .. import qt from .. import icons from .Hdf5Node import Hdf5Node @@ -40,6 +39,7 @@ from ...io._sliceh5 import DatasetSlice from ...io.url import DataUrl from ..._utils import nfs_cache_refresh as _nfs_cache_refresh +from ..._utils import Partial as _Partial import h5py @@ -241,10 +241,15 @@ def __init__(self, parent=None, ownFiles=True): # to access to the content of the Python object with the `destroyed` # signal cause the Python method was already removed with the QWidget, # while the QObject still exists. + # # We use a static method plus explicit references to objects to # release. The callback do not use any ref to self. - onDestroy = functools.partial(self._closeFileList, self.__openedFiles) - self.destroyed.connect(onDestroy) + # + # Since Python 3.13, `functools.partial` can cause the callback to + # still segfaults, possible because it keeps a reference to the + # current call stack and hence this Qt object. + + self.destroyed.connect(_Partial(self._closeFileList, self.__openedFiles)) @staticmethod def _closeFileList(fileList): From f34f3076691397236f82b19061a77f212494e372 Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Mon, 15 Jun 2026 16:53:37 +0200 Subject: [PATCH 2/4] apply fix to AbstractDataFileDialog --- src/silx/gui/dialog/AbstractDataFileDialog.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/silx/gui/dialog/AbstractDataFileDialog.py b/src/silx/gui/dialog/AbstractDataFileDialog.py index c2d3580c5e..0b684dc46e 100644 --- a/src/silx/gui/dialog/AbstractDataFileDialog.py +++ b/src/silx/gui/dialog/AbstractDataFileDialog.py @@ -33,7 +33,6 @@ import sys import os import logging -import functools import numpy @@ -42,6 +41,7 @@ from silx.gui.hdf5.Hdf5TreeModel import Hdf5TreeModel from . import utils from .FileTypeComboBox import FileTypeComboBox +from ..._utils import Partial as _Partial import fabio @@ -606,10 +606,15 @@ def _init(self): # to access to the content of the Python object with the `destroyed` # signal cause the Python method was already removed with the QWidget, # while the QObject still exists. + # # We use a static method plus explicit references to objects to # release. The callback do not use any ref to self. - onDestroy = functools.partial(self._closeFileList, self.__openedFiles) - self.destroyed.connect(onDestroy) + # + # Since Python 3.13, `functools.partial` can cause the callback to + # still segfaults, possible because it keeps a reference to the + # current call stack and hence this Qt object. + + self.destroyed.connect(_Partial(self._closeFileList, self.__openedFiles)) @staticmethod def _closeFileList(fileList): From ddeafa4986b298725a6be19da988f7248e21c9e7 Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Mon, 15 Jun 2026 17:17:12 +0200 Subject: [PATCH 3/4] let the partial ignore all arguments --- src/silx/_utils.py | 14 ++++++-------- src/silx/gui/dialog/AbstractDataFileDialog.py | 2 +- src/silx/gui/hdf5/Hdf5TreeModel.py | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/silx/_utils.py b/src/silx/_utils.py index 01c6c31b1e..14c45421b6 100644 --- a/src/silx/_utils.py +++ b/src/silx/_utils.py @@ -39,12 +39,11 @@ def nfs_cache_refresh(dirname: str) -> None: pass -class Partial: +class IgnoreArgPartial: """ - Alternative implementation of ``functools.partial``. - - Stores a callable together with positional and keyword arguments and - invokes it later when called. + Alternative implementation of ``functools.partial`` but the partial + function ignores any arguments. This was done because the signature + is not correct which causes PyQt to pass arguments when it shouldn't. This class was introduced as a workaround for segfaults observed with ``functools.partial`` in Qt/Python object destruction and garbage @@ -61,9 +60,8 @@ def __init__(self, func, *args, **kwargs): self._args = args self._kwargs = kwargs - def __call__(self, *args, **kwargs): - kwargs = {**self._kwargs, **kwargs} - return self._func(*self._args, *args, **kwargs) + def __call__(self, *_, **__): + return self._func(*self._args, **self._kwargs) def __repr__(self): return ( diff --git a/src/silx/gui/dialog/AbstractDataFileDialog.py b/src/silx/gui/dialog/AbstractDataFileDialog.py index 0b684dc46e..97223ee77c 100644 --- a/src/silx/gui/dialog/AbstractDataFileDialog.py +++ b/src/silx/gui/dialog/AbstractDataFileDialog.py @@ -41,7 +41,7 @@ from silx.gui.hdf5.Hdf5TreeModel import Hdf5TreeModel from . import utils from .FileTypeComboBox import FileTypeComboBox -from ..._utils import Partial as _Partial +from ..._utils import IgnoreArgPartial as _Partial import fabio diff --git a/src/silx/gui/hdf5/Hdf5TreeModel.py b/src/silx/gui/hdf5/Hdf5TreeModel.py index a989a8ba53..796b3c8253 100644 --- a/src/silx/gui/hdf5/Hdf5TreeModel.py +++ b/src/silx/gui/hdf5/Hdf5TreeModel.py @@ -39,7 +39,7 @@ from ...io._sliceh5 import DatasetSlice from ...io.url import DataUrl from ..._utils import nfs_cache_refresh as _nfs_cache_refresh -from ..._utils import Partial as _Partial +from ..._utils import IgnoreArgPartial as _Partial import h5py From ee750d5e45842213de9b3aac6018c093758ce06c Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Tue, 16 Jun 2026 17:50:56 +0200 Subject: [PATCH 4/4] do not allow arguments instead of ignoring arguments --- src/silx/_utils.py | 9 +++++---- src/silx/gui/dialog/AbstractDataFileDialog.py | 2 +- src/silx/gui/hdf5/Hdf5TreeModel.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/silx/_utils.py b/src/silx/_utils.py index 14c45421b6..d6a8302324 100644 --- a/src/silx/_utils.py +++ b/src/silx/_utils.py @@ -39,11 +39,12 @@ def nfs_cache_refresh(dirname: str) -> None: pass -class IgnoreArgPartial: +class NoArgPartial: """ Alternative implementation of ``functools.partial`` but the partial - function ignores any arguments. This was done because the signature - is not correct which causes PyQt to pass arguments when it shouldn't. + function does not accept any arguments. This was done because otherwise + the function signature would not be correct which causes PyQt to pass + arguments when it shouldn't. This class was introduced as a workaround for segfaults observed with ``functools.partial`` in Qt/Python object destruction and garbage @@ -60,7 +61,7 @@ def __init__(self, func, *args, **kwargs): self._args = args self._kwargs = kwargs - def __call__(self, *_, **__): + def __call__(self): return self._func(*self._args, **self._kwargs) def __repr__(self): diff --git a/src/silx/gui/dialog/AbstractDataFileDialog.py b/src/silx/gui/dialog/AbstractDataFileDialog.py index 97223ee77c..c7b03cf883 100644 --- a/src/silx/gui/dialog/AbstractDataFileDialog.py +++ b/src/silx/gui/dialog/AbstractDataFileDialog.py @@ -41,7 +41,7 @@ from silx.gui.hdf5.Hdf5TreeModel import Hdf5TreeModel from . import utils from .FileTypeComboBox import FileTypeComboBox -from ..._utils import IgnoreArgPartial as _Partial +from ..._utils import NoArgPartial as _Partial import fabio diff --git a/src/silx/gui/hdf5/Hdf5TreeModel.py b/src/silx/gui/hdf5/Hdf5TreeModel.py index 796b3c8253..5b59166abc 100644 --- a/src/silx/gui/hdf5/Hdf5TreeModel.py +++ b/src/silx/gui/hdf5/Hdf5TreeModel.py @@ -39,7 +39,7 @@ from ...io._sliceh5 import DatasetSlice from ...io.url import DataUrl from ..._utils import nfs_cache_refresh as _nfs_cache_refresh -from ..._utils import IgnoreArgPartial as _Partial +from ..._utils import NoArgPartial as _Partial import h5py