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

yihua pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hudi-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 453bb426 fix(python): register HudiCoreError so callers can catch it 
(#915)
453bb426 is described below

commit 453bb4264b00384397bd18954e177e5eb60e12df
Author: Y Ethan Guo <[email protected]>
AuthorDate: Fri Oct 30 09:10:01 2026 -0700

    fix(python): register HudiCoreError so callers can catch it (#915)
---
 python/hudi/__init__.py             |  2 ++
 python/hudi/_internal.pyi           |  3 +++
 python/src/internal.rs              |  2 +-
 python/src/lib.rs                   |  4 ++++
 python/tests/test_streaming_read.py | 11 ++++++-----
 python/tests/test_table_misc.py     | 21 +++++++++++++++++----
 python/tests/test_table_write.py    | 20 +++++++++++++-------
 7 files changed, 46 insertions(+), 17 deletions(-)

diff --git a/python/hudi/__init__.py b/python/hudi/__init__.py
index 81c10e94..413502e2 100644
--- a/python/hudi/__init__.py
+++ b/python/hudi/__init__.py
@@ -18,6 +18,7 @@
 
 from hudi._config import HudiPlanConfig, HudiReadConfig, HudiTableConfig
 from hudi._internal import (
+    HudiCoreError,
     HudiDataFusionDataSource,
     HudiFileGroupReader,
     HudiFileSlice,
@@ -33,6 +34,7 @@ from hudi._internal import __version__ as __version__
 from hudi.table.builder import HudiTableBuilder
 
 __all__ = [
+    "HudiCoreError",
     "HudiDataFusionDataSource",
     "HudiFileGroupReader",
     "HudiFileSlice",
diff --git a/python/hudi/_internal.pyi b/python/hudi/_internal.pyi
index dfb7ae24..3536daa2 100644
--- a/python/hudi/_internal.pyi
+++ b/python/hudi/_internal.pyi
@@ -35,6 +35,9 @@ WriteData = Union[
 
 def _config_keys() -> Dict[str, List[Tuple[str, str]]]: ...
 
+class HudiCoreError(Exception):
+    """Raised for errors from the Hudi core library."""
+
 class HudiQueryType:
     """Selects the read semantic on :class:`HudiReadOptions`."""
 
diff --git a/python/src/internal.rs b/python/src/internal.rs
index 9d807362..80de956c 100644
--- a/python/src/internal.rs
+++ b/python/src/internal.rs
@@ -62,7 +62,7 @@ use std::error::Error;
 
 type RecordBatchBoxStream = BoxStream<'static, HudiResult<RecordBatch>>;
 
-create_exception!(_internal, HudiCoreError, PyException);
+create_exception!(hudi._internal, HudiCoreError, PyException);
 
 fn convert_to_py_err<I>(err: I) -> PyErr
 where
diff --git a/python/src/lib.rs b/python/src/lib.rs
index aa796387..0e366353 100644
--- a/python/src/lib.rs
+++ b/python/src/lib.rs
@@ -44,6 +44,10 @@ fn _internal(_py: Python, m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_class::<HudiTable>()?;
     m.add_class::<HudiTimeline>()?;
     m.add_class::<HudiWriteResult>()?;
+    m.add(
+        "HudiCoreError",
+        m.py().get_type::<internal::HudiCoreError>(),
+    )?;
 
     m.add_function(wrap_pyfunction!(internal::_config_keys, m)?)?;
 
diff --git a/python/tests/test_streaming_read.py 
b/python/tests/test_streaming_read.py
index cdce16ba..6beaaa0d 100644
--- a/python/tests/test_streaming_read.py
+++ b/python/tests/test_streaming_read.py
@@ -21,6 +21,7 @@ import pyarrow as pa
 import pytest
 
 from hudi import (
+    HudiCoreError,
     HudiFileGroupReader,
     HudiQueryType,
     HudiReadOptions,
@@ -66,7 +67,7 @@ def test_read_options_typed_accessors_round_trip():
 def test_read_options_query_type_bad_value_raises():
     """A bogus value left in hudi_options under the query-type key surfaces as 
an error."""
     bogus = HudiReadOptions(hudi_options={"hoodie.read.query.type": "bogus"})
-    with pytest.raises(Exception):
+    with pytest.raises(HudiCoreError):
         bogus.query_type()
 
 
@@ -77,20 +78,20 @@ def test_read_options_batch_size_bad_value_raises():
     bogus = HudiReadOptions(
         hudi_options={"hoodie.read.stream.batch_size": "not_a_number"}
     )
-    with pytest.raises(Exception):
+    with pytest.raises(HudiCoreError):
         bogus.batch_size()
 
-    with pytest.raises(Exception, match="must be > 0"):
+    with pytest.raises(HudiCoreError, match="must be > 0"):
         HudiReadOptions().with_batch_size(0)
 
 
 def test_read_options_with_filters_validates_at_build_time():
     """`with_filters` parses + cardinality-validates upfront, so a bad 
operator or
     empty IN value list raises here rather than at read time."""
-    with pytest.raises(Exception):
+    with pytest.raises(HudiCoreError):
         HudiReadOptions().with_filters([("col", "BAD_OP", "x")])
 
-    with pytest.raises(Exception, match="at least one value"):
+    with pytest.raises(HudiCoreError, match="at least one value"):
         HudiReadOptions().with_filters([("col", "IN", "")])
 
 
diff --git a/python/tests/test_table_misc.py b/python/tests/test_table_misc.py
index 4819de97..f2a3e370 100644
--- a/python/tests/test_table_misc.py
+++ b/python/tests/test_table_misc.py
@@ -17,10 +17,25 @@
 
 """Tests for HudiTable accessor and stats APIs."""
 
-from hudi import HudiFileGroupReader, HudiQueryType, HudiReadOptions, HudiTable
+import pytest
+
+from hudi import (
+    HudiCoreError,
+    HudiFileGroupReader,
+    HudiQueryType,
+    HudiReadOptions,
+    HudiTable,
+)
 from hudi._internal import get_test_table_path
 
 
+def test_core_errors_raise_hudi_core_error(tmp_path):
+    assert issubclass(HudiCoreError, Exception)
+    assert HudiCoreError.__module__ == "hudi._internal"
+    with pytest.raises(HudiCoreError):
+        HudiTable(str(tmp_path / "missing"))
+
+
 def test_table_metadata_getters(v8_trips_table):
     """Table-level getters round-trip the values from hoodie.properties."""
     table = HudiTable(v8_trips_table)
@@ -141,10 +156,8 @@ def test_read_dispatches_on_query_type(v8_trips_table):
 
 
 def test_read_stream_errors_on_incremental(v8_trips_table):
-    import pytest
-
     table = HudiTable(v8_trips_table)
-    with pytest.raises(Exception, match="not yet supported"):
+    with pytest.raises(HudiCoreError, match="not yet supported"):
         
table.read_stream(HudiReadOptions().with_query_type(HudiQueryType.Incremental))
 
 
diff --git a/python/tests/test_table_write.py b/python/tests/test_table_write.py
index 7ce5b1a5..2e3a14d9 100644
--- a/python/tests/test_table_write.py
+++ b/python/tests/test_table_write.py
@@ -24,7 +24,13 @@ from decimal import Decimal
 import pyarrow as pa
 import pytest
 
-from hudi import HudiQueryType, HudiReadOptions, HudiTable, HudiWriteResult
+from hudi import (
+    HudiCoreError,
+    HudiQueryType,
+    HudiReadOptions,
+    HudiTable,
+    HudiWriteResult,
+)
 
 SCHEMA = pa.schema(
     [
@@ -128,7 +134,7 @@ def test_create_rejects_bad_table_type(tmp_path):
 
 def test_create_on_existing_table_raises(tmp_path):
     HudiTable.create(str(tmp_path / "t"), record_key="id")
-    with pytest.raises(Exception, match="already exists"):
+    with pytest.raises(HudiCoreError, match="already exists"):
         HudiTable.create(str(tmp_path / "t"), record_key="id")
 
 
@@ -149,7 +155,7 @@ def test_create_on_existing_table_raises(tmp_path):
     ],
 )
 def test_create_rejects_inconsistent_index_options(tmp_path, flags, error):
-    with pytest.raises(Exception, match=error):
+    with pytest.raises(HudiCoreError, match=error):
         HudiTable.create(str(tmp_path / "t"), record_key="id", **flags)
     assert not (tmp_path / "t").exists()
 
@@ -301,7 +307,7 @@ def test_update_set_batches_must_hold_one_row(tmp_path):
     with pytest.raises(ValueError, match="got no batches"):
         table.update("id = 't1'", [])
     two = [pa.record_batch({"fare": [1.0]}), pa.record_batch({"fare": [2.0]})]
-    with pytest.raises(Exception, match="single-row"):
+    with pytest.raises(HudiCoreError, match="single-row"):
         table.update("id = 't1'", two)
     assert snapshot(table)["t1"] == ("sf", 1, 30.0)
 
@@ -744,11 +750,11 @@ def test_write_errors_leave_the_table_unchanged(tmp_path):
     timeline = tmp_path / "trips" / ".hoodie" / "timeline"
     instants = sorted(p.name for p in timeline.iterdir())
     no_key = pa.table({"city": ["sf"], "ts": [2], "fare": [1.0]})
-    with pytest.raises(Exception, match="schema does not match"):
+    with pytest.raises(HudiCoreError, match="schema does not match"):
         table.upsert(no_key)
-    with pytest.raises(Exception, match="nope"):
+    with pytest.raises(HudiCoreError, match="nope"):
         table.upsert(trips(("t1", "sf", 2, 1.0)), update_columns=["nope"])
-    with pytest.raises(Exception, match="'no_such_column' not found"):
+    with pytest.raises(HudiCoreError, match="'no_such_column' not found"):
         table.delete("no_such_column = 1")
     assert sorted(p.name for p in timeline.iterdir()) == instants
     assert snapshot(table)["t1"] == ("sf", 1, 30.0)

Reply via email to