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)