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)

Reply via email to