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 1dbb7591a feat(bindings/python): Add stubs for some more types (#6703)
1dbb7591a is described below

commit 1dbb7591a5232002a5321f9d25c05be41dc1f421
Author: Chitral Verma <[email protected]>
AuthorDate: Sat Oct 18 17:06:41 2025 +0530

    feat(bindings/python): Add stubs for some more types (#6703)
    
    * Stubs for Layers
    
    * Stubs for Metadata, Entry, and EntryMode
    
    * Add to API Reference
---
 .../python/docs/api/{metadata.md => capability.md} |   4 +-
 bindings/python/docs/api/exceptions.md             |  45 +++++++
 bindings/python/docs/api/layers.md                 |  32 +++++
 bindings/python/docs/api/types.md                  |  25 ++++
 bindings/python/mkdocs.yml                         |  13 +-
 bindings/python/pyrightconfig.json                 |   6 +
 bindings/python/python/opendal/__init__.pyi        |  57 +--------
 bindings/python/python/opendal/layers.pyi          | 131 ++++++++++++++++---
 bindings/python/python/opendal/types.pyi           | 140 +++++++++++++++++++++
 bindings/python/src/layers.rs                      | 105 ++++++++++++++--
 bindings/python/src/lib.rs                         |  25 ++--
 bindings/python/src/metadata.rs                    | 120 +++++++++++++-----
 12 files changed, 568 insertions(+), 135 deletions(-)

diff --git a/bindings/python/docs/api/metadata.md 
b/bindings/python/docs/api/capability.md
similarity index 59%
rename from bindings/python/docs/api/metadata.md
rename to bindings/python/docs/api/capability.md
index f0df34777..14bac70df 100644
--- a/bindings/python/docs/api/metadata.md
+++ b/bindings/python/docs/api/capability.md
@@ -1,6 +1,6 @@
-::: opendal.Metadata
+::: opendal.Capability
     options:
-      heading: "opendal.Metadata"
+      heading: "opendal.Capability"
       heading_level: 2
       show_source: false
       show_bases: false
diff --git a/bindings/python/docs/api/exceptions.md 
b/bindings/python/docs/api/exceptions.md
new file mode 100644
index 000000000..873246ce7
--- /dev/null
+++ b/bindings/python/docs/api/exceptions.md
@@ -0,0 +1,45 @@
+This page documents all exceptions raised by the OpenDAL.
+
+::: opendal.exceptions.Error
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.AlreadyExists
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.ConditionNotMatch
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.ConfigInvalid
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.IsADirectory
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.IsSameFile
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.NotADirectory
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.NotFound
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.PermissionDenied
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.Unexpected
+    options:
+      heading_level: 2
+
+::: opendal.exceptions.Unsupported
+    options:
+      heading_level: 2
diff --git a/bindings/python/docs/api/layers.md 
b/bindings/python/docs/api/layers.md
new file mode 100644
index 000000000..9d89e75ff
--- /dev/null
+++ b/bindings/python/docs/api/layers.md
@@ -0,0 +1,32 @@
+# Layers   
+
+This page documents all layers in OpenDAL.
+
+## Layer
+::: opendal.layers.Layer
+    options:
+      heading: "opendal.layers.Layer"
+      heading_level: 2
+      show_source: false
+      show_bases: false
+
+## RetryLayer   
+::: opendal.layers.RetryLayer
+    options:
+      heading: "opendal.layers.RetryLayer"
+      heading_level: 2
+      show_source: false
+
+## ConcurrentLimitLayer   
+::: opendal.layers.ConcurrentLimitLayer
+    options:
+      heading: "opendal.layers.ConcurrentLimitLayer"
+      heading_level: 2
+      show_source: false
+
+## MimeGuessLayer   
+::: opendal.layers.MimeGuessLayer
+    options:
+      heading: "opendal.layers.MimeGuessLayer"
+      heading_level: 2
+      show_source: false
diff --git a/bindings/python/docs/api/types.md 
b/bindings/python/docs/api/types.md
new file mode 100644
index 000000000..396d9bee4
--- /dev/null
+++ b/bindings/python/docs/api/types.md
@@ -0,0 +1,25 @@
+# Types   
+
+This page documents all types in OpenDAL.
+
+## Entry
+::: opendal.Entry
+    options:
+      heading: "opendal.Entry"
+      heading_level: 2
+      show_source: false
+      show_bases: false
+
+## EntryMode   
+::: opendal.types.EntryMode
+    options:
+      heading: "opendal.EntryMode"
+      heading_level: 2
+
+## Metadata   
+::: opendal.types.Metadata
+    options:
+      heading: "opendal.Metadata"
+      heading_level: 2
+      show_source: false
+      show_bases: false
diff --git a/bindings/python/mkdocs.yml b/bindings/python/mkdocs.yml
index 13db72908..6adeb6474 100644
--- a/bindings/python/mkdocs.yml
+++ b/bindings/python/mkdocs.yml
@@ -43,11 +43,14 @@ nav:
       - Pandas: examples/pandas.ipynb
       - Polars: examples/polars.ipynb
   - API Reference:
-      - api/operator.md
-      - api/file.md
-      - api/async_operator.md
-      - api/async_file.md
-      - api/metadata.md
+      - AsyncFile: api/async_file.md
+      - AsyncOperator: api/async_operator.md
+      - Capability: api/capability.md
+      - Exceptions: api/exceptions.md
+      - File: api/file.md
+      - Layers: api/layers.md
+      - Operator: api/operator.md
+      - Types: api/types.md
 
 markdown_extensions:
   - pymdownx.highlight:
diff --git a/bindings/python/pyrightconfig.json 
b/bindings/python/pyrightconfig.json
new file mode 100644
index 000000000..a76866465
--- /dev/null
+++ b/bindings/python/pyrightconfig.json
@@ -0,0 +1,6 @@
+{
+  "include": ["python/opendal/"],
+  "ignore": [
+    "python/opendal/*.pyi"
+  ]
+}
diff --git a/bindings/python/python/opendal/__init__.pyi 
b/bindings/python/python/opendal/__init__.pyi
index 825b1f6d6..91ec09466 100644
--- a/bindings/python/python/opendal/__init__.pyi
+++ b/bindings/python/python/opendal/__init__.pyi
@@ -17,7 +17,6 @@
 
 import os
 from collections.abc import AsyncIterable, Iterable
-from datetime import datetime
 from types import TracebackType
 from typing import TypeAlias, final
 
@@ -30,6 +29,7 @@ from opendal import layers as layers
 from opendal.__base import _Base
 from opendal.capability import Capability
 from opendal.layers import Layer
+from opendal.types import Entry, Metadata
 
 PathBuf: TypeAlias = str | os.PathLike
 
@@ -703,61 +703,6 @@ class AsyncFile:
     async def writable(self) -> bool:
         """Check if the file is writable."""
 
-@final
-class Entry:
-    """An entry in the directory listing."""
-
-    @property
-    def path(self) -> str:
-        """The path of the entry."""
-    @property
-    def metadata(self) -> Metadata:
-        """The metadata of the entry."""
-
-@final
-class Metadata:
-    @property
-    def content_disposition(self) -> str | None:
-        """The content disposition of the object."""
-    @property
-    def content_length(self) -> int:
-        """The content length of the object."""
-    @property
-    def content_md5(self) -> str | None:
-        """The MD5 checksum of the object."""
-    @property
-    def content_type(self) -> str | None:
-        """The mime type of the object."""
-    @property
-    def content_encoding(self) -> str | None:
-        """The content encoding of the object."""
-    @property
-    def etag(self) -> str | None:
-        """The ETag of the object."""
-    @property
-    def mode(self) -> EntryMode:
-        """The mode of the object."""
-    @property
-    def is_file(self) -> bool:
-        """Returns `True` if this metadata is for a file."""
-    @property
-    def is_dir(self) -> bool:
-        """Returns `True` if this metadata is for a directory."""
-    @property
-    def last_modified(self) -> datetime | None:
-        """The last modified time of the object."""
-    @property
-    def version(self) -> str | None:
-        """The version of the object, if available."""
-    @property
-    def user_metadata(self) -> str | None:
-        """The user defined metadata of the object."""
-
-@final
-class EntryMode:
-    def is_file(self) -> bool: ...
-    def is_dir(self) -> bool: ...
-
 @final
 class PresignedRequest:
     @property
diff --git a/bindings/python/python/opendal/layers.pyi 
b/bindings/python/python/opendal/layers.pyi
index c305e9ab0..8dbd071c9 100644
--- a/bindings/python/python/opendal/layers.pyi
+++ b/bindings/python/python/opendal/layers.pyi
@@ -15,25 +15,122 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from typing import final
+# This file is automatically generated by pyo3_stub_gen
+# ruff: noqa: E501, F401
 
-class Layer: ...
+import builtins
+import typing
 
-@final
-class RetryLayer(Layer):
-    def __init__(
-        self,
-        max_times: int | None = None,
-        factor: float | None = None,
-        jitter: bool = False,
-        max_delay: float | None = None,
-        min_delay: float | None = None,
-    ) -> None: ...
-
-@final
[email protected]
 class ConcurrentLimitLayer(Layer):
-    def __init__(self, limit: int) -> None: ...
+    r"""
+    ConcurrentLimitLayer.
+
+    Create a layer that limits the number of concurrent operations.
+
+    Notes
+    -----
+    All operators wrapped by this layer will share a common semaphore. This
+    allows you to reuse the same layer across multiple operators, ensuring
+    that the total number of concurrent requests across the entire
+    application does not exceed the limit.
+    """
+
+    def __new__(cls, limit: builtins.int) -> ConcurrentLimitLayer:
+        r"""
+        Create a new ConcurrentLimitLayer.
+
+        Parameters
+        ----------
+        limit : int
+            Maximum number of concurrent operations allowed.
+
+        Returns
+        -------
+        ConcurrentLimitLayer
+        """
 
-@final
+class Layer:
+    r"""
+    Layer.
+
+    Layers are used to intercept the operations on the underlying storage.
+    """
+
[email protected]
 class MimeGuessLayer(Layer):
-    def __init__(self) -> None: ...
+    r"""
+    MimeGuessLayer.
+
+    Create a layer that guesses MIME types for objects based on their
+    paths or content.
+
+    This layer uses the `mime_guess` crate
+    (see https://crates.io/crates/mime_guess) to infer the
+    ``Content-Type``.
+
+    Notes
+    -----
+    This layer will not override a ``Content-Type`` that has already
+    been set, either manually or by the backend service. It is only
+    applied if no content type is present.
+
+    A ``Content-Type`` is not guaranteed. If the file extension is
+    uncommon or unknown, the content type will remain unset.
+    """
+
+    def __new__(cls) -> MimeGuessLayer:
+        r"""
+        Create a new MimeGuessLayer.
+
+        Returns
+        -------
+        MimeGuessLayer
+        """
+
[email protected]
+class RetryLayer(Layer):
+    r"""
+    RetryLayer.
+
+    A layer that retries operations that fail with temporary errors.
+
+    Operations are retried if they fail with an error for which
+    `Error.is_temporary` returns `True`. If all retries are exhausted,
+    the error is marked as persistent and then returned.
+
+    Notes
+    -----
+    After an operation on a `Reader` or `Writer` has failed through
+    all retries, the object is in an undefined state. Reusing it
+    can lead to exceptions.
+    """
+
+    def __new__(
+        cls,
+        max_times: builtins.int | None = None,
+        factor: builtins.float | None = None,
+        jitter: builtins.bool = False,
+        max_delay: builtins.float | None = None,
+        min_delay: builtins.float | None = None,
+    ) -> RetryLayer:
+        r"""
+        Create a new RetryLayer.
+
+        Parameters
+        ----------
+        max_times : Optional[int]
+            Maximum number of retry attempts. Defaults to ``3``.
+        factor : Optional[float]
+            Backoff factor applied between retries. Defaults to ``2.0``.
+        jitter : bool
+            Whether to apply jitter to the backoff. Defaults to ``False``.
+        max_delay : Optional[float]
+            Maximum delay (in seconds) between retries. Defaults to ``60.0``.
+        min_delay : Optional[float]
+            Minimum delay (in seconds) between retries. Defaults to ``1.0``.
+
+        Returns
+        -------
+        RetryLayer
+        """
diff --git a/bindings/python/python/opendal/types.pyi 
b/bindings/python/python/opendal/types.pyi
new file mode 100644
index 000000000..48377f206
--- /dev/null
+++ b/bindings/python/python/opendal/types.pyi
@@ -0,0 +1,140 @@
+# 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 datetime
+import enum
+import typing
+
[email protected]
+class Entry:
+    r"""
+    Entry.
+
+    An entry representing a path and its associated metadata.
+
+    Notes
+    -----
+    If this entry is a directory, ``path`` **must** end with ``/``.
+    Otherwise, ``path`` **must not** end with ``/``.
+    """
+
+    @property
+    def path(self) -> builtins.str:
+        r"""The path of entry relative to the operator's root."""
+    @property
+    def name(self) -> builtins.str:
+        r"""The name of entry, representing the last segment of the path."""
+    @property
+    def metadata(self) -> Metadata:
+        r"""The metadata of this entry."""
+
[email protected]
+class Metadata:
+    r"""
+    The metadata of an ``Entry``.
+
+    The metadata is always tied to a specific context and is not a global
+    state. For example, two versions of the same path might have different
+    content lengths.
+
+    Notes
+    -----
+    In systems that support versioning, such as AWS S3, the metadata may
+    represent a specific version of a file. Use :attr:`version` to get
+    the version of a file if it is available.
+    """
+
+    @property
+    def content_disposition(self) -> builtins.str | None:
+        r"""The content disposition of this entry."""
+    @property
+    def content_length(self) -> builtins.int:
+        r"""The content length of this entry."""
+    @property
+    def content_md5(self) -> builtins.str | None:
+        r"""The content MD5 of this entry."""
+    @property
+    def content_type(self) -> builtins.str | None:
+        r"""The content type of this entry."""
+    @property
+    def content_encoding(self) -> builtins.str | None:
+        r"""The content encoding of this entry."""
+    @property
+    def etag(self) -> builtins.str | None:
+        r"""The ETag of this entry."""
+    @property
+    def mode(self) -> EntryMode:
+        r"""The mode of this entry."""
+    @property
+    def is_file(self) -> builtins.bool:
+        r"""Whether this entry is a file."""
+    @property
+    def is_dir(self) -> builtins.bool:
+        r"""Whether this entry is a directory."""
+    @property
+    def last_modified(self) -> datetime.datetime:
+        r"""The last modified timestamp of this entry."""
+    @property
+    def version(self) -> builtins.str | None:
+        r"""The version of this entry."""
+    @property
+    def user_metadata(self) -> builtins.dict[builtins.str, builtins.str] | 
None:
+        r"""The user-defined metadata of this entry."""
+
[email protected]
+class EntryMode(enum.Enum):
+    r"""
+    EntryMode.
+
+    The mode of an entry, indicating if it is a file or a directory.
+    """
+
+    File = ...
+    r"""
+    The entry is a file and has data to read.
+    """
+    Dir = ...
+    r"""
+    The entry is a directory and can be listed.
+    """
+    Unknown = ...
+    r"""
+    The mode of the entry is unknown.
+    """
+
+    def is_file(self) -> builtins.bool:
+        r"""
+        Check if the entry mode is `File`.
+
+        Returns
+        -------
+        bool
+            True if the entry is a file.
+        """
+    def is_dir(self) -> builtins.bool:
+        r"""
+        Check if the entry mode is `Dir`.
+
+        Returns
+        -------
+        bool
+            True if the entry is a directory.
+        """
diff --git a/bindings/python/src/layers.rs b/bindings/python/src/layers.rs
index 59b0c3540..3948f30d6 100644
--- a/bindings/python/src/layers.rs
+++ b/bindings/python/src/layers.rs
@@ -17,18 +17,34 @@
 
 use std::time::Duration;
 
-use opendal::Operator;
-use pyo3::prelude::*;
-
 use crate::*;
+use opendal::Operator;
 
 pub trait PythonLayer: Send + Sync {
     fn layer(&self, op: Operator) -> Operator;
 }
 
+/// Layer
+///
+/// Layers are used to intercept the operations on the underlying storage.
+#[gen_stub_pyclass]
 #[pyclass(module = "opendal.layers", subclass)]
 pub struct Layer(pub Box<dyn PythonLayer>);
 
+/// RetryLayer
+///
+/// A layer that retries operations that fail with temporary errors.
+///
+/// Operations are retried if they fail with an error for which
+/// `Error.is_temporary` returns `True`. If all retries are exhausted,
+/// the error is marked as persistent and then returned.
+///
+/// Notes
+/// -----
+/// After an operation on a `Reader` or `Writer` has failed through
+/// all retries, the object is in an undefined state. Reusing it
+/// can lead to exceptions.
+#[gen_stub_pyclass]
 #[pyclass(module = "opendal.layers", extends=Layer)]
 #[derive(Clone)]
 pub struct RetryLayer(ocore::layers::RetryLayer);
@@ -39,8 +55,28 @@ impl PythonLayer for RetryLayer {
     }
 }
 
+#[gen_stub_pymethods]
 #[pymethods]
 impl RetryLayer {
+    /// Create a new RetryLayer.
+    ///
+    /// Parameters
+    /// ----------
+    /// max_times : Optional[int]
+    ///     Maximum number of retry attempts. Defaults to ``3``.
+    /// factor : Optional[float]
+    ///     Backoff factor applied between retries. Defaults to ``2.0``.
+    /// jitter : bool
+    ///     Whether to apply jitter to the backoff. Defaults to ``False``.
+    /// max_delay : Optional[float]
+    ///     Maximum delay (in seconds) between retries. Defaults to ``60.0``.
+    /// min_delay : Optional[float]
+    ///     Minimum delay (in seconds) between retries. Defaults to ``1.0``.
+    ///
+    /// Returns
+    /// -------
+    /// RetryLayer
+    #[gen_stub(override_return_type(type_repr = "RetryLayer"))]
     #[new]
     #[pyo3(signature = (
         max_times = None,
@@ -67,10 +103,10 @@ impl RetryLayer {
             retry = retry.with_jitter();
         }
         if let Some(max_delay) = max_delay {
-            retry = retry.with_max_delay(Duration::from_micros((max_delay * 
1000000.0) as u64));
+            retry = retry.with_max_delay(Duration::from_micros((max_delay * 
1_000_000.0) as u64));
         }
         if let Some(min_delay) = min_delay {
-            retry = retry.with_min_delay(Duration::from_micros((min_delay * 
1000000.0) as u64));
+            retry = retry.with_min_delay(Duration::from_micros((min_delay * 
1_000_000.0) as u64));
         }
 
         let retry_layer = Self(retry);
@@ -81,7 +117,18 @@ impl RetryLayer {
     }
 }
 
-#[pyclass(module = "opendal.layers", extends=Layer)]
+/// ConcurrentLimitLayer
+///
+/// Create a layer that limits the number of concurrent operations.
+///
+/// Notes
+/// -----
+/// All operators wrapped by this layer will share a common semaphore. This
+/// allows you to reuse the same layer across multiple operators, ensuring
+/// that the total number of concurrent requests across the entire
+/// application does not exceed the limit.
+#[gen_stub_pyclass]
+#[pyclass(module = "opendal.layers", extends = Layer)]
 #[derive(Clone)]
 pub struct ConcurrentLimitLayer(ocore::layers::ConcurrentLimitLayer);
 
@@ -91,8 +138,20 @@ impl PythonLayer for ConcurrentLimitLayer {
     }
 }
 
