Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
52 changes: 52 additions & 0 deletions bindings/python/python/opendal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,58 @@
# specific language governing permissions and limitations
# under the License.

from collections.abc import Iterable
import os
from typing import Optional

from ._opendal import *

__all__ = _opendal.__all__

_PATH_TYPES = (str, bytes, os.PathLike)


def _coerce_many_paths(path: object) -> Optional[list[str]]:
if isinstance(path, _PATH_TYPES):
return None

try:
iterator = iter(path) # type: ignore[arg-type]
except TypeError:
return None

paths = list(iterator)
if any(not isinstance(p, _PATH_TYPES) for p in paths):
raise TypeError("all paths must be str, bytes, or os.PathLike objects")
return paths


_blocking_delete = Operator.delete


def _operator_delete(self: Operator, path: object) -> None:
paths = _coerce_many_paths(path)
if paths is None:
_blocking_delete(self, path)
return
if not paths:
return
self.delete_many(paths)


Operator.delete = _operator_delete # type: ignore[assignment]

_async_delete = AsyncOperator.delete


async def _async_operator_delete(self: AsyncOperator, path: object) -> None:
paths = _coerce_many_paths(path)
if paths is None:
await _async_delete(self, path) # type: ignore[arg-type]
return
if not paths:
return
await self.delete_many(paths)


AsyncOperator.delete = _async_operator_delete # type: ignore[assignment]
14 changes: 8 additions & 6 deletions bindings/python/python/opendal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,12 @@ class Operator(_Base):
Args:
path (str|Path): The path to the directory.
"""
def delete(self, path: PathBuf) -> None:
"""Delete the object at the given path.
def delete(self, path: PathBuf | Iterable[PathBuf]) -> None:
"""Delete one object or multiple objects.

Args:
path (str|Path): The path to the object.
path (str | Path | Iterable[str | Path]): Either a single path or
an iterable of paths to delete.
"""
def exists(self, path: PathBuf) -> bool:
"""Check if the object at the given path exists.
Expand Down Expand Up @@ -413,11 +414,12 @@ class AsyncOperator(_Base):
Args:
path (str|Path): The path to the directory.
"""
async def delete(self, path: PathBuf) -> None:
"""Delete the object at the given path.
async def delete(self, path: PathBuf | Iterable[PathBuf]) -> None:
"""Delete one object or multiple objects.

Args:
path (str|Path): The path to the object.
path (str | Path | Iterable[str | Path]): Either a single path or
an iterable of paths to delete.
"""
async def exists(self, path: PathBuf) -> bool:
"""Check if the object at the given path exists.
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(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(targets)

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