diff --git a/pydra/engine/specs.py b/pydra/engine/specs.py index feeeb9665..cccd272a9 100644 --- a/pydra/engine/specs.py +++ b/pydra/engine/specs.py @@ -999,9 +999,9 @@ def get_value( "that don't return stable hash values for specific object " "types across multiple processes (see bytes_repr() " '"singledispatch "function in pydra/utils/hash.py).' - "You may need to implement a specific `bytes_repr()` " - '"singledispatch overload"s or `__bytes_repr__()` ' - "dunder methods to handle one or more types in " + "You may need to write specific `bytes_repr()` " + "implementations (see `pydra.utils.hash.register_serializer`) or a " + "`__bytes_repr__()` dunder methods to handle one or more types in " "your interface inputs." ) _, split_depth = TypeParser.strip_splits(self.type) diff --git a/pydra/engine/submitter.py b/pydra/engine/submitter.py index dc107a130..cbb4064e7 100644 --- a/pydra/engine/submitter.py +++ b/pydra/engine/submitter.py @@ -228,10 +228,10 @@ async def expand_workflow(self, wf, rerun=False): "that don't return stable hash values for specific object " "types across multiple processes (see bytes_repr() " '"singledispatch "function in pydra/utils/hash.py).' - "You may need to implement a specific `bytes_repr()` " - '"singledispatch overload"s or `__bytes_repr__()` ' - "dunder methods to handle one or more types in " - "your interface inputs." + "You may need to write specific `bytes_repr()` " + "implementations (see `pydra.utils.hash.register_serializer`) " + "or `__bytes_repr__()` dunder methods to handle one " + "or more types in your interface inputs." ) raise RuntimeError(msg) for task in tasks: diff --git a/pydra/utils/__init__.py b/pydra/utils/__init__.py index 7fe4b8595..9008779e2 100644 --- a/pydra/utils/__init__.py +++ b/pydra/utils/__init__.py @@ -1,11 +1 @@ -from pathlib import Path -import platformdirs -from pydra._version import __version__ - -user_cache_dir = Path( - platformdirs.user_cache_dir( - appname="pydra", - appauthor="nipype", - version=__version__, - ) -) +from .misc import user_cache_dir, add_exc_note # noqa: F401 diff --git a/pydra/utils/hash.py b/pydra/utils/hash.py index 4d3c6a74b..6f35da0f7 100644 --- a/pydra/utils/hash.py +++ b/pydra/utils/hash.py @@ -19,7 +19,7 @@ from filelock import SoftFileLock import attrs.exceptions from fileformats.core import FileSet -from . import user_cache_dir +from . import user_cache_dir, add_exc_note logger = logging.getLogger("pydra") @@ -194,10 +194,6 @@ def __contains__(self, object_id): return object_id in self._hashes -class UnhashableError(ValueError): - """Error for objects that cannot be hashed""" - - def hash_function(obj, **kwargs): """Generate hash of object.""" return hash_object(obj, **kwargs).hex() @@ -221,7 +217,17 @@ def hash_object( try: return hash_single(obj, cache) except Exception as e: - raise UnhashableError(f"Cannot hash object {obj!r} due to '{e}'") from e + tp = type(obj) + add_exc_note( + e, + ( + f"and therefore cannot hash `{obj!r}` of type " + f"`{tp.__module__}.{tp.__name__}`. Consider implementing a " + "specific `bytes_repr()`(see pydra.utils.hash.register_serializer) " + "or a `__bytes_repr__()` dunder methods for this type" + ), + ) + raise e def hash_single(obj: object, cache: Cache) -> Hash: diff --git a/pydra/utils/misc.py b/pydra/utils/misc.py new file mode 100644 index 000000000..9a40769c9 --- /dev/null +++ b/pydra/utils/misc.py @@ -0,0 +1,33 @@ +from pathlib import Path +import platformdirs +from pydra._version import __version__ + +user_cache_dir = Path( + platformdirs.user_cache_dir( + appname="pydra", + appauthor="nipype", + version=__version__, + ) +) + + +def add_exc_note(e: Exception, note: str) -> Exception: + """Adds a note to an exception in a Python <3.11 compatible way + + Parameters + ---------- + e : Exception + the exception to add the note to + note : str + the note to add + + Returns + ------- + Exception + returns the exception again + """ + if hasattr(e, "add_note"): + e.add_note(note) + else: + e.args = (e.args[0] + "\n" + note,) + return e diff --git a/pydra/utils/tests/test_hash.py b/pydra/utils/tests/test_hash.py index e63e1c68b..2c74de6e4 100644 --- a/pydra/utils/tests/test_hash.py +++ b/pydra/utils/tests/test_hash.py @@ -11,7 +11,6 @@ from fileformats.text import TextFile from ..hash import ( Cache, - UnhashableError, bytes_repr, hash_object, register_serializer, @@ -386,3 +385,28 @@ def test_persistent_hash_cache_not_dir(text_file): """ with pytest.raises(ValueError, match="not a directory"): PersistentCache(text_file.fspath) + + +def test_unhashable(): + """ + Test that an error is raised if an unhashable object is provided + """ + + class A: + + def __bytes_repr__(self, cache: Cache) -> ty.Generator[bytes, None, None]: + raise TypeError("unhashable") + + def __repr__(self): + return "A()" + + # hash_object(A()) + + with pytest.raises( + TypeError, + match=( + "unhashable\nand therefore cannot hash `A\(\)` of type " + "`pydra.utils.tests.test_hash.A`" + ), + ): + hash_object(A())