This is an automated email from the ASF dual-hosted git repository.
xuanwo 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 9b11e7fa1 feat(bindings/python): Add stubs for Exception and
Capability (#6690)
9b11e7fa1 is described below
commit 9b11e7fa1c153ccff3abb3deb9b9bfdb954e0192
Author: Chitral Verma <[email protected]>
AuthorDate: Sat Oct 18 10:37:15 2025 +0530
feat(bindings/python): Add stubs for Exception and Capability (#6690)
* Add stub-gen
* Stubs for exceptions and capability
---
bindings/python/Cargo.toml | 8 +-
bindings/python/justfile | 21 +++-
bindings/python/python/opendal/__init__.pyi | 128 +------------------
bindings/python/python/opendal/capability.pyi | 171 ++++++++++++++++++++++++++
bindings/python/python/opendal/exceptions.pyi | 49 ++++----
bindings/python/ruff.toml | 2 +-
bindings/python/src/bin/stub_gen.rs | 8 ++
bindings/python/src/capability.rs | 58 +++++----
bindings/python/src/errors.rs | 42 +++++--
bindings/python/src/lib.rs | 42 ++++---
bindings/python/src/utils.rs | 42 +++++++
bindings/python/tests/test_exceptions.py | 4 +-
12 files changed, 364 insertions(+), 211 deletions(-)
diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml
index d20de2329..3ac16216a 100644
--- a/bindings/python/Cargo.toml
+++ b/bindings/python/Cargo.toml
@@ -152,9 +152,14 @@ services-yandex-disk = ["opendal/services-yandex-disk"]
# we build cp311-abi3 and cp310 wheels now, move this to pyo3 after we drop
cp310
abi3 = ["pyo3/abi3-py311"]
+[[bin]]
+doc = false
+name = "stub_gen"
+
[lib]
-crate-type = ["cdylib"]
+crate-type = ["cdylib", "rlib"]
doc = false
+name = "_opendal"
[dependencies]
bytes = "1.5.0"
@@ -168,6 +173,7 @@ opendal = { version = ">=0", path = "../../core", features
= [
] }
pyo3 = { version = "0.26.0", features = ["generate-import-lib", "jiff-02"] }
pyo3-async-runtimes = { version = "0.26.0", features = ["tokio-runtime"] }
+pyo3-stub-gen = { version = "0.15" }
tokio = "1"
[target.'cfg(unix)'.dependencies.opendal]
diff --git a/bindings/python/justfile b/bindings/python/justfile
index cbc8ac64a..f1e152f0d 100644
--- a/bindings/python/justfile
+++ b/bindings/python/justfile
@@ -32,7 +32,7 @@ set ignore-comments := true
[group('maintenance')]
setup:
@echo "{{ BOLD }}--- Installing/ validating dependencies ---{{ NORMAL }}"
- @uv sync --managed-python --all-groups --all-extras --compile-bytecode
--upgrade
+ @uv sync --managed-python --all-groups --all-extras --compile-bytecode
# Clean up all caches, build artifacts, and the venv
[group('maintenance')]
@@ -50,16 +50,25 @@ clean:
# Dev & Build
#
==============================================================================
+# Generate Python type stubs
+[group('build')]
+stub-gen: setup
+ @echo "{{ BOLD }}--- Generating Python type stubs ---{{ NORMAL }}"
+ @cargo run --quiet --bin stub_gen
+ @echo "{{ BOLD }}--- Formatting and fixing generated stubs ---{{ NORMAL }}"
+ -@bash -c 'shopt -s globstar; uv run ruff check **/*.pyi --fix
--unsafe-fixes --silent || true'
+ @just fmt
+
# Compile and produce a release wheel with optimizations
[group('release')]
-build-release *args: setup
+build-release *args: stub-gen
@echo "{{ BOLD }}--- Building release wheel ---{{ NORMAL }}"
@uv run maturin build -m ./Cargo.toml --profile release --strip --release
--out dist {{ args }}
@uv run mkdocs build
# Build and install the release wheel in the current venv
[group('release')]
-install-release *args: setup
+install-release *args: stub-gen
@echo "{{ BOLD }}--- Installing release wheel ---{{ NORMAL }}"
@uv run maturin develop -m ./Cargo.toml --profile release --strip
--release {{ args }}
@@ -73,20 +82,20 @@ bench: install-release
# Build only a source distribution (sdist) without compiling
[group('release')]
-sdist *args: setup
+sdist *args: stub-gen
@echo "{{ BOLD }}--- Building source distribution without compiling ---{{
NORMAL }}"
@uv run maturin sdist -m ./Cargo.toml --out dist {{ args }}
# Compile and produce a development wheel
[group('dev')]
-build-dev *args: setup
+build-dev *args: stub-gen
@echo "{{ BOLD }}--- Building development wheel ---{{ NORMAL }}"
@uv run maturin build -m ./Cargo.toml --out dist {{ args }}
@uv run mkdocs build
# Build and install the development wheel in the current venv
[group('dev')]
-install-dev *args: setup
+install-dev *args: stub-gen
@echo "{{ BOLD }}--- Installing development wheel ---{{ NORMAL }}"
@uv run maturin develop -m ./Cargo.toml {{ args }}
diff --git a/bindings/python/python/opendal/__init__.pyi
b/bindings/python/python/opendal/__init__.pyi
index 0e35e3410..825b1f6d6 100644
--- a/bindings/python/python/opendal/__init__.pyi
+++ b/bindings/python/python/opendal/__init__.pyi
@@ -28,6 +28,7 @@ except ImportError:
from opendal import exceptions as exceptions
from opendal import layers as layers
from opendal.__base import _Base
+from opendal.capability import Capability
from opendal.layers import Layer
PathBuf: TypeAlias = str | os.PathLike
@@ -765,130 +766,3 @@ class PresignedRequest:
def method(self) -> str: ...
@property
def headers(self) -> dict[str, str]: ...
-
-@final
-class Capability:
- """Storage capability information."""
-
- stat: bool
- """If operator supports stat."""
-
- stat_with_if_match: bool
- """If operator supports stat with if match."""
-
- stat_with_if_none_match: bool
- """If operator supports stat with if none match."""
-
- read: bool
- """Indicates if the operator supports read operations."""
-
- read_with_if_match: bool
- """Indicates if conditional read operations using If-Match are
supported."""
-
- read_with_if_none_match: bool
- """Indicates if conditional read operations using If-None-Match are
supported."""
-
- read_with_if_modified_since: bool
- """If-Modified-Since condition supported for read."""
-
- read_with_if_unmodified_since: bool
- """If-Unmodified-Since condition supported for read."""
-
- read_with_override_cache_control: bool
- """Cache-Control header override supported for read."""
-
- read_with_override_content_disposition: bool
- """Content-Disposition header override supported for read."""
-
- read_with_override_content_type: bool
- """Indicates if Content-Type header override is supported during read
operations."""
-
- read_with_version: bool
- """Indicates if versions read operations are supported."""
-
- write: bool
- """Indicates if the operator supports write operations."""
-
- write_can_multi: bool
- """Indicates if multiple write operations can be performed on the same
object."""
-
- write_can_empty: bool
- """Indicates if writing empty content is supported."""
-
- write_can_append: bool
- """Indicates if append operations are supported."""
-
- write_with_content_type: bool
- """Indicates if Content-Type can be specified during write operations."""
-
- write_with_content_disposition: bool
- """Indicates if Content-Disposition can be specified during write
operations."""
-
- write_with_content_encoding: bool
- """Indicates if Content-Encoding can be specified during write
operations."""
-
- write_with_cache_control: bool
- """Indicates if Cache-Control can be specified during write operations."""
-
- write_with_if_match: bool
- """Indicates if conditional write operations using If-Match are
supported."""
-
- write_with_if_none_match: bool
- """Indicates if conditional write operations using If-None-Match are
supported."""
-
- write_with_if_not_exists: bool
- """Indicates if write operations can be conditional on object
non-existence."""
-
- write_with_user_metadata: bool
- """Indicates if custom user metadata can be attached during write
operations."""
-
- write_multi_max_size: int | None
- """Maximum part size for multipart uploads (e.g. 5GiB for AWS S3)."""
-
- write_multi_min_size: int | None
- """Minimum part size for multipart uploads (e.g. 5MiB for AWS S3)."""
-
- write_total_max_size: int | None
- """Maximum total size for write operations (e.g. 1MB for Cloudflare D1)."""
-
- create_dir: bool
- """If operator supports create dir."""
-
- delete: bool
- """If operator supports delete."""
-
- copy: bool
- """If operator supports copy."""
-
- rename: bool
- """If operator supports rename."""
-
- list: bool
- """If operator supports list."""
-
- list_with_limit: bool
- """If backend supports list with limit."""
-
- list_with_start_after: bool
- """If backend supports list with start after."""
-
- list_with_recursive: bool
- """If backend supports list with recursive."""
-
- presign: bool
- """If operator supports presign."""
-
- presign_read: bool
- """If operator supports presign read."""
-
- presign_stat: bool
- """If operator supports presign stat."""
-
- presign_write: bool
- """If operator supports presign write."""
-
- presign_delete: bool
- """If operator supports presign delete."""
-
- shared: bool
- """If operator supports shared."""
diff --git a/bindings/python/python/opendal/capability.pyi
b/bindings/python/python/opendal/capability.pyi
new file mode 100644
index 000000000..259de3ea8
--- /dev/null
+++ b/bindings/python/python/opendal/capability.pyi
@@ -0,0 +1,171 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# This file is automatically generated by pyo3_stub_gen
+# ruff: noqa: E501, F401
+
+import builtins
+import typing
+
[email protected]
+class Capability:
+ r"""
+ Capability defines the supported operations and their constraints for an
Operator.
+
+ This structure provides a comprehensive description of an Operator's
+ capabilities, including:
+
+ - Basic operations support (read, write, delete, etc.)
+ - Advanced operation variants (conditional operations, metadata handling)
+ - Operational constraints (size limits, batch limitations)
+ """
+
+ @property
+ def stat(self) -> builtins.bool:
+ r"""If operator supports stat."""
+ @property
+ def stat_with_if_match(self) -> builtins.bool:
+ r"""If operator supports stat with if match."""
+ @property
+ def stat_with_if_none_match(self) -> builtins.bool:
+ r"""If operator supports stat with if none match."""
+ @property
+ def read(self) -> builtins.bool:
+ r"""If the operator supports read operations."""
+ @property
+ def read_with_if_match(self) -> builtins.bool:
+ r"""If conditional read operations using If-Match are supported."""
+ @property
+ def read_with_if_none_match(self) -> builtins.bool:
+ r"""If conditional read operations using If-None-Match are
supported."""
+ @property
+ def read_with_if_modified_since(self) -> builtins.bool:
+ r"""If conditional read operations using If-Modified-Since are
supported."""
+ @property
+ def read_with_if_unmodified_since(self) -> builtins.bool:
+ r"""If conditional read operations using If-Unmodified-Since are
supported."""
+ @property
+ def read_with_override_cache_control(self) -> builtins.bool:
+ r"""If Cache-Control header override is supported during read
operations."""
+ @property
+ def read_with_override_content_disposition(self) -> builtins.bool:
+ r"""If Content-Disposition header can be overridden during read
operations."""
+ @property
+ def read_with_override_content_type(self) -> builtins.bool:
+ r"""If Content-Type header override is supported during read
operations."""
+ @property
+ def read_with_version(self) -> builtins.bool:
+ r"""If versions read operations are supported."""
+ @property
+ def write(self) -> builtins.bool:
+ r"""If the operator supports write operations."""
+ @property
+ def write_can_multi(self) -> builtins.bool:
+ r"""If multiple write operations can be performed on the same
object."""
+ @property
+ def write_can_empty(self) -> builtins.bool:
+ r"""If writing empty content is supported."""
+ @property
+ def write_can_append(self) -> builtins.bool:
+ r"""If append operations are supported."""
+ @property
+ def write_with_content_type(self) -> builtins.bool:
+ r"""If Content-Type can be specified during write operations."""
+ @property
+ def write_with_content_disposition(self) -> builtins.bool:
+ r"""If Content-Disposition can be specified during write operations."""
+ @property
+ def write_with_content_encoding(self) -> builtins.bool:
+ r"""If Content-Encoding can be specified during write operations."""
+ @property
+ def write_with_cache_control(self) -> builtins.bool:
+ r"""If Cache-Control can be specified during write operations."""
+ @property
+ def write_with_if_match(self) -> builtins.bool:
+ r"""If conditional write operations using If-Match are supported."""
+ @property
+ def write_with_if_none_match(self) -> builtins.bool:
+ r"""If conditional write operations using If-None-Match are
supported."""
+ @property
+ def write_with_if_not_exists(self) -> builtins.bool:
+ r"""If write operations can be conditional on object non-existence."""
+ @property
+ def write_with_user_metadata(self) -> builtins.bool:
+ r"""If custom user metadata can be attached during write operations."""
+ @property
+ def write_multi_max_size(self) -> builtins.int | None:
+ r"""
+ Maximum size supported for multipart uploads.
+
+ For example, AWS S3 supports up to 5GiB per part in multipart uploads.
+ """
+ @property
+ def write_multi_min_size(self) -> builtins.int | None:
+ r"""
+ Minimum size required for multipart uploads (except for the last part).
+
+ For example, AWS S3 requires at least 5MiB per part.
+ """
+ @property
+ def write_total_max_size(self) -> builtins.int | None:
+ r"""
+ Maximum total size supported for write operations.
+
+ For example, Cloudflare D1 has a 1MB total size limit.
+ """
+ @property
+ def create_dir(self) -> builtins.bool:
+ r"""If operator supports create dir."""
+ @property
+ def delete(self) -> builtins.bool:
+ r"""If operator supports delete."""
+ @property
+ def copy(self) -> builtins.bool:
+ r"""If operator supports copy."""
+ @property
+ def rename(self) -> builtins.bool:
+ r"""If operator supports rename."""
+ @property
+ def list(self) -> builtins.bool:
+ r"""If operator supports list."""
+ @property
+ def list_with_limit(self) -> builtins.bool:
+ r"""If backend supports list with limit."""
+ @property
+ def list_with_start_after(self) -> builtins.bool:
+ r"""If backend supports list with start after."""
+ @property
+ def list_with_recursive(self) -> builtins.bool:
+ r"""If backend supports list without delimiter."""
+ @property
+ def presign(self) -> builtins.bool:
+ r"""If operator supports presign."""
+ @property
+ def presign_read(self) -> builtins.bool:
+ r"""If operator supports presign read."""
+ @property
+ def presign_stat(self) -> builtins.bool:
+ r"""If operator supports presign stat."""
+ @property
+ def presign_write(self) -> builtins.bool:
+ r"""If operator supports presign write."""
+ @property
+ def presign_delete(self) -> builtins.bool:
+ r"""If operator supports presign delete."""
+ @property
+ def shared(self) -> builtins.bool:
+ r"""If operator supports shared."""
diff --git a/bindings/python/python/opendal/exceptions.pyi
b/bindings/python/python/opendal/exceptions.pyi
index dd18e34a5..8a1e21e57 100644
--- a/bindings/python/python/opendal/exceptions.pyi
+++ b/bindings/python/python/opendal/exceptions.pyi
@@ -15,35 +15,40 @@
# specific language governing permissions and limitations
# under the License.
-class Error(Exception):
- """Base class for exceptions in this module."""
+# This file is automatically generated by pyo3_stub_gen
+# ruff: noqa: E501, F401
-class Unexpected(Error):
- """Unexpected errors."""
+import builtins
-class Unsupported(Error):
- """Unsupported operation."""
+class AlreadyExists(builtins.Exception):
+ r"""Already exists."""
-class ConfigInvalid(Error):
- """Config is invalid."""
+class ConditionNotMatch(builtins.Exception):
+ r"""Condition not match."""
-class NotFound(Error):
- """Not found."""
+class ConfigInvalid(builtins.Exception):
+ r"""Config is invalid."""
-class PermissionDenied(Error):
- """Permission denied."""
+class Error(builtins.Exception):
+ r"""OpenDAL Base Exception."""
-class IsADirectory(Error):
- """Is a directory."""
+class IsADirectory(builtins.Exception):
+ r"""Is a directory."""
-class NotADirectory(Error):
- """Not a directory."""
+class IsSameFile(builtins.Exception):
+ r"""Is same file."""
-class AlreadyExists(Error):
- """Already exists."""
+class NotADirectory(builtins.Exception):
+ r"""Not a directory."""
-class IsSameFile(Error):
- """Is same file."""
+class NotFound(builtins.Exception):
+ r"""Not found."""
-class ConditionNotMatch(Error):
- """Condition not match."""
+class PermissionDenied(builtins.Exception):
+ r"""Permission denied."""
+
+class Unexpected(builtins.Exception):
+ r"""Unexpected errors."""
+
+class Unsupported(builtins.Exception):
+ r"""Unsupported operation."""
diff --git a/bindings/python/ruff.toml b/bindings/python/ruff.toml
index fcf0389f7..6d47e327b 100644
--- a/bindings/python/ruff.toml
+++ b/bindings/python/ruff.toml
@@ -73,6 +73,6 @@ strict = true
known-first-party = ["opendal"]
[lint.per-file-ignores]
-"*.pyi" = ["PYI021", "ANN003", "RUF100", "ANN201", "ANN001"]
+"*.pyi" = ["PYI021", "ANN003", "RUF100"]
"benchmark/*" = ["D100", "D101", "D103", "ANN201"]
"tests/*" = ["D101", "D100", "D103", "ANN201", "ANN001"]
diff --git a/bindings/python/src/bin/stub_gen.rs
b/bindings/python/src/bin/stub_gen.rs
new file mode 100644
index 000000000..2ca271276
--- /dev/null
+++ b/bindings/python/src/bin/stub_gen.rs
@@ -0,0 +1,8 @@
+use pyo3_stub_gen::Result;
+
+fn main() -> Result<()> {
+ // `stub_info` is a function defined by `define_stub_info_gatherer!` macro.
+ let stub = _opendal::stub_info()?;
+ stub.generate()?;
+ Ok(())
+}
diff --git a/bindings/python/src/capability.rs
b/bindings/python/src/capability.rs
index 4edf92fb4..39c10d75c 100644
--- a/bindings/python/src/capability.rs
+++ b/bindings/python/src/capability.rs
@@ -17,9 +17,16 @@
use pyo3::prelude::*;
-/// Capability is used to describe what operations are supported
-/// by current Operator.
-#[pyclass(get_all, module = "opendal")]
+/// Capability defines the supported operations and their constraints for an
Operator.
+///
+/// This structure provides a comprehensive description of an Operator's
+/// capabilities, including:
+///
+/// - Basic operations support (read, write, delete, etc.)
+/// - Advanced operation variants (conditional operations, metadata handling)
+/// - Operational constraints (size limits, batch limitations)
+#[crate::gen_stub_pyclass]
+#[pyclass(get_all, module = "opendal.capability")]
pub struct Capability {
/// If operator supports stat.
pub stat: bool,
@@ -28,56 +35,59 @@ pub struct Capability {
/// If operator supports stat with if none match.
pub stat_with_if_none_match: bool,
- /// Indicates if the operator supports read operations.
+ /// If the operator supports read operations.
pub read: bool,
- /// Indicates if conditional read operations using If-Match are supported.
+ /// If conditional read operations using If-Match are supported.
pub read_with_if_match: bool,
- /// Indicates if conditional read operations using If-None-Match are
supported.
+ /// If conditional read operations using If-None-Match are supported.
pub read_with_if_none_match: bool,
- /// Indicates if conditional read operations using If-Modified-Since are
supported.
+ /// If conditional read operations using If-Modified-Since are supported.
pub read_with_if_modified_since: bool,
- /// Indicates if conditional read operations using If-Unmodified-Since are
supported.
+ /// If conditional read operations using If-Unmodified-Since are supported.
pub read_with_if_unmodified_since: bool,
- /// Indicates if Cache-Control header override is supported during read
operations.
+ /// If Cache-Control header override is supported during read operations.
pub read_with_override_cache_control: bool,
- /// Indicates if Content-Disposition header override is supported during
read operations.
+ /// If Content-Disposition header can be overridden during read operations.
pub read_with_override_content_disposition: bool,
- /// Indicates if Content-Type header override is supported during read
operations.
+ /// If Content-Type header override is supported during read operations.
pub read_with_override_content_type: bool,
- /// Indicates if versions read operations are supported.
+ /// If versions read operations are supported.
pub read_with_version: bool,
- /// Indicates if the operator supports write operations.
+ /// If the operator supports write operations.
pub write: bool,
- /// Indicates if multiple write operations can be performed on the same
object.
+ /// If multiple write operations can be performed on the same object.
pub write_can_multi: bool,
- /// Indicates if writing empty content is supported.
+ /// If writing empty content is supported.
pub write_can_empty: bool,
- /// Indicates if append operations are supported.
+ /// If append operations are supported.
pub write_can_append: bool,
- /// Indicates if Content-Type can be specified during write operations.
+ /// If Content-Type can be specified during write operations.
pub write_with_content_type: bool,
- /// Indicates if Content-Disposition can be specified during write
operations.
+ /// If Content-Disposition can be specified during write operations.
pub write_with_content_disposition: bool,
- /// Indicates if Content-Encoding can be specified during write operations.
+ /// If Content-Encoding can be specified during write operations.
pub write_with_content_encoding: bool,
- /// Indicates if Cache-Control can be specified during write operations.
+ /// If Cache-Control can be specified during write operations.
pub write_with_cache_control: bool,
- /// Indicates if conditional write operations using If-Match are supported.
+ /// If conditional write operations using If-Match are supported.
pub write_with_if_match: bool,
- /// Indicates if conditional write operations using If-None-Match are
supported.
+ /// If conditional write operations using If-None-Match are supported.
pub write_with_if_none_match: bool,
- /// Indicates if write operations can be conditional on object
non-existence.
+ /// If write operations can be conditional on object non-existence.
pub write_with_if_not_exists: bool,
- /// Indicates if custom user metadata can be attached during write
operations.
+ /// If custom user metadata can be attached during write operations.
pub write_with_user_metadata: bool,
/// Maximum size supported for multipart uploads.
+ ///
/// For example, AWS S3 supports up to 5GiB per part in multipart uploads.
pub write_multi_max_size: Option<usize>,
/// Minimum size required for multipart uploads (except for the last part).
+ ///
/// For example, AWS S3 requires at least 5MiB per part.
pub write_multi_min_size: Option<usize>,
/// Maximum total size supported for write operations.
+ ///
/// For example, Cloudflare D1 has a 1MB total size limit.
pub write_total_max_size: Option<usize>,
diff --git a/bindings/python/src/errors.rs b/bindings/python/src/errors.rs
index 336c36a52..4601b59b7 100644
--- a/bindings/python/src/errors.rs
+++ b/bindings/python/src/errors.rs
@@ -15,9 +15,9 @@
// specific language governing permissions and limitations
// under the License.
-use pyo3::create_exception;
use pyo3::exceptions::PyException;
use pyo3::exceptions::PyIOError;
+use pyo3_stub_gen::create_exception;
use crate::*;
@@ -27,34 +27,54 @@ create_exception!(
PyException,
"OpenDAL Base Exception"
);
-create_exception!(opendal.exceptions, Unexpected, Error, "Unexpected errors");
+create_exception!(
+ opendal.exceptions,
+ Unexpected,
+ PyException,
+ "Unexpected errors"
+);
create_exception!(
opendal.exceptions,
Unsupported,
- Error,
+ PyException,
"Unsupported operation"
);
create_exception!(
opendal.exceptions,
ConfigInvalid,
- Error,
+ PyException,
"Config is invalid"
);
-create_exception!(opendal.exceptions, NotFound, Error, "Not found");
+create_exception!(opendal.exceptions, NotFound, PyException, "Not found");
create_exception!(
opendal.exceptions,
PermissionDenied,
- Error,
+ PyException,
"Permission denied"
);
-create_exception!(opendal.exceptions, IsADirectory, Error, "Is a directory");
-create_exception!(opendal.exceptions, NotADirectory, Error, "Not a directory");
-create_exception!(opendal.exceptions, AlreadyExists, Error, "Already exists");
-create_exception!(opendal.exceptions, IsSameFile, Error, "Is same file");
+create_exception!(
+ opendal.exceptions,
+ IsADirectory,
+ PyException,
+ "Is a directory"
+);
+create_exception!(
+ opendal.exceptions,
+ NotADirectory,
+ PyException,
+ "Not a directory"
+);
+create_exception!(
+ opendal.exceptions,
+ AlreadyExists,
+ PyException,
+ "Already exists"
+);
+create_exception!(opendal.exceptions, IsSameFile, PyException, "Is same file");
create_exception!(
opendal.exceptions,
ConditionNotMatch,
- Error,
+ PyException,
"Condition not match"
);
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 37001dfca..ad0b1ae3b 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -38,6 +38,7 @@ mod errors;
pub use errors::*;
mod options;
pub use options::*;
+use pyo3_stub_gen::{define_stub_info_gatherer, derive::*};
#[pymodule(gil_used = false)]
fn _opendal(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
@@ -47,11 +48,13 @@ fn _opendal(py: Python, m: &Bound<'_, PyModule>) ->
PyResult<()> {
m.add_class::<File>()?;
m.add_class::<AsyncFile>()?;
+ // Capability module
+ add_pymodule!(py, m, "capability", [Capability])?;
+
m.add_class::<Entry>()?;
m.add_class::<EntryMode>()?;
m.add_class::<Metadata>()?;
m.add_class::<PresignedRequest>()?;
- m.add_class::<Capability>()?;
m.add_class::<WriteOptions>()?;
m.add_class::<ReadOptions>()?;
@@ -69,21 +72,26 @@ fn _opendal(py: Python, m: &Bound<'_, PyModule>) ->
PyResult<()> {
.getattr("modules")?
.set_item("opendal.layers", layers_module)?;
- let exception_module = PyModule::new(py, "exceptions")?;
- exception_module.add("Error", py.get_type::<Error>())?;
- exception_module.add("Unexpected", py.get_type::<Unexpected>())?;
- exception_module.add("Unsupported", py.get_type::<Unsupported>())?;
- exception_module.add("ConfigInvalid", py.get_type::<ConfigInvalid>())?;
- exception_module.add("NotFound", py.get_type::<NotFound>())?;
- exception_module.add("PermissionDenied",
py.get_type::<PermissionDenied>())?;
- exception_module.add("IsADirectory", py.get_type::<IsADirectory>())?;
- exception_module.add("NotADirectory", py.get_type::<NotADirectory>())?;
- exception_module.add("AlreadyExists", py.get_type::<AlreadyExists>())?;
- exception_module.add("IsSameFile", py.get_type::<IsSameFile>())?;
- exception_module.add("ConditionNotMatch",
py.get_type::<ConditionNotMatch>())?;
- m.add_submodule(&exception_module)?;
- py.import("sys")?
- .getattr("modules")?
- .set_item("opendal.exceptions", exception_module)?;
+ // Exceptions module
+ add_pyexceptions!(
+ py,
+ m,
+ "exceptions",
+ [
+ Error,
+ Unexpected,
+ Unsupported,
+ ConfigInvalid,
+ NotFound,
+ PermissionDenied,
+ IsADirectory,
+ NotADirectory,
+ AlreadyExists,
+ IsSameFile,
+ ConditionNotMatch
+ ]
+ )?;
Ok(())
}
+
+define_stub_info_gatherer!(stub_info);
diff --git a/bindings/python/src/utils.rs b/bindings/python/src/utils.rs
index 95dfb4669..f3e736c2e 100644
--- a/bindings/python/src/utils.rs
+++ b/bindings/python/src/utils.rs
@@ -73,3 +73,45 @@ impl Buffer {
Ok(())
}
}
+
+/// Macro to create and register a PyO3 submodule with multiple classes.
+///
+/// Example:
+/// ```rust
+/// add_pymodule!(py, m, "services", [PyScheme, PyOtherClass]);
+/// ```
+#[macro_export]
+macro_rules! add_pymodule {
+ ($py:expr, $parent:expr, $name:expr, [$($cls:ty),* $(,)?]) => {{
+ let sub_module = pyo3::types::PyModule::new($py, $name)?;
+ $(
+ sub_module.add_class::<$cls>()?;
+ )*
+ $parent.add_submodule(&sub_module)?;
+ $py.import("sys")?
+ .getattr("modules")?
+ .set_item(format!("opendal.{}", $name), &sub_module)?;
+ Ok::<_, pyo3::PyErr>(())
+ }};
+}
+
+/// Macro to create and register a PyO3 submodule containing exception types.
+///
+/// Example:
+/// ```rust
+/// add_pyexceptions!(py, m, "exceptions", [Error, Unexpected]);
+/// ```
+#[macro_export]
+macro_rules! add_pyexceptions {
+ ($py:expr, $parent:expr, $name:expr, [$($exc:ty),* $(,)?]) => {{
+ let sub_module = pyo3::types::PyModule::new($py, $name)?;
+ $(
+ sub_module.add(stringify!($exc), $py.get_type::<$exc>())?;
+ )*
+ $parent.add_submodule(&sub_module)?;
+ $py.import("sys")?
+ .getattr("modules")?
+ .set_item(format!("opendal.{}", $name), &sub_module)?;
+ Ok::<_, pyo3::PyErr>(())
+ }};
+}
diff --git a/bindings/python/tests/test_exceptions.py
b/bindings/python/tests/test_exceptions.py
index faaf9a194..f07cbeabf 100644
--- a/bindings/python/tests/test_exceptions.py
+++ b/bindings/python/tests/test_exceptions.py
@@ -15,13 +15,13 @@
# specific language governing permissions and limitations
# under the License.
+import builtins
import inspect
from opendal import exceptions
-from opendal.exceptions import Error
def test_exceptions():
for _name, obj in inspect.getmembers(exceptions):
if inspect.isclass(obj):
- assert issubclass(obj, Error)
+ assert issubclass(obj, builtins.Exception)