+#[gen_stub_pymethods]
 #[pymethods]
 impl ConcurrentLimitLayer {
+    /// Create a new ConcurrentLimitLayer.
+    ///
+    /// Parameters
+    /// ----------
+    /// limit : int
+    ///     Maximum number of concurrent operations allowed.
+    ///
+    /// Returns
+    /// -------
+    /// ConcurrentLimitLayer
+    #[gen_stub(override_return_type(type_repr = "ConcurrentLimitLayer"))]
     #[new]
     #[pyo3(signature = (limit))]
     fn new(limit: usize) -> PyResult<PyClassInitializer<Self>> {
@@ -104,7 +163,25 @@ impl ConcurrentLimitLayer {
     }
 }
 
-#[pyclass(module = "opendal.layers", extends=Layer)]
+/// MimeGuessLayer
+///
+/// Create a layer that guesses MIME types for objects based on their
+/// paths or content.
+///
+/// This layer uses the `mime_guess` crate
+/// (see https://crates.io/crates/mime_guess) to infer the
+/// ``Content-Type``.
+///
+/// Notes
+/// -----
+/// This layer will not override a ``Content-Type`` that has already
+/// been set, either manually or by the backend service. It is only
+/// applied if no content type is present.
+///
+/// A ``Content-Type`` is not guaranteed. If the file extension is
+/// uncommon or unknown, the content type will remain unset.
+#[gen_stub_pyclass]
+#[pyclass(module = "opendal.layers", extends = Layer)]
 #[derive(Clone)]
 pub struct MimeGuessLayer(ocore::layers::MimeGuessLayer);
 
@@ -114,14 +191,20 @@ impl PythonLayer for MimeGuessLayer {
     }
 }
 
