Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/silx/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,36 @@ def nfs_cache_refresh(dirname: str) -> None:
_ = entry.stat()
except Exception:
pass


class IgnoreArgPartial:
"""
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
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, *_, **__):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hesitating between

  • IgnoreArgPartial ignore all arguments: __call__(self, *_, **__)
  • NoArgPartial do not accept any arguments: __call__(self)

Perhaps the second is a better pattern?

@t20100 t20100 Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for NoArgPartial

return self._func(*self._args, **self._kwargs)

def __repr__(self):
return (
f"{type(self).__name__}("
f"{self._func!r}, "
f"args={self._args!r}, "
f"kwargs={self._kwargs!r})"
)
11 changes: 8 additions & 3 deletions src/silx/gui/dialog/AbstractDataFileDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import sys
import os
import logging
import functools

import numpy

Expand All @@ -42,6 +41,7 @@
from silx.gui.hdf5.Hdf5TreeModel import Hdf5TreeModel
from . import utils
from .FileTypeComboBox import FileTypeComboBox
from ..._utils import IgnoreArgPartial as _Partial

import fabio

Expand Down Expand Up @@ -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):
Expand Down
11 changes: 8 additions & 3 deletions src/silx/gui/hdf5/Hdf5TreeModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

import os
import logging
import functools
from .. import qt
from .. import icons
from .Hdf5Node import Hdf5Node
Expand All @@ -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 IgnoreArgPartial as _Partial

import h5py

Expand Down Expand Up @@ -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):
Expand Down
Loading