You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by as...@apache.org on 2026/04/30 14:54:23 UTC

(opendal) branch main updated: fix(bindings/python): add RateLimited and RangeNotSatisfied exception kinds (#7437)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 5207106d9 fix(bindings/python): add RateLimited and RangeNotSatisfied exception kinds (#7437)
5207106d9 is described below

commit 5207106d9508591390b57940528c0f3cd241c338
Author: TennyZhuang <zt...@gmail.com>
AuthorDate: Thu Apr 30 22:54:17 2026 +0800

    fix(bindings/python): add RateLimited and RangeNotSatisfied exception kinds (#7437)
    
    * fix(bindings/python): add RateLimited and RangeNotSatisfied exception kinds
    
    Two ErrorKind variants introduced in Rust core — RateLimited and
    RangeNotSatisfied — were not mapped in the Python bindings. Both were
    silently swallowed by the catch-all `_ => Unexpected::new_err(e)` in
    format_pyerr_impl, making it impossible for Python callers to distinguish
    rate-limit responses (e.g. S3 429) or out-of-range reads from generic
    unexpected errors.
    
    This commit adds:
    - create_exception! declarations for RateLimited and RangeNotSatisfied
    - Explicit arms in format_pyerr_impl for both new kinds
    - Registration in the exceptions module in lib.rs
    - .pyi stub entries (alphabetically sorted)
    - Tests verifying presence, raisability, and mutual distinctness of the
      new exception classes
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
    
    * fix(bindings/python): fix ruff EM101 linting errors in test_exceptions.py
    
    Assign exception message strings to variables before raising to comply
    with ruff EM101 rule (no string literals in raise statements).
    
    * fix(bindings/python): fix cargo fmt formatting in errors.rs and lib.rs
    
    - Convert RateLimited create_exception! macro to single-line format
      to match existing short-doc patterns (NotFound, IsSameFile)
    - Add trailing comma after RangeNotSatisfied in add_pyexceptions!
      macro array in lib.rs (required by edition=2024 rustfmt)
    
    * fix(bindings/python): fix ruff PT012 and remove unrelated release_java.yml change
    
    - Move variable assignments outside pytest.raises blocks to comply
      with PT012 (single simple statement per raises block)
    - Replace try/except distinctness test with direct issubclass checks,
      which is clearer and avoids PT012 violations
    - Revert unrelated -DskipStagingRepositoryClose=true change in
      release_java.yml that was not part of this PR
    
    ---------
    
    Co-authored-by: tianyizhuang <ti...@tianyizhuangs-Mac-mini.local>
    Co-authored-by: Claude Sonnet 4.6 <no...@anthropic.com>
---
 bindings/python/python/opendal/exceptions.pyi |  6 ++++
 bindings/python/src/errors.rs                 |  9 +++++
 bindings/python/src/lib.rs                    |  4 ++-
 bindings/python/tests/test_exceptions.py      | 48 +++++++++++++++++++++++++++
 4 files changed, 66 insertions(+), 1 deletion(-)

diff --git a/bindings/python/python/opendal/exceptions.pyi b/bindings/python/python/opendal/exceptions.pyi
index 8a1e21e57..806ebfba0 100644
--- a/bindings/python/python/opendal/exceptions.pyi
+++ b/bindings/python/python/opendal/exceptions.pyi
@@ -47,6 +47,12 @@ class NotFound(builtins.Exception):
 class PermissionDenied(builtins.Exception):
     r"""Permission denied."""
 
+class RateLimited(builtins.Exception):
+    r"""Rate limited."""
+
+class RangeNotSatisfied(builtins.Exception):
+    r"""Range not satisfied."""
+
 class Unexpected(builtins.Exception):
     r"""Unexpected errors."""
 
diff --git a/bindings/python/src/errors.rs b/bindings/python/src/errors.rs
index 4601b59b7..2bf65fe89 100644
--- a/bindings/python/src/errors.rs
+++ b/bindings/python/src/errors.rs
@@ -77,6 +77,13 @@ create_exception!(
     PyException,
     "Condition not match"
 );
+create_exception!(opendal.exceptions, RateLimited, PyException, "Rate limited");
+create_exception!(
+    opendal.exceptions,
+    RangeNotSatisfied,
+    PyException,
+    "Range not satisfied"
+);
 
 fn format_pyerr_impl(err: &ocore::Error) -> PyErr {
     let e = format!("{err:?}");
@@ -91,6 +98,8 @@ fn format_pyerr_impl(err: &ocore::Error) -> PyErr {
         ocore::ErrorKind::AlreadyExists => AlreadyExists::new_err(e),
         ocore::ErrorKind::IsSameFile => IsSameFile::new_err(e),
         ocore::ErrorKind::ConditionNotMatch => ConditionNotMatch::new_err(e),
+        ocore::ErrorKind::RateLimited => RateLimited::new_err(e),
+        ocore::ErrorKind::RangeNotSatisfied => RangeNotSatisfied::new_err(e),
         _ => Unexpected::new_err(e),
     }
 }
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 13c6eccaa..bf413eb75 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -97,7 +97,9 @@ fn _opendal(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
             NotADirectory,
             AlreadyExists,
             IsSameFile,
-            ConditionNotMatch
+            ConditionNotMatch,
+            RateLimited,
+            RangeNotSatisfied,
         ]
     )?;
     Ok(())
diff --git a/bindings/python/tests/test_exceptions.py b/bindings/python/tests/test_exceptions.py
index f07cbeabf..82bcb2147 100644
--- a/bindings/python/tests/test_exceptions.py
+++ b/bindings/python/tests/test_exceptions.py
@@ -18,6 +18,8 @@
 import builtins
 import inspect
 
+import pytest
+
 from opendal import exceptions
 
 
@@ -25,3 +27,49 @@ def test_exceptions():
     for _name, obj in inspect.getmembers(exceptions):
         if inspect.isclass(obj):
             assert issubclass(obj, builtins.Exception)
+
+
+def test_all_expected_exceptions_present():
+    """Verify every expected exception class is present in opendal.exceptions."""
+    expected = [
+        "Error",
+        "Unexpected",
+        "Unsupported",
+        "ConfigInvalid",
+        "NotFound",
+        "PermissionDenied",
+        "IsADirectory",
+        "NotADirectory",
+        "AlreadyExists",
+        "IsSameFile",
+        "ConditionNotMatch",
+        "RateLimited",
+        "RangeNotSatisfied",
+    ]
+    for name in expected:
+        assert hasattr(exceptions, name), f"exceptions.{name} is missing"
+        cls = getattr(exceptions, name)
+        assert inspect.isclass(cls), f"exceptions.{name} is not a class"
+        assert issubclass(cls, builtins.Exception), (
+            f"exceptions.{name} does not inherit from Exception"
+        )
+
+
+def test_rate_limited_is_catchable():
+    """RateLimited can be raised and caught like a standard exception."""
+    msg = "too many requests"
+    with pytest.raises(exceptions.RateLimited):
+        raise exceptions.RateLimited(msg)
+
+
+def test_range_not_satisfied_is_catchable():
+    """RangeNotSatisfied can be raised and caught like a standard exception."""
+    msg = "requested range not satisfiable"
+    with pytest.raises(exceptions.RangeNotSatisfied):
+        raise exceptions.RangeNotSatisfied(msg)
+
+
+def test_exceptions_are_distinct():
+    """RateLimited and RangeNotSatisfied must not accidentally catch each other."""
+    assert not issubclass(exceptions.RateLimited, exceptions.RangeNotSatisfied)
+    assert not issubclass(exceptions.RangeNotSatisfied, exceptions.RateLimited)