You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@burr.apache.org by eb...@apache.org on 2026/06/13 06:03:19 UTC

(burr) branch main updated: fix: #351 make serde errors informative (#806)

This is an automated email from the ASF dual-hosted git repository.

elijahbenizzy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/burr.git


The following commit(s) were added to refs/heads/main by this push:
     new 38f49b61 fix: #351 make serde errors informative (#806)
38f49b61 is described below

commit 38f49b61d84a71d593d18d2376852f3dfcc9f287
Author: adawang <ad...@gmail.com>
AuthorDate: Sat Jun 13 14:03:13 2026 +0800

    fix: #351 make serde errors informative (#806)
    
    Pandas serde now raises a clear ValueError explaining the required pandas_kwargs 'path' entry instead of an opaque TypeError/KeyError; State.serialize/deserialize wrap failures with the offending field name; unknown deserializer keys now list registered keys and hint at the missing registration import.
    
    Signed-off-by: adawang <ad...@gmail.com>
---
 burr/core/serde.py                      |  7 ++++++-
 burr/core/state.py                      | 32 ++++++++++++++++++++------------
 burr/integrations/serde/pandas.py       | 22 ++++++++++++++++++----
 tests/core/test_serde.py                | 10 ++++++++++
 tests/integrations/serde/test_pandas.py | 27 +++++++++++++++++++++++++++
 5 files changed, 81 insertions(+), 17 deletions(-)

diff --git a/burr/core/serde.py b/burr/core/serde.py
index d7f9824e..effdd892 100644
--- a/burr/core/serde.py
+++ b/burr/core/serde.py
@@ -61,7 +61,12 @@ class StringDispatch:
         if key in self.func_map:
             return self.func_map[key](*args, **kwargs)
         else:
-            raise ValueError(f"No function registered for key: {key}")
+            raise ValueError(
+                f"No deserializer registered for key: '{key}'. "
+                f"Registered keys: {sorted(self.func_map)}. "
+                f"Make sure the module registering the deserializer for '{key}' has been "
+                f"imported (e.g. `import burr.integrations.serde.pandas` for 'pandas.DataFrame')."
+            )
 
 
 deserializer = StringDispatch()
diff --git a/burr/core/state.py b/burr/core/state.py
index ad0d96a5..f86c1c78 100644
--- a/burr/core/state.py
+++ b/burr/core/state.py
@@ -328,15 +328,20 @@ class State(Mapping, Generic[StateType]):
 
         def _serialize(k, v, **extrakwargs) -> Union[dict, str]:
             """chooses the correct serde function for the given key and calls it"""
-            if k in FIELD_SERIALIZATION:
-                result = FIELD_SERIALIZATION[k][0](v, **extrakwargs)
-                if not isinstance(result, dict):
-                    raise ValueError(
-                        f"Field serde for {k} must return a dict,"
-                        f" but {FIELD_SERIALIZATION[k][0].__name__} returned {type(result)} ({str(result)[0:10]})."
-                    )
-                return result
-            return serde.serialize(v, **extrakwargs)
+            try:
+                if k in FIELD_SERIALIZATION:
+                    result = FIELD_SERIALIZATION[k][0](v, **extrakwargs)
+                    if not isinstance(result, dict):
+                        raise ValueError(
+                            f"Field serde for {k} must return a dict,"
+                            f" but {FIELD_SERIALIZATION[k][0].__name__} returned {type(result)} ({str(result)[0:10]})."
+                        )
+                    return result
+                return serde.serialize(v, **extrakwargs)
+            except Exception as e:
+                raise ValueError(
+                    f"Failed to serialize state field '{k}' (value of type {type(v).__name__}): {e}"
+                ) from e
 
         return {k: _serialize(k, v, **kwargs) for k, v in _dict.items()}
 
@@ -346,9 +351,12 @@ class State(Mapping, Generic[StateType]):
 
         def _deserialize(k, v: Union[str, dict], **extrakwargs) -> Callable:
             """chooses the correct serde function for the given key and calls it"""
-            if k in FIELD_SERIALIZATION:
-                return FIELD_SERIALIZATION[k][1](v, **extrakwargs)
-            return serde.deserialize(v, **extrakwargs)
+            try:
+                if k in FIELD_SERIALIZATION:
+                    return FIELD_SERIALIZATION[k][1](v, **extrakwargs)
+                return serde.deserialize(v, **extrakwargs)
+            except Exception as e:
+                raise ValueError(f"Failed to deserialize state field '{k}': {e}") from e
 
         return State({k: _deserialize(k, v, **kwargs) for k, v in json_dict.items()})
 
diff --git a/burr/integrations/serde/pandas.py b/burr/integrations/serde/pandas.py
index 62f50259..22ee32b0 100644
--- a/burr/integrations/serde/pandas.py
+++ b/burr/integrations/serde/pandas.py
@@ -18,6 +18,7 @@
 # try to import to serialize Pandas Objects
 import hashlib
 import os
+from typing import Optional
 
 import pandas as pd
 
@@ -25,7 +26,9 @@ from burr.core import serde
 
 
 @serde.serialize.register(pd.DataFrame)
