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 cdb9871d1 feat(bindings/python): Enhance Stat, Lister, Metadata & 
Entry  (#6232)
cdb9871d1 is described below

commit cdb9871d16117b7b0645b7e31011ee350c9895e2
Author: Chitral Verma <chitralve...@gmail.com>
AuthorDate: Mon Jun 9 15:50:56 2025 +0530

    feat(bindings/python): Enhance Stat, Lister, Metadata & Entry  (#6232)
    
    * enhance metadata in lister for fs service
    
    * updates to entry and metadata
    
    * allow all options with list and scan
    
    * clippy
    
    * clippy
    
    * add deprecation warning to scan
    
    * fix docs for sync/ async list and scan
    
    * fix formatting
    
    * fix formatting
    
    * add stat options
    
    * use deprecated decorator
    
    * use deprecated decorator
    
    * Update bindings/python/python/opendal/__init__.pyi
    
    Co-authored-by: Nadeshiko Manju <lizheao940...@gmail.com>
    
    * fix formatting
    
    * Reset lister.rs
    
    ---------
    
    Co-authored-by: Asuka Minato <i...@asukaminato.eu.org>
    Co-authored-by: Nadeshiko Manju <lizheao940...@gmail.com>
---
 bindings/python/python/opendal/__init__.pyi      | 258 +++++++++++++++++------
 bindings/python/python/opendal/exceptions.pyi    |  20 +-
 bindings/python/src/lib.rs                       |   2 +
 bindings/python/src/metadata.rs                  |  78 +++++--
 bindings/python/src/operator.rs                  | 113 ++++++----
 bindings/python/src/options.rs                   |  50 +++++
 bindings/python/tests/test_async_pickle_types.py |   5 +-
 bindings/python/tests/test_pickle_rw.py          |   6 +-
 bindings/python/tests/test_sync_pickle_types.py  |   5 +-
 bindings/python/tests/test_write.py              |  26 ---
 10 files changed, 392 insertions(+), 171 deletions(-)

diff --git a/bindings/python/python/opendal/__init__.pyi 
b/bindings/python/python/opendal/__init__.pyi
index 994c91c65..cd7e24c80 100644
--- a/bindings/python/python/opendal/__init__.pyi
+++ b/bindings/python/python/opendal/__init__.pyi
@@ -17,9 +17,14 @@
 
 import os
 from collections.abc import AsyncIterable, Iterable
+from datetime import datetime
 from types import TracebackType
 from typing import Any, Union, final
 
+try:
+    from warnings import deprecated
+except ImportError:
+    from typing_extensions import deprecated
 from opendal import exceptions as exceptions
 from opendal import layers as layers
 from opendal.__base import _Base
@@ -44,6 +49,7 @@ class Operator(_Base):
         op.write("hello.txt", b"hello world")
         ```
     """
+
     def __init__(self, scheme: str, **options: Any) -> None: ...
     def layer(self, layer: Layer) -> Operator:
         """Add new layers upon the current operator.
@@ -51,7 +57,8 @@ class Operator(_Base):
         Args:
             layer (Layer): The layer to be added.
 
-        Returns:
+        Returns
+        -------
             The new operator with the layer added.
         """
     def open(self, path: PathBuf, mode: str, **options: Any) -> File:
@@ -68,7 +75,8 @@ class Operator(_Base):
                 - If `mode == "wb"`: options match the
                   [OpenDAL 
`WriteOptions`](https://opendal.apache.org/docs/rust/opendal/options/struct.WriteOptions.html).
 
-        Returns:
+        Returns
+        -------
             File: A file-like object that can be used to read or write the 
file.
 
         Example:
@@ -108,7 +116,8 @@ class Operator(_Base):
                 - if_unmodified_since (datetime): Only read if the object was 
not
                     modified since this timestamp. This timestamp must be in 
UTC.
 
-        Returns:
+        Returns
+        -------
             bytes: The content of the object as bytes.
         """
     def write(self, path: PathBuf, bs: bytes, **options: Any) -> None:
@@ -139,17 +148,35 @@ class Operator(_Base):
                 - user_metadata (dict[str, str]): Custom user metadata to 
associate
                     with the object.
 
-        Returns:
+        Returns
+        -------
             None
         """
-    def stat(self, path: PathBuf) -> Metadata:
+    def stat(self, path: PathBuf, **kwargs) -> Metadata:
         """Get the metadata of the object at the given path.
 
         Args:
-            path (str|Path): The path to the object.
+            path (str | Path): The path to the object.
+            **kwargs (Any): Optional stat parameters matching the
+                [OpenDAL 
`StatOptions`](https://opendal.apache.org/docs/rust/opendal/options/struct.StatOptions.html):
 
-        Returns:
-            The metadata of the object.
+                - version (str): Specify the version of the object to read, if
+                    supported by the backend.
+                - if_match (str): Read only if the ETag matches the given 
value.
+                - if_none_match (str): Read-only if the ETag does not match the
+                    given value.
+                - if_modified_since (datetime): Only read if the object was 
modified
+                    since this timestamp. This timestamp must be in UTC.
+                - if_unmodified_since (datetime): Only read if the object was 
not
+                    modified since this timestamp. This timestamp must be in 
UTC.
+                - cache_control (str): Override the cache-control header for 
the object.
+                - content_type (str): Explicitly set the Content-Type header 
for
+                    the object.
+                - content_disposition (str): Sets how the object should be 
presented
+                    (e.g., as an attachment).
+        Returns
+        -------
+            Metadata: The metadata of the object.
         """
     def create_dir(self, path: PathBuf) -> None:
         """Create a directory at the given path.
@@ -169,32 +196,59 @@ class Operator(_Base):
         Args:
             path (str|Path): The path to the object.
 
-        Returns:
+        Returns
+        -------
             True if the object exists, False otherwise.
         """
-    def list(self, path: PathBuf, *, start_after: str | None = None) -> 
Iterable[Entry]:
-        """List the objects at the given path.
-
-        Args:
-            path (str|Path): The path to the directory.
-            start_after (str | None): The key to start listing from.
-
-        Returns:
-            An iterable of entries representing the objects in the directory.
-        """
-    def scan(self, path: PathBuf) -> Iterable[Entry]:
+    def list(self, path: PathBuf, **kwargs) -> Iterable[Entry]:
+        """List objects at the given path.
+
+        Args:
+            path (str | Path): The path to the directory/ prefix.
+            **kwargs (Any): Optional listing parameters matching the
+                [OpenDAL 
`ListOptions`](https://opendal.apache.org/docs/rust/opendal/options/struct.ListOptions.html):
+
+                - limit (int): The limit passed to the underlying service to 
specify the
+                    max results that could return per-request. Users could use 
this to
+                    control the memory usage of list operation. If not set, 
all matching
+                    entries will be listed.
+                - start_after (str): Start listing after this key. Useful for 
pagination
+                    or resuming interrupted listings.
+                - recursive (bool): Whether to list entries recursively 
through all
+                    subdirectories. If False, lists only top-level entries 
(entries
+                    under the given path).
+                - versions (bool): Whether to include all versions of objects, 
if the
+                    underlying service supports versioning.
+                - deleted (bool): Whether to include deleted objects, if the 
underlying
+                    service supports soft-deletes or versioning.
+
+        Returns
+        -------
+            Iterable[Entry]: An iterable of entries representing the objects 
in the
+                directory or prefix.
+        """
+    @deprecated("Use `list()` instead.")
+    def scan(self, path: PathBuf, **kwargs) -> Iterable[Entry]:
         """Scan the objects at the given path recursively.
 
         Args:
-            path (str|Path): The path to the directory.
+            path (str | Path): The path to the directory/ prefix.
+            **kwargs (Any): Optional listing parameters matching the
+                [OpenDAL 
`ListOptions`](https://opendal.apache.org/docs/rust/opendal/options/struct.ListOptions.html),
+                excluding `recursive` which is always enforced as `True`
 
-        Returns:
-            An iterable of entries representing the objects in the directory.
+        Returns
+        -------
+            Iterable[Entry]: An iterable of all entries under the given path,
+                recursively traversing all subdirectories. Each entry 
represents
+                an object (e.g., file or directory) discovered within the full
+                descendant hierarchy of the specified path.
         """
     def capability(self) -> Capability:
         """Get the capability of the operator.
 
-        Returns:
+        Returns
+        -------
             The capability of the operator.
         """
     def copy(self, source: PathBuf, target: PathBuf) -> None:
@@ -212,7 +266,7 @@ class Operator(_Base):
             target (str|Path): The target path.
         """
     def remove_all(self, path: PathBuf) -> None:
-        """Convert into an async operator"""
+        """Convert into an async operator."""
     def to_async_operator(self) -> AsyncOperator: ...
 
 @final
@@ -232,6 +286,7 @@ class AsyncOperator(_Base):
         await op.write("hello.txt", b"hello world")
         ```
     """
+
     def __init__(self, scheme: str, **options: Any) -> None: ...
     def layer(self, layer: Layer) -> AsyncOperator: ...
     async def open(self, path: PathBuf, mode: str, **options: Any) -> 
AsyncFile:
@@ -248,7 +303,8 @@ class AsyncOperator(_Base):
                 - If `mode == "wb"`: options match the
                   [OpenDAL 
`WriteOptions`](https://opendal.apache.org/docs/rust/opendal/options/struct.WriteOptions.html).
 
-        Returns:
+        Returns
+        -------
             AsyncFile: A file-like object that can be used to read or write 
the file.
 
         Example:
@@ -289,7 +345,8 @@ class AsyncOperator(_Base):
                 - if_unmodified_since (datetime): Only read if the object was 
not
                     modified since this timestamp. This timestamp must be in 
UTC.
 
-        Returns:
+        Returns
+        -------
             The content of the object as bytes.
         """
     async def write(self, path: PathBuf, bs: bytes, **options: Any) -> None:
@@ -320,17 +377,35 @@ class AsyncOperator(_Base):
                 - user_metadata (dict[str, str]): Custom user metadata to 
associate
                     with the object.
 
-        Returns:
+        Returns
+        -------
             None
         """
-    async def stat(self, path: PathBuf) -> Metadata:
+    async def stat(self, path: PathBuf, **kwargs) -> Metadata:
         """Get the metadata of the object at the given path.
 
         Args:
-            path (str|Path): The path to the object.
+            path (str | Path): The path to the object.
+            **kwargs (Any): Optional stat parameters matching the
+                [OpenDAL 
`StatOptions`](https://opendal.apache.org/docs/rust/opendal/options/struct.StatOptions.html):
 
-        Returns:
-            The metadata of the object.
+                - version (str): Specify the version of the object to read, if
+                    supported by the backend.
+                - if_match (str): Read only if the ETag matches the given 
value.
+                - if_none_match (str): Read-only if the ETag does not match the
+                    given value.
+                - if_modified_since (datetime): Only read if the object was 
modified
+                    since this timestamp. This timestamp must be in UTC.
+                - if_unmodified_since (datetime): Only read if the object was 
not
+                    modified since this timestamp. This timestamp must be in 
UTC.
+                - cache_control (str): Override the cache-control header for 
the object.
+                - content_type (str): Explicitly set the Content-Type header 
for
+                    the object.
+                - content_disposition (str): Sets how the object should be 
presented
+                    (e.g., as an attachment).
+        Returns
+        -------
+            Metadata: The metadata of the object.
         """
     async def create_dir(self, path: PathBuf) -> None:
         """Create a directory at the given path.
@@ -350,29 +425,54 @@ class AsyncOperator(_Base):
         Args:
             path (str|Path): The path to the object.
 
-        Returns:
+        Returns
+        -------
             True if the object exists, False otherwise.
         """
-    async def list(
-        self, path: PathBuf, *, start_after: str | None = None
-    ) -> AsyncIterable[Entry]:
-        """List the objects at the given path.
-
-        Args:
-            path (str|Path): The path to the directory.
-            start_after (str | None): The key to start listing from.
-
-        Returns:
-            An iterable of entries representing the objects in the directory.
-        """
-    async def scan(self, path: PathBuf) -> AsyncIterable[Entry]:
+    async def list(self, path: PathBuf, **kwargs) -> AsyncIterable[Entry]:
+        """List objects at the given path.
+
+        Args:
+            path (str | Path): The path to the directory/ prefix.
+            **kwargs (Any): Optional listing parameters matching the
+                [OpenDAL 
`ListOptions`](https://opendal.apache.org/docs/rust/opendal/options/struct.ListOptions.html):
+
+                - limit (int): The limit passed to the underlying service to 
specify the
+                    max results that could return per-request. Users could use 
this to
+                    control the memory usage of list operation. If not set, 
all matching
+                    entries will be listed.
+                - start_after (str): Start listing after this key. Useful for 
pagination
+                    or resuming interrupted listings.
+                - recursive (bool): Whether to list entries recursively 
through all
+                    subdirectories. If False, lists only top-level entries 
(entries
+                    under the given path).
+                - versions (bool): Whether to include all versions of objects, 
if the
+                    underlying service supports versioning.
+                - deleted (bool): Whether to include deleted objects, if the 
underlying
+                    service supports soft-deletes or versioning.
+
+        Returns
+        -------
+            Iterable[Entry]: An iterable of entries representing the objects 
in the
+                directory or prefix.
+        """
+    @deprecated("Use `list()` instead.")
+    async def scan(self, path: PathBuf, **kwargs) -> AsyncIterable[Entry]:
         """Scan the objects at the given path recursively.
 
+
         Args:
-            path (str|Path): The path to the directory.
+            path (str | Path): The path to the directory/ prefix.
+            **kwargs (Any): Optional listing parameters matching the
+                [OpenDAL 
`ListOptions`](https://opendal.apache.org/docs/rust/opendal/options/struct.ListOptions.html),
+                excluding `recursive` which is always enforced as `True`
 
-        Returns:
-            An iterable of entries representing the objects in the directory.
+        Returns
+        -------
+            Iterable[Entry]: An iterable of all entries under the given path,
+                recursively traversing all subdirectories. Each entry 
represents
+                an object (e.g., file or directory) discovered within the full
+                descendant hierarchy of the specified path.
         """
     async def presign_stat(self, path: PathBuf, expire_second: int) -> 
PresignedRequest:
         """Generate a presigned URL for stat operation.
@@ -381,7 +481,8 @@ class AsyncOperator(_Base):
             path (str|Path): The path to the object.
             expire_second (int): The expiration time in seconds.
 
-        Returns:
+        Returns
+        -------
             A presigned request object.
         """
     async def presign_read(self, path: PathBuf, expire_second: int) -> 
PresignedRequest:
@@ -391,7 +492,8 @@ class AsyncOperator(_Base):
             path (str|Path): The path to the object.
             expire_second (int): The expiration time in seconds.
 
-        Returns:
+        Returns
+        -------
             A presigned request object.
         """
     async def presign_write(
@@ -403,7 +505,8 @@ class AsyncOperator(_Base):
             path (str|Path): The path to the object.
             expire_second (int): The expiration time in seconds.
 
-        Returns:
+        Returns
+        -------
             A presigned request object.
         """
     async def presign_delete(
@@ -415,7 +518,8 @@ class AsyncOperator(_Base):
             path (str|Path): The path to the object.
             expire_second (int): The expiration time in seconds.
 
-        Returns:
+        Returns
+        -------
             A presigned request object.
         """
     def capability(self) -> Capability: ...
@@ -448,13 +552,15 @@ class File:
 
     Created by the `open` method of the `Operator` class.
     """
+
     def read(self, size: int | None = None) -> bytes:
         """Read the content of the file.
 
         Args:
             size (int): The number of bytes to read. If None, read all.
 
-        Returns:
+        Returns
+        -------
             The content of the file as bytes.
         """
     def readline(self, size: int | None = None) -> bytes:
@@ -463,7 +569,8 @@ class File:
         Args:
             size (int): The number of bytes to read. If None, read until 
newline.
 
-        Returns:
+        Returns
+        -------
             The line read from the file as bytes.
         """
     def write(self, bs: bytes) -> None:
@@ -479,13 +586,15 @@ class File:
             pos (int): The position to set.
             whence (int): The reference point for the position. Can be 0, 1, 
or 2.
 
-        Returns:
+        Returns
+        -------
             The new position in the file.
         """
     def tell(self) -> int:
         """Get the current position in the file.
 
-        Returns:
+        Returns
+        -------
             The current position in the file.
         """
     def close(self) -> None:
@@ -512,7 +621,8 @@ class File:
         Args:
             buffer (bytes|bytearray): The buffer to read into.
 
-        Returns:
+        Returns
+        -------
             The number of bytes read.
         """
     def seekable(self) -> bool:
@@ -527,13 +637,15 @@ class AsyncFile:
 
     Created by the `open` method of the `AsyncOperator` class.
     """
+
     async def read(self, size: int | None = None) -> bytes:
         """Read the content of the file.
 
         Args:
             size (int): The number of bytes to read. If None, read all.
 
-        Returns:
+        Returns
+        -------
             The content of the file as bytes.
         """
     async def write(self, bs: bytes) -> None:
@@ -549,13 +661,15 @@ class AsyncFile:
             pos (int): The position to set.
             whence (int): The reference point for the position. Can be 0, 1, 
or 2.
 
-        Returns:
+        Returns
+        -------
             The new position in the file.
         """
     async def tell(self) -> int:
         """Get the current position in the file.
 
-        Returns:
+        Returns
+        -------
             The current position in the file.
         """
     async def close(self) -> None:
@@ -582,9 +696,13 @@ class AsyncFile:
 @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:
@@ -601,11 +719,29 @@ class Metadata:
     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:
diff --git a/bindings/python/python/opendal/exceptions.pyi 
b/bindings/python/python/opendal/exceptions.pyi
index 89f6311d6..dd18e34a5 100644
--- a/bindings/python/python/opendal/exceptions.pyi
+++ b/bindings/python/python/opendal/exceptions.pyi
@@ -19,31 +19,31 @@ class Error(Exception):
     """Base class for exceptions in this module."""
 
 class Unexpected(Error):
-    """Unexpected errors"""
+    """Unexpected errors."""
 
 class Unsupported(Error):
-    """Unsupported operation"""
+    """Unsupported operation."""
 
 class ConfigInvalid(Error):
-    """Config is invalid"""
+    """Config is invalid."""
 
 class NotFound(Error):
-    """Not found"""
+    """Not found."""
 
 class PermissionDenied(Error):
-    """Permission denied"""
+    """Permission denied."""
 
 class IsADirectory(Error):
-    """Is a directory"""
+    """Is a directory."""
 
 class NotADirectory(Error):
-    """Not a directory"""
+    """Not a directory."""
 
 class AlreadyExists(Error):
-    """Already exists"""
+    """Already exists."""
 
 class IsSameFile(Error):
-    """Is same file"""
+    """Is same file."""
 
 class ConditionNotMatch(Error):
-    """Condition not match"""
+    """Condition not match."""
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 456c3114e..37001dfca 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -55,6 +55,8 @@ fn _opendal(py: Python, m: &Bound<'_, PyModule>) -> 
PyResult<()> {
 
     m.add_class::<WriteOptions>()?;
     m.add_class::<ReadOptions>()?;
+    m.add_class::<ListOptions>()?;
+    m.add_class::<StatOptions>()?;
 
     // Layer module
     let layers_module = PyModule::new(py, "layers")?;
diff --git a/bindings/python/src/metadata.rs b/bindings/python/src/metadata.rs
index bf3da4520..f7bdd8a14 100644
--- a/bindings/python/src/metadata.rs
+++ b/bindings/python/src/metadata.rs
@@ -17,6 +17,7 @@
 
 use chrono::prelude::*;
 use pyo3::prelude::*;
+use std::collections::HashMap;
 
 use crate::*;
 
@@ -31,18 +32,28 @@ impl Entry {
 
 #[pymethods]
 impl Entry {
-    /// Path of entry. Path is relative to operator's root.
+    /// Path of entry. Path is relative to the operator's root.
     #[getter]
     pub fn path(&self) -> &str {
         self.0.path()
     }
 
+    /// Metadata of entry.
+    #[getter]
+    pub fn metadata(&self) -> Metadata {
+        Metadata::new(self.0.metadata().clone())
+    }
+
     fn __str__(&self) -> &str {
         self.0.path()
     }
 
     fn __repr__(&self) -> String {
-        format!("Entry({:?})", self.0.path())
+        format!(
+            "Entry(path={:?}, metadata={})",
+            self.path(),
+            self.metadata().__repr__()
+        )
     }
 }
 
@@ -80,37 +91,72 @@ impl Metadata {
         self.0.content_type()
     }
 
+    /// Content Type of this entry.
+    #[getter]
+    pub fn content_encoding(&self) -> Option<&str> {
+        self.0.content_encoding()
+    }
+
     /// ETag of this entry.
     #[getter]
     pub fn etag(&self) -> Option<&str> {
         self.0.etag()
     }
 
-    /// mode represent this entry's mode.
+    /// mode represents this entry's mode.
     #[getter]
     pub fn mode(&self) -> EntryMode {
         EntryMode(self.0.mode())
     }
 
+    /// Returns `true` if this metadata is for a file.
+    #[getter]
+    pub fn is_file(&self) -> bool {
+        self.mode().is_file()
+    }
+
+    /// Returns `true` if this metadata is for a directory.
+    #[getter]
+    pub fn is_dir(&self) -> bool {
+        self.mode().is_dir()
+    }
+
     /// Last modified time
     #[getter]
     pub fn last_modified(&self) -> Option<DateTime<Utc>> {
         self.0.last_modified()
     }
-    pub fn __repr__(&self) -> String {
-        let last_modified_str = match self.0.last_modified() {
-            Some(dt) => dt.format("%Y-%m-%dT%H:%M:%S").to_string(),
-            None => "None".to_string(),
-        };
 
-        format!(
-            "Metadata(mode={}, content_length={}, content_type={}, 
last_modified={}, etag={})",
-            self.0.mode(),
-            self.0.content_length(),
-            self.0.content_type().unwrap_or("None"),
-            last_modified_str,
-            self.0.etag().unwrap_or("None"),
-        )
+    /// Version of this entry, if available.
+    #[getter]
+    pub fn version(&self) -> Option<&str> {
+        self.0.version()
+    }
+
+    /// User defined metadata of this entry
+    #[getter]
+    pub fn user_metadata(&self) -> Option<&HashMap<String, String>> {
+        self.0.user_metadata()
+    }
+
+    pub fn __repr__(&self) -> String {
+        let mut parts = vec![];
+
+        parts.push(format!("mode={}", self.0.mode()));
+        parts.push(format!(
+            "content_disposition={:?}",
+            self.0.content_disposition()
+        ));
+        parts.push(format!("content_length={}", self.0.content_length()));
+        parts.push(format!("content_md5={:?}", self.0.content_md5()));
+        parts.push(format!("content_type={:?}", self.0.content_type()));
+        parts.push(format!("content_encoding={:?}", 
self.0.content_encoding()));
+        parts.push(format!("etag={:?}", self.0.etag()));
+        parts.push(format!("last_modified={:?}", self.0.last_modified()));
+        parts.push(format!("version={:?}", self.0.version()));
+        parts.push(format!("user_metadata={:?}", self.0.user_metadata()));
+
+        format!("Metadata({})", parts.join(", "))
     }
 }
 
diff --git a/bindings/python/src/operator.rs b/bindings/python/src/operator.rs
index 166dbe5d9..b281b28ed 100644
--- a/bindings/python/src/operator.rs
+++ b/bindings/python/src/operator.rs
@@ -169,11 +169,16 @@ impl Operator {
             .map_err(format_pyerr)
     }
 
-    /// Get the current path's metadata **without cache** directly.
-    pub fn stat(&self, path: PathBuf) -> PyResult<Metadata> {
+    /// Get metadata for the current path **without cache** directly.
+    #[pyo3(signature = (path, **kwargs))]
+    pub fn stat(&self, path: PathBuf, kwargs: Option<&Bound<PyDict>>) -> 
PyResult<Metadata> {
         let path = path.to_string_lossy().to_string();
+        let kwargs = kwargs
+            .map(|v| v.extract::<StatOptions>())
+            .transpose()?
+            .unwrap_or_default();
         self.core
-            .stat(&path)
+            .stat_options(&path, kwargs.into())
             .map_err(format_pyerr)
             .map(Metadata::new)
     }
@@ -236,36 +241,35 @@ impl Operator {
     }
 
     /// List current dir path.
-    #[pyo3(signature = (path, *, start_after=None))]
-    pub fn list(&self, path: PathBuf, start_after: Option<String>) -> 
PyResult<BlockingLister> {
+    #[pyo3(signature = (path, **kwargs))]
+    pub fn list(&self, path: PathBuf, kwargs: Option<&Bound<PyDict>>) -> 
PyResult<BlockingLister> {
         let path = path.to_string_lossy().to_string();
+
+        let kwargs = kwargs
+            .map(|v| v.extract::<ListOptions>())
+            .transpose()?
+            .unwrap_or_default();
+
         let l = self
             .core
-            .lister_options(
-                &path,
-                ocore::options::ListOptions {
-                    start_after,
-                    ..Default::default()
-                },
-            )
+            .lister_options(&path, kwargs.into())
             .map_err(format_pyerr)?;
         Ok(BlockingLister::new(l))
     }
 
     /// List dir in a flat way.
-    pub fn scan(&self, path: PathBuf) -> PyResult<BlockingLister> {
-        let path = path.to_string_lossy().to_string();
-        let l = self
-            .core
-            .lister_options(
-                &path,
-                ocore::options::ListOptions {
-                    recursive: true,
-                    ..Default::default()
-                },
-            )
-            .map_err(format_pyerr)?;
-        Ok(BlockingLister::new(l))
+    #[pyo3(signature = (path, **kwargs))]
+    pub fn scan<'p>(
+        &self,
+        py: Python<'p>,
+        path: PathBuf,
+        kwargs: Option<&Bound<PyDict>>,
+    ) -> PyResult<BlockingLister> {
+        let d = PyDict::new(py);
+        let kwargs = kwargs.unwrap_or(&d);
+        kwargs.set_item("recursive", true)?;
+
+        self.list(path, Some(kwargs))
     }
 
     pub fn capability(&self) -> PyResult<capability::Capability> {
@@ -466,13 +470,24 @@ impl AsyncOperator {
         })
     }
 
-    /// Get current path's metadata **without cache** directly.
-    pub fn stat<'p>(&'p self, py: Python<'p>, path: PathBuf) -> 
PyResult<Bound<'p, PyAny>> {
+    /// Get metadata for the current path **without cache** directly.
+    #[pyo3(signature = (path, **kwargs))]
+    pub fn stat<'p>(
+        &'p self,
+        py: Python<'p>,
+        path: PathBuf,
+        kwargs: Option<&Bound<PyDict>>,
+    ) -> PyResult<Bound<'p, PyAny>> {
         let this = self.core.clone();
         let path = path.to_string_lossy().to_string();
+        let kwargs = kwargs
+            .map(|v| v.extract::<StatOptions>())
+            .transpose()?
+            .unwrap_or_default();
+
         future_into_py(py, async move {
             let res: Metadata = this
-                .stat(&path)
+                .stat_options(&path, kwargs.into())
                 .await
                 .map_err(format_pyerr)
                 .map(Metadata::new)?;
@@ -575,38 +590,44 @@ impl AsyncOperator {
     }
 
     /// List current dir path.
-    #[pyo3(signature = (path, *, start_after=None))]
+    #[pyo3(signature = (path, **kwargs))]
     pub fn list<'p>(
         &'p self,
         py: Python<'p>,
         path: PathBuf,
-        start_after: Option<String>,
+        kwargs: Option<&Bound<PyDict>>,
     ) -> PyResult<Bound<'p, PyAny>> {
         let this = self.core.clone();
         let path = path.to_string_lossy().to_string();
+        let kwargs = kwargs
+            .map(|v| v.extract::<ListOptions>())
+            .transpose()?
+            .unwrap_or_default();
+
         future_into_py(py, async move {
-            let mut builder = this.lister_with(&path);
-            if let Some(start_after) = start_after {
-                builder = builder.start_after(&start_after);
-            }
-            let lister = builder.await.map_err(format_pyerr)?;
+            let lister = this
+                .lister_options(&path, kwargs.into())
+                .await
+                .map_err(format_pyerr)?;
             let pylister = Python::with_gil(|py| 
AsyncLister::new(lister).into_py_any(py))?;
 
             Ok(pylister)
         })
     }
 
-    /// List dir in flat way.
-    pub fn scan<'p>(&'p self, py: Python<'p>, path: PathBuf) -> 
PyResult<Bound<'p, PyAny>> {
-        let this = self.core.clone();
-        let path = path.to_string_lossy().to_string();
-        future_into_py(py, async move {
-            let builder = this.lister_with(&path).recursive(true);
-            let lister = builder.await.map_err(format_pyerr)?;
-            let pylister: PyObject =
-                Python::with_gil(|py| 
AsyncLister::new(lister).into_py_any(py))?;
-            Ok(pylister)
-        })
+    /// List dir in a flat way.
+    #[pyo3(signature = (path, **kwargs))]
+    pub fn scan<'p>(
+        &'p self,
+        py: Python<'p>,
+        path: PathBuf,
+        kwargs: Option<&Bound<PyDict>>,
+    ) -> PyResult<Bound<'p, PyAny>> {
+        let d = PyDict::new(py);
+        let kwargs = kwargs.unwrap_or(&d);
+        kwargs.set_item("recursive", true)?;
+
+        self.list(py, path, Some(kwargs))
     }
 
     /// Presign an operation for stat(head) which expires after 
`expire_second` seconds.
diff --git a/bindings/python/src/options.rs b/bindings/python/src/options.rs
index f47fdb147..6e5953c01 100644
--- a/bindings/python/src/options.rs
+++ b/bindings/python/src/options.rs
@@ -122,3 +122,53 @@ impl From<WriteOptions> for ocore::options::WriteOptions {
         }
     }
 }
+
+#[pyclass(module = "opendal")]
+#[derive(FromPyObject, Default, Debug)]
+pub struct ListOptions {
+    pub limit: Option<usize>,
+    pub start_after: Option<String>,
+    pub recursive: Option<bool>,
+    pub versions: Option<bool>,
+    pub deleted: Option<bool>,
+}
+
+impl From<ListOptions> for ocore::options::ListOptions {
+    fn from(opts: ListOptions) -> Self {
+        Self {
+            limit: opts.limit,
+            start_after: opts.start_after,
+            recursive: opts.recursive.unwrap_or(false),
+            versions: opts.versions.unwrap_or(false),
+            deleted: opts.deleted.unwrap_or(false),
+        }
+    }
+}
+
+#[pyclass(module = "opendal")]
+#[derive(FromPyObject, Default, Debug)]
+pub struct StatOptions {
+    pub version: Option<String>,
+    pub if_match: Option<String>,
+    pub if_none_match: Option<String>,
+    pub if_modified_since: Option<DateTime<Utc>>,
+    pub if_unmodified_since: Option<DateTime<Utc>>,
+    pub content_type: Option<String>,
+    pub cache_control: Option<String>,
+    pub content_disposition: Option<String>,
+}
+
+impl From<StatOptions> for ocore::options::StatOptions {
+    fn from(opts: StatOptions) -> Self {
+        Self {
+            version: opts.version,
+            if_match: opts.if_match,
+            if_none_match: opts.if_none_match,
+            if_modified_since: opts.if_modified_since,
+            if_unmodified_since: opts.if_unmodified_since,
+            override_content_type: opts.content_type,
+            override_cache_control: opts.cache_control,
+            override_content_disposition: opts.content_disposition,
+        }
+    }
+}
diff --git a/bindings/python/tests/test_async_pickle_types.py 
b/bindings/python/tests/test_async_pickle_types.py
index fca814ccf..4462e739e 100644
--- a/bindings/python/tests/test_async_pickle_types.py
+++ b/bindings/python/tests/test_async_pickle_types.py
@@ -26,10 +26,7 @@ import pytest
 @pytest.mark.asyncio
 @pytest.mark.need_capability("read", "write", "delete", "shared")
 async def test_operator_pickle(service_name, operator, async_operator):
-    """
-    Test AsyncOperator's pickle serialization and deserialization.
-    """
-
+    """Test AsyncOperator's pickle serialization and deserialization."""
     size = randint(1, 1024)
     filename = f"random_file_{str(uuid4())}"
     content = os.urandom(size)
diff --git a/bindings/python/tests/test_pickle_rw.py 
b/bindings/python/tests/test_pickle_rw.py
index 575a13881..3c4d538a2 100644
--- a/bindings/python/tests/test_pickle_rw.py
+++ b/bindings/python/tests/test_pickle_rw.py
@@ -24,9 +24,7 @@ import pytest
 
 @pytest.mark.need_capability("read", "write", "delete")
 def test_sync_file_pickle(service_name, operator, async_operator):
-    """
-    Test pickle streaming serialization and deserialization using OpenDAL 
operator.
-    """
+    """Test pickle streaming serialization and deserialization using 
operator."""
     data = {
         "a": 1,
         "b": "hello",
@@ -34,7 +32,7 @@ def test_sync_file_pickle(service_name, operator, 
async_operator):
         "d": {"e": 4},
         "f": None,
         "g": b"hello\nworld",
-        "h": set([1, 2, 3]),
+        "h": {1, 2, 3},
         "i": 1.23,
         "j": True,
         "k": datetime.strptime("2024-01-01", "%Y-%m-%d"),
diff --git a/bindings/python/tests/test_sync_pickle_types.py 
b/bindings/python/tests/test_sync_pickle_types.py
index ef1467c62..bc360adfa 100644
--- a/bindings/python/tests/test_sync_pickle_types.py
+++ b/bindings/python/tests/test_sync_pickle_types.py
@@ -25,10 +25,7 @@ import pytest
 
 @pytest.mark.need_capability("read", "write", "delete", "shared")
 def test_operator_pickle(service_name, operator, async_operator):
-    """
-    Test Operator's pickle serialization and deserialization.
-    """
-
+    """Test Operator's pickle serialization and deserialization."""
     size = randint(1, 1024)
     filename = f"random_file_{str(uuid4())}"
     content = os.urandom(size)
diff --git a/bindings/python/tests/test_write.py 
b/bindings/python/tests/test_write.py
index 571d06344..b13026875 100644
--- a/bindings/python/tests/test_write.py
+++ b/bindings/python/tests/test_write.py
@@ -37,19 +37,6 @@ def test_sync_write(service_name, operator, async_operator):
     assert metadata.mode.is_file()
     assert metadata.content_length == size
 
-    last_modified = (
-        metadata.last_modified.strftime("%Y-%m-%dT%H:%M:%S")
-        if metadata.last_modified
-        else None
-    )
-    assert repr(metadata) == (
-        "Metadata(mode=file, "
-        f"content_length={metadata.content_length}, "
-        f"content_type={metadata.content_type}, "
-        f"last_modified={last_modified}, "
-        f"etag={metadata.etag})"
-    )
-
     operator.delete(filename)
 
 
@@ -65,19 +52,6 @@ def test_sync_write_path(service_name, operator, 
async_operator):
     assert metadata.mode.is_file()
     assert metadata.content_length == size
 
-    last_modified = (
-        metadata.last_modified.strftime("%Y-%m-%dT%H:%M:%S")
-        if metadata.last_modified
-        else None
-    )
-    assert repr(metadata) == (
-        "Metadata(mode=file, "
-        f"content_length={metadata.content_length}, "
-        f"content_type={metadata.content_type}, "
-        f"last_modified={last_modified}, "
-        f"etag={metadata.etag})"
-    )
-
     operator.delete(filename)
 
 


Reply via email to