Skip to content

Commit e284b65

Browse files
authored
Merge pull request #734 from tclose/dont-catch-unhashable
Add note instead of catching and raising unhashable exception
2 parents 4e66ab6 + 317392c commit e284b65

File tree

6 files changed

+78
-25
lines changed

6 files changed

+78
-25
lines changed

pydra/engine/specs.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -999,9 +999,9 @@ def get_value(
999999
"that don't return stable hash values for specific object "
10001000
"types across multiple processes (see bytes_repr() "
10011001
'"singledispatch "function in pydra/utils/hash.py).'
1002-
"You may need to implement a specific `bytes_repr()` "
1003-
'"singledispatch overload"s or `__bytes_repr__()` '
1004-
"dunder methods to handle one or more types in "
1002+
"You may need to write specific `bytes_repr()` "
1003+
"implementations (see `pydra.utils.hash.register_serializer`) or a "
1004+
"`__bytes_repr__()` dunder methods to handle one or more types in "
10051005
"your interface inputs."
10061006
)
10071007
_, split_depth = TypeParser.strip_splits(self.type)

pydra/engine/submitter.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,10 @@ async def expand_workflow(self, wf, rerun=False):
228228
"that don't return stable hash values for specific object "
229229
"types across multiple processes (see bytes_repr() "
230230
'"singledispatch "function in pydra/utils/hash.py).'
231-
"You may need to implement a specific `bytes_repr()` "
232-
'"singledispatch overload"s or `__bytes_repr__()` '
233-
"dunder methods to handle one or more types in "
234-
"your interface inputs."
231+
"You may need to write specific `bytes_repr()` "
232+
"implementations (see `pydra.utils.hash.register_serializer`) "
233+
"or `__bytes_repr__()` dunder methods to handle one "
234+
"or more types in your interface inputs."
235235
)
236236
raise RuntimeError(msg)
237237
for task in tasks:

pydra/utils/__init__.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1 @@
1-
from pathlib import Path
2-
import platformdirs
3-
from pydra._version import __version__
4-
5-
user_cache_dir = Path(
6-
platformdirs.user_cache_dir(
7-
appname="pydra",
8-
appauthor="nipype",
9-
version=__version__,
10-
)
11-
)
1+
from .misc import user_cache_dir, add_exc_note # noqa: F401

pydra/utils/hash.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from filelock import SoftFileLock
2020
import attrs.exceptions
2121
from fileformats.core import FileSet
22-
from . import user_cache_dir
22+
from . import user_cache_dir, add_exc_note
2323

2424
logger = logging.getLogger("pydra")
2525

@@ -194,10 +194,6 @@ def __contains__(self, object_id):
194194
return object_id in self._hashes
195195

196196

197-
class UnhashableError(ValueError):
198-
"""Error for objects that cannot be hashed"""
199-
200-
201197
def hash_function(obj, **kwargs):
202198
"""Generate hash of object."""
203199
return hash_object(obj, **kwargs).hex()
@@ -221,7 +217,17 @@ def hash_object(
221217
try:
222218
return hash_single(obj, cache)
223219
except Exception as e:
224-
raise UnhashableError(f"Cannot hash object {obj!r} due to '{e}'") from e
220+
tp = type(obj)
221+
add_exc_note(
222+
e,
223+
(
224+
f"and therefore cannot hash `{obj!r}` of type "
225+
f"`{tp.__module__}.{tp.__name__}`. Consider implementing a "
226+
"specific `bytes_repr()`(see pydra.utils.hash.register_serializer) "
227+
"or a `__bytes_repr__()` dunder methods for this type"
228+
),
229+
)
230+
raise e
225231

226232

227233
def hash_single(obj: object, cache: Cache) -> Hash:

pydra/utils/misc.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from pathlib import Path
2+
import platformdirs
3+
from pydra._version import __version__
4+
5+
user_cache_dir = Path(
6+
platformdirs.user_cache_dir(
7+
appname="pydra",
8+
appauthor="nipype",
9+
version=__version__,
10+
)
11+
)
12+
13+
14+
def add_exc_note(e: Exception, note: str) -> Exception:
15+
"""Adds a note to an exception in a Python <3.11 compatible way
16+
17+
Parameters
18+
----------
19+
e : Exception
20+
the exception to add the note to
21+
note : str
22+
the note to add
23+
24+
Returns
25+
-------
26+
Exception
27+
returns the exception again
28+
"""
29+
if hasattr(e, "add_note"):
30+
e.add_note(note)
31+
else:
32+
e.args = (e.args[0] + "\n" + note,)
33+
return e

pydra/utils/tests/test_hash.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from fileformats.text import TextFile
1212
from ..hash import (
1313
Cache,
14-
UnhashableError,
1514
bytes_repr,
1615
hash_object,
1716
register_serializer,
@@ -386,3 +385,28 @@ def test_persistent_hash_cache_not_dir(text_file):
386385
"""
387386
with pytest.raises(ValueError, match="not a directory"):
388387
PersistentCache(text_file.fspath)
388+
389+
390+
def test_unhashable():
391+
"""
392+
Test that an error is raised if an unhashable object is provided
393+
"""
394+
395+
class A:
396+
397+
def __bytes_repr__(self, cache: Cache) -> ty.Generator[bytes, None, None]:
398+
raise TypeError("unhashable")
399+
400+
def __repr__(self):
401+
return "A()"
402+
403+
# hash_object(A())
404+
405+
with pytest.raises(
406+
TypeError,
407+
match=(
408+
"unhashable\nand therefore cannot hash `A\(\)` of type "
409+
"`pydra.utils.tests.test_hash.A`"
410+
),
411+
):
412+
hash_object(A())

0 commit comments

Comments
 (0)