+#[gen_stub_pymethods]
 #[pymethods]
 impl MimeGuessLayer {
+    /// Create a new MimeGuessLayer.
+    ///
+    /// Returns
+    /// -------
+    /// MimeGuessLayer
+    #[gen_stub(override_return_type(type_repr = "MimeGuessLayer"))]
     #[new]
-    #[pyo3(signature = ())]
     fn new() -> PyResult<PyClassInitializer<Self>> {
-        let mime_guess_layer = Self(ocore::layers::MimeGuessLayer::default());
-        let class = 
PyClassInitializer::from(Layer(Box::new(mime_guess_layer.clone())))
-            .add_subclass(mime_guess_layer);
+        let mime_guess = Self(ocore::layers::MimeGuessLayer::default());
+        let class =
+            
PyClassInitializer::from(Layer(Box::new(mime_guess.clone()))).add_subclass(mime_guess);
         Ok(class)
     }
 }
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index ad0b1ae3b..f86b5fa22 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -51,9 +51,17 @@ fn _opendal(py: Python, m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     // Capability module
     add_pymodule!(py, m, "capability", [Capability])?;
 
-    m.add_class::<Entry>()?;
-    m.add_class::<EntryMode>()?;
-    m.add_class::<Metadata>()?;
+    // Layers module
+    add_pymodule!(
+        py,
+        m,
+        "layers",
+        [Layer, RetryLayer, ConcurrentLimitLayer, MimeGuessLayer]
+    )?;
+
+    // Types module
+    add_pymodule!(py, m, "types", [Entry, EntryMode, Metadata])?;
+
     m.add_class::<PresignedRequest>()?;
 
     m.add_class::<WriteOptions>()?;
@@ -61,17 +69,6 @@ fn _opendal(py: Python, m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_class::<ListOptions>()?;
     m.add_class::<StatOptions>()?;
 
-    // Layer module
-    let layers_module = PyModule::new(py, "layers")?;
-    layers_module.add_class::<Layer>()?;
-    layers_module.add_class::<RetryLayer>()?;
-    layers_module.add_class::<ConcurrentLimitLayer>()?;
-    layers_module.add_class::<MimeGuessLayer>()?;
-    m.add_submodule(&layers_module)?;
-    py.import("sys")?
-        .getattr("modules")?
-        .set_item("opendal.layers", layers_module)?;
-
     // Exceptions module
     add_pyexceptions!(
         py,
diff --git a/bindings/python/src/metadata.rs b/bindings/python/src/metadata.rs
index 3c9d33a17..7c249268f 100644
--- a/bindings/python/src/metadata.rs
+++ b/bindings/python/src/metadata.rs
@@ -18,7 +18,16 @@
 use crate::*;
 use std::collections::HashMap;
 
-#[pyclass(module = "opendal")]
+/// Entry
+///
+/// An entry representing a path and its associated metadata.
+///
+/// Notes
+/// -----
+/// If this entry is a directory, ``path`` **must** end with ``/``.
+/// Otherwise, ``path`` **must not** end with ``/``.
+#[gen_stub_pyclass]
+#[pyclass(module = "opendal.types")]
 pub struct Entry(ocore::Entry);
 
 impl Entry {
@@ -27,24 +36,33 @@ impl Entry {
     }
 }
 
+#[gen_stub_pymethods]
 #[pymethods]
 impl Entry {
-    /// Path of entry. Path is relative to the operator's root.
+    /// The path of entry relative to the operator's root.
     #[getter]
     pub fn path(&self) -> &str {
         self.0.path()
     }
 
-    /// Metadata of entry.
+    /// The name of entry, representing the last segment of the path.
+    #[getter]
+    pub fn name(&self) -> &str {
+        self.0.name()
+    }
+
+    /// The metadata of this entry.
     #[getter]
     pub fn metadata(&self) -> Metadata {
         Metadata::new(self.0.metadata().clone())
     }
 
+    #[gen_stub(skip)]
     fn __str__(&self) -> &str {
         self.0.path()
     }
 
+    #[gen_stub(skip)]
     fn __repr__(&self) -> String {
         format!(
             "Entry(path={:?}, metadata={})",
@@ -54,7 +72,19 @@ impl Entry {
     }
 }
 
-#[pyclass(module = "opendal")]
+/// The metadata of an ``Entry``.
+///
+/// The metadata is always tied to a specific context and is not a global
+/// state. For example, two versions of the same path might have different
+/// content lengths.
+///
+/// Notes
+/// -----
+/// In systems that support versioning, such as AWS S3, the metadata may
+/// represent a specific version of a file. Use :attr:`version` to get
+/// the version of a file if it is available.
+#[gen_stub_pyclass]
+#[pyclass(module = "opendal.types")]
 pub struct Metadata(ocore::Metadata);
 
 impl Metadata {
@@ -63,79 +93,83 @@ impl Metadata {
     }
 }
 
+#[gen_stub_pymethods]
 #[pymethods]
 impl Metadata {
+    /// The content disposition of this entry.
     #[getter]
     pub fn content_disposition(&self) -> Option<&str> {
         self.0.content_disposition()
     }
 
-    /// Content length of this entry.
+    /// The content length of this entry.
     #[getter]
     pub fn content_length(&self) -> u64 {
         self.0.content_length()
     }
 
-    /// Content MD5 of this entry.
+    /// The content MD5 of this entry.
     #[getter]
     pub fn content_md5(&self) -> Option<&str> {
         self.0.content_md5()
     }
 
-    /// Content Type of this entry.
+    /// The content type of this entry.
     #[getter]
     pub fn content_type(&self) -> Option<&str> {
         self.0.content_type()
     }
 
-    /// Content Type of this entry.
+    /// The content encoding of this entry.
     #[getter]
     pub fn content_encoding(&self) -> Option<&str> {
         self.0.content_encoding()
     }
 
-    /// ETag of this entry.
+    /// The ETag of this entry.
     #[getter]
     pub fn etag(&self) -> Option<&str> {
         self.0.etag()
     }
 
-    /// mode represents this entry's mode.
+    /// The mode of this entry.
     #[getter]
     pub fn mode(&self) -> EntryMode {
-        EntryMode(self.0.mode())
+        EntryMode::new(self.0.mode())
     }
 
-    /// Returns `true` if this metadata is for a file.
+    /// Whether this entry is a file.
     #[getter]
     pub fn is_file(&self) -> bool {
         self.mode().is_file()
     }
 
-    /// Returns `true` if this metadata is for a directory.
+    /// Whether this entry is a directory.
     #[getter]
     pub fn is_dir(&self) -> bool {
         self.mode().is_dir()
     }
 
-    /// Last modified time
+    /// The last modified timestamp of this entry.
+    #[gen_stub(override_return_type(type_repr = "datetime.datetime", 
imports=("datetime")))]
     #[getter]
     pub fn last_modified(&self) -> Option<jiff::Timestamp> {
         self.0.last_modified().map(Into::into)
     }
 
-    /// Version of this entry, if available.
+    /// The version of this entry.
     #[getter]
     pub fn version(&self) -> Option<&str> {
         self.0.version()
     }
 
-    /// User defined metadata of this entry
+    /// The user-defined metadata of this entry.
     #[getter]
     pub fn user_metadata(&self) -> Option<&HashMap<String, String>> {
         self.0.user_metadata()
     }
 
+    #[gen_stub(skip)]
     pub fn __repr__(&self) -> String {
         let mut parts = vec![];
 
@@ -157,26 +191,52 @@ impl Metadata {
     }
 }
 
-#[pyclass(module = "opendal")]
-pub struct EntryMode(ocore::EntryMode);
+/// EntryMode
+///
+/// The mode of an entry, indicating if it is a file or a directory.
+#[gen_stub_pyclass_enum]
+#[pyclass(eq, eq_int, hash, frozen, module = "opendal.types")]
+#[pyo3(rename_all = "PascalCase")]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum EntryMode {
+    /// The entry is a file and has data to read.
+    FILE,
+    /// The entry is a directory and can be listed.
+    DIR,
+    /// The mode of the entry is unknown.
+    Unknown,
+}
+
+impl EntryMode {
+    pub fn new(mode: ocore::EntryMode) -> Self {
+        match mode {
+            ocore::EntryMode::FILE => Self::FILE,
+            ocore::EntryMode::DIR => Self::DIR,
+            ocore::EntryMode::Unknown => Self::Unknown,
+        }
+    }
+}
 
+#[gen_stub_pymethods]
 #[pymethods]
 impl EntryMode {
-    /// Returns `True` if this is a file.
+    /// Check if the entry mode is `File`.
+    ///
+    /// Returns
+    /// -------
+    /// bool
+    ///     True if the entry is a file.
     pub fn is_file(&self) -> bool {
-        self.0.is_file()
+        matches!(self, EntryMode::FILE)
     }
 
-    /// Returns `True` if this is a directory.
+    /// Check if the entry mode is `Dir`.
+    ///
+    /// Returns
+    /// -------
+    /// bool
+    ///     True if the entry is a directory.
     pub fn is_dir(&self) -> bool {
-        self.0.is_dir()
-    }
-
-    pub fn __repr__(&self) -> &'static str {
-        match self.0 {
-            ocore::EntryMode::FILE => "EntryMode.FILE",
-            ocore::EntryMode::DIR => "EntryMode.DIR",
-            ocore::EntryMode::Unknown => "EntryMode.UNKNOWN",
-        }
+        matches!(self, EntryMode::DIR)
     }
 }

Reply via email to