Repository: silx-kit/silx (python)

Issue:
silx.io.h5py_utils does not catch some exceptions with libhdf5 v1.14.x
There's been a change of error message in libhdf5 `H5Oopen`:
* 1.12.x (version embedded h5py wheel <=3.9): `"Unable to open object"`
* 1.14.x (version embedded h5py wheel >=3.10): `"Unable to synchronously open object"`

This makes the retry machinery to fail.
To reproduce with 2 ipython sessions:

```
In [1]: import h5py
In [2]: f = h5py.File("test.h5", "w")
In [3]: f.flush()
```

```
In [1]: import h5py; from silx.io import h5py_utils
In [2]: @h5py_utils.retry(retry_timeout=1, retry_period=0.1)
   ...: def read():
   ...:     with h5py.File("test.h5", locking=False) as f:
   ...:         print("try read data")
   ...:         return f["data"][()]
   ...: 
In [3]: read()
```
-> `KeyError: "Unable to synchronously open object (object 'data' doesn't exist)"`

This works as expected with h5py 3.9

Pull request description:
silx.io.h5py_utils: Fixed support of  libhdf5 1.14.x
<!-- Thank you for your pull request! -->

Checklist:

- [x] The PR title is formatted as: `<Module or Topic>: <Action> <Summary>` (see [contributing guidelines](https://github.com/silx-kit/silx/blob/main/CONTRIBUTING.rst#pull-request-title-format))

<hr>

<!-- provide a description of the changes below -->

This PR fixes the detection of hdf5-related exceptions in `h5py_utils`'s `retry`.
The error message has changed in libhdf5 and some exceptions were no longer interpreted correctly.

closes #4241

Patch:
diff --git a/src/silx/io/h5py_utils.py b/src/silx/io/h5py_utils.py
index f28d130ba..6ee6bda55 100644
--- a/src/silx/io/h5py_utils.py
+++ b/src/silx/io/h5py_utils.py
@@ -1,5 +1,5 @@
 # /*##########################################################################
-# Copyright (C) 2016-2024 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2025 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
@@ -146,7 +146,11 @@ def retry_h5py_error(e):
         elif isinstance(e, KeyError):
             # For example this needs to be retried:
             # KeyError: 'Unable to open object (bad object header version number)'
-            return "Unable to open object" in str(e)
+            message = str(e)
+            return (
+                "Unable to open object" in message
+                or "Unable to synchronously open object" in message
+            )
     elif isinstance(e, retry_mod.RetryError):
         return True
     return False


Test patch:
diff --git a/src/silx/io/test/test_h5py_utils.py b/src/silx/io/test/test_h5py_utils.py
index 276814d6b..89157e14c 100644
--- a/src/silx/io/test/test_h5py_utils.py
+++ b/src/silx/io/test/test_h5py_utils.py
@@ -1,5 +1,5 @@
 # /*##########################################################################
-# Copyright (C) 2016-2024 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2025 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
@@ -37,6 +37,9 @@ import tempfile
 import multiprocessing
 from contextlib import contextmanager
 
+import h5py
+import pytest
+
 from .. import h5py_utils
 from ...utils.retry import RetryError, RetryTimeoutError
 
@@ -486,3 +489,30 @@ class TestH5pyUtils(unittest.TestCase):
         kw = {"retry_timeout": insufficient_timeout}
         with self.assertRaises(RetryTimeoutError):
             list(iter_data(filename, "/check", **kw))
+
+
+def test_retry_timeout(tmp_path):
+    filepath = tmp_path / "test.h5"
+    with h5py.File(filepath, "w"):
+        pass
+
+    @h5py_utils.retry(retry_timeout=1, retry_period=0.1)
+    def read():
+        with h5py.File(filepath, locking=False) as f:
+            group = f["group"]
+            return group["data"][()]
+
+    with pytest.raises(RetryTimeoutError):
+        read()
+
+    with h5py.File(filepath, "w") as f:
+        f.create_group("group")
+
+    with pytest.raises(RetryTimeoutError):
+        read()
+
+    with h5py.File(filepath, "w") as f:
+        f["group/data"] = 1
+
+    data = read()
+    assert data == 1