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)