-def serialize_pandas_df(value: pd.DataFrame, pandas_kwargs: dict, **kwargs) -> dict:
+def serialize_pandas_df(
+    value: pd.DataFrame, pandas_kwargs: Optional[dict] = None, **kwargs
+) -> dict:
     """Custom serde for pandas dataframes.
 
     Saves the dataframe to a parquet file and returns the path to the file.
@@ -37,6 +40,15 @@ def serialize_pandas_df(value: pd.DataFrame, pandas_kwargs: dict, **kwargs) -> d
     :param kwargs:
     :return:
     """
+    if not isinstance(pandas_kwargs, dict) or "path" not in pandas_kwargs:
+        raise ValueError(
+            "Serializing a pandas DataFrame requires a `path` entry in `pandas_kwargs` -- "
+            "this is the base path where the dataframe is saved as a parquet file. "
+            "Pass it through whatever triggers serialization, e.g. "
+            '`LocalTrackingClient(..., serde_kwargs={"pandas_kwargs": {"path": "/some/dir"}})` '
+            'or `state.serialize(pandas_kwargs={"path": "/some/dir"})`. '
+            f"Got pandas_kwargs={pandas_kwargs!r}."
+        )
     hash_object = hashlib.sha256()
     hash_value = str(value.columns) + str(value.shape) + str(value.dtypes)
     hash_object.update(hash_value.encode())
@@ -53,15 +65,17 @@ def serialize_pandas_df(value: pd.DataFrame, pandas_kwargs: dict, **kwargs) -> d
 
 
 @serde.deserializer.register("pandas.DataFrame")
-def deserialize_pandas_df(value: dict, pandas_kwargs: dict, **kwargs) -> pd.DataFrame:
+def deserialize_pandas_df(
+    value: dict, pandas_kwargs: Optional[dict] = None, **kwargs
+) -> pd.DataFrame:
     """Custom deserializer for pandas dataframes.
 
     :param value: the dictionary to pull the path from to load the parquet file.
-    :param pandas_kwargs: other args to pass to the pandas read_parquet function.
+    :param pandas_kwargs: other args to pass to the pandas read_parquet function. Optional.
     :param kwargs:
     :return: pandas dataframe
     """
-    kwargs = pandas_kwargs.copy()
+    kwargs = pandas_kwargs.copy() if pandas_kwargs is not None else {}
     if "path" in kwargs:
         # remove this to not clash; we already have the full path.
         kwargs.pop("path")
diff --git a/tests/core/test_serde.py b/tests/core/test_serde.py
index 7e3de390..302a1cdc 100644
--- a/tests/core/test_serde.py
+++ b/tests/core/test_serde.py
@@ -64,3 +64,13 @@ def test_string_dispatch_with_key():
     dispatch = StringDispatch()
     dispatch.register("test_key")(lambda x: x)
     assert dispatch.call("test_key", "test_value") == "test_value"
+
+
+def test_string_dispatch_no_key_informative_message():
+    dispatch = StringDispatch()
+    dispatch.register("known_key")(lambda value: value)
+    with pytest.raises(ValueError) as exc_info:
+        dispatch.call("nonexistent_key")
+    assert "nonexistent_key" in str(exc_info.value)
+    assert "known_key" in str(exc_info.value)
+    assert "imported" in str(exc_info.value)
diff --git a/tests/integrations/serde/test_pandas.py b/tests/integrations/serde/test_pandas.py
index 90879052..90ce9950 100644
--- a/tests/integrations/serde/test_pandas.py
+++ b/tests/integrations/serde/test_pandas.py
@@ -18,6 +18,7 @@
 import os
 
 import pandas as pd
+import pytest
 
 from burr.core import serde, state
 
@@ -41,3 +42,29 @@ def test_serde_of_pandas_dataframe(tmp_path):
     ng = state.State.deserialize(serialized, pandas_kwargs={"path": tmp_path})
     assert isinstance(ng["df"], pd.DataFrame)
     pd.testing.assert_frame_equal(ng["df"], df)
+
+
+def test_serialize_pandas_df_without_pandas_kwargs_raises_informative_error():
+    df = pd.DataFrame({"a": [1, 2, 3]})
+    og = state.State({"df": df})
+    with pytest.raises(ValueError) as exc_info:
+        og.serialize()
+    assert "Failed to serialize state field 'df'" in str(exc_info.value)
+    assert "pandas_kwargs" in str(exc_info.value)
+    assert "path" in str(exc_info.value)
+
+
+def test_serialize_pandas_df_without_path_raises_informative_error():
+    df = pd.DataFrame({"a": [1, 2, 3]})
+    og = state.State({"df": df})
+    with pytest.raises(ValueError) as exc_info:
+        og.serialize(pandas_kwargs={"compression": "snappy"})
+    assert "path" in str(exc_info.value)
+
+
+def test_deserialize_pandas_df_without_pandas_kwargs(tmp_path):
+    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+    og = state.State({"df": df})
+    serialized = og.serialize(pandas_kwargs={"path": tmp_path})
+    ng = state.State.deserialize(serialized)
+    pd.testing.assert_frame_equal(ng["df"], df)