Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions bindings/python/python/opendal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ class Operator(_Base):
-------
None
"""
def delete_many(self, paths: Iterable[PathBuf]) -> None:
"""Delete multiple objects in a single request.

Args:
paths (Iterable[str | Path]): Collection of object paths to delete.
Each element is treated the same as calling :py:meth:`delete`
individually.

Notes
-----
Missing objects are ignored by default.
"""
def stat(self, path: PathBuf, **kwargs) -> Metadata:
"""Get the metadata of the object at the given path.

Expand Down Expand Up @@ -381,6 +393,18 @@ class AsyncOperator(_Base):
-------
None
"""
async def delete_many(self, paths: Iterable[PathBuf]) -> None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could just make delete accept this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated it according to your suggestion, but I feel the updated code is a bit complex.

There are two ways to achieve this goal:

Use the current approach in the PR, hack the function in the Python layer to do forwarding.
Accept a generic object as a parameter in Rust, then check the PyType type in Rust and forward accordingly.
Both methods significantly impact readability and seem to introduce unnecessary complexity. If you agree, I can roll back this PR.

"""Delete multiple objects in a single request.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no guarantee about this.


Args:
paths (Iterable[str | Path]): Collection of object paths to delete.
Each element is treated the same as calling :py:meth:`delete`
individually.

Notes
-----
Missing objects are ignored by default.
"""
async def stat(self, path: PathBuf, **kwargs) -> Metadata:
"""Get the metadata of the object at the given path.

Expand Down
31 changes: 31 additions & 0 deletions bindings/python/src/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,18 @@ impl Operator {
self.core.delete(&path).map_err(format_pyerr)
}

/// Delete multiple paths in a single call.
///
/// Accepts any iterable of path-like objects. Paths that do not exist are ignored.
pub fn delete_many(&self, paths: Vec<PathBuf>) -> PyResult<()> {
let paths: Vec<String> = paths
.into_iter()
.map(|path| path.to_string_lossy().to_string())
.collect();

self.core.delete_iter(paths).map_err(format_pyerr)
}

/// Checks if the given path exists.
///
/// # Notes
Expand Down Expand Up @@ -577,6 +589,25 @@ impl AsyncOperator {
)
}

/// Delete multiple paths in a single call.
///
/// Accepts any iterable of path-like objects. Paths that do not exist are ignored.
pub fn delete_many<'p>(
&'p self,
py: Python<'p>,
paths: Vec<PathBuf>,
) -> PyResult<Bound<'p, PyAny>> {
let this = self.core.clone();
let paths: Vec<String> = paths
.into_iter()
.map(|path| path.to_string_lossy().to_string())
.collect();

future_into_py(py, async move {
this.delete_iter(paths).await.map_err(format_pyerr)
})
}

/// Check given path is exists.
///
/// # Notes
Expand Down
15 changes: 15 additions & 0 deletions bindings/python/tests/test_async_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,18 @@ async def test_async_remove_all(service_name, operator, async_operator):
with pytest.raises(NotFound):
await async_operator.read(f"{parent}/{path}")
await async_operator.remove_all(f"{parent}/")


@pytest.mark.asyncio
@pytest.mark.need_capability("read", "write", "delete")
async def test_async_delete_many(service_name, operator, async_operator):
parent = f"delete_many_{str(uuid4())}"
targets = [f"{parent}/file_{idx}.txt" for idx in range(3)]

for path in targets:
await async_operator.write(path, os.urandom(16))

await async_operator.delete_many(targets)

for path in targets:
assert not await async_operator.exists(path)
14 changes: 14 additions & 0 deletions bindings/python/tests/test_sync_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,17 @@ def test_sync_remove_all(service_name, operator, async_operator):
with pytest.raises(NotFound):
operator.read(f"{parent}/{path}")
operator.remove_all(f"{parent}/")


@pytest.mark.need_capability("read", "write", "delete")
def test_sync_delete_many(service_name, operator, async_operator):
parent = f"delete_many_{str(uuid4())}"
targets = [f"{parent}/file_{idx}.txt" for idx in range(3)]

for path in targets:
operator.write(path, os.urandom(16))

operator.delete_many(targets)

for path in targets:
assert not operator.exists(path)
Loading