From 4ff8106d97ec1337ece1396ce866689150adc10c Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Thu, 2 Nov 2023 19:58:11 +0800 Subject: [PATCH] feat(binding/python): Support rename API for Python binding Signed-off-by: Manjusaka --- bindings/python/python/opendal/__init__.pyi | 2 + bindings/python/src/asyncio.rs | 13 +++ bindings/python/src/lib.rs | 4 + bindings/python/tests/conftest.py | 8 +- bindings/python/tests/test_async_rename.py | 112 ++++++++++++++++++++ bindings/python/tests/test_sync_rename.py | 105 ++++++++++++++++++ 6 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 bindings/python/tests/test_async_rename.py create mode 100644 bindings/python/tests/test_sync_rename.py diff --git a/bindings/python/python/opendal/__init__.pyi b/bindings/python/python/opendal/__init__.pyi index 4d7f8a7eec8a..dcb7faf8d7e4 100644 --- a/bindings/python/python/opendal/__init__.pyi +++ b/bindings/python/python/opendal/__init__.pyi @@ -41,6 +41,7 @@ class Operator: def scan(self, path: str) -> Iterable[Entry]: ... def capability(self) -> Capability: ... def copy(self, source: str, target: str): ... + def rename(self, source: str, target: str): ... class AsyncOperator: def __init__(self, scheme: str, **kwargs): ... @@ -69,6 +70,7 @@ class AsyncOperator: ) -> PresignedRequest: ... def capability(self) -> Capability: ... async def copy(self, source: str, target: str): ... + async def rename(self, source: str, target: str): ... class Reader: def read(self, size: Optional[int] = None) -> memoryview: ... diff --git a/bindings/python/src/asyncio.rs b/bindings/python/src/asyncio.rs index 12c9eefee903..027a73f98bef 100644 --- a/bindings/python/src/asyncio.rs +++ b/bindings/python/src/asyncio.rs @@ -158,6 +158,19 @@ impl AsyncOperator { }) } + /// Rename filename + pub fn rename<'p>( + &'p self, + py: Python<'p>, + source: String, + target: String, + ) -> PyResult<&'p PyAny> { + let this = self.0.clone(); + future_into_py(py, async move { + this.rename(&source, &target).await.map_err(format_pyerr) + }) + } + /// Create a dir at given path. /// /// # Notes diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index dee9db8f4a4b..65dc8fdb6aa0 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -181,6 +181,10 @@ impl Operator { self.0.copy(source, target).map_err(format_pyerr) } + /// Rename filename. + pub fn rename(&self, source: &str, target: &str) -> PyResult<()> { + self.0.rename(source, target).map_err(format_pyerr) + } /// Create a dir at given path. /// /// # Notes diff --git a/bindings/python/tests/conftest.py b/bindings/python/tests/conftest.py index bc831de6919c..970bb3ba5b38 100644 --- a/bindings/python/tests/conftest.py +++ b/bindings/python/tests/conftest.py @@ -56,12 +56,16 @@ def setup_config(service_name): @pytest.fixture() def operator(service_name, setup_config): - return opendal.Operator(service_name, **setup_config).layer(opendal.layers.RetryLayer()) + return opendal.Operator(service_name, **setup_config).layer( + opendal.layers.RetryLayer() + ) @pytest.fixture() def async_operator(service_name, setup_config): - return opendal.AsyncOperator(service_name, **setup_config).layer(opendal.layers.RetryLayer()) + return opendal.AsyncOperator(service_name, **setup_config).layer( + opendal.layers.RetryLayer() + ) @pytest.fixture(autouse=True) diff --git a/bindings/python/tests/test_async_rename.py b/bindings/python/tests/test_async_rename.py new file mode 100644 index 000000000000..77ed1fc32256 --- /dev/null +++ b/bindings/python/tests/test_async_rename.py @@ -0,0 +1,112 @@ +# 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. + +import os +from random import randint +from uuid import uuid4 + +import pytest + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "rename") +async def test_async_rename_file(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}" + await async_operator.rename(source_path, target_path) + with pytest.raises(FileNotFoundError) as e_info: + await async_operator.read(source_path) + assert await async_operator.read(target_path) == content + await async_operator.delete(target_path) + await async_operator.delete(source_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "rename") +async def test_async_rename_non_exists_file(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(FileNotFoundError) as e_info: + await async_operator.rename(source_path, target_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "rename") +async def test_async_rename_directory(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}/" + await async_operator.create_dir(source_path) + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(Exception) as e_info: + await async_operator.rename(source_path, target_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "rename") +async def test_async_rename_file_to_directory(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}/" + with pytest.raises(Exception) as e_info: + await async_operator.rename(source_path, target_path) + await async_operator.delete(source_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "rename") +async def test_async_rename_self(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + with pytest.raises(Exception) as e_info: + await async_operator.rename(source_path, source_path) + await async_operator.delete(source_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "rename") +async def test_async_rename_nested(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}/{str(uuid4())}/{str(uuid4())}" + await async_operator.rename(source_path, target_path) + with pytest.raises(FileNotFoundError) as e_info: + await async_operator.read(source_path) + assert await async_operator.read(target_path) == content + await async_operator.delete(target_path) + await async_operator.delete(source_path) + + +@pytest.mark.asyncio +@pytest.mark.need_capability("read", "write", "rename") +async def test_async_rename_overwrite(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + source_content = os.urandom(1024) + target_content = os.urandom(1024) + assert source_content != target_content + await async_operator.write(source_path, source_content) + await async_operator.write(target_path, target_content) + await async_operator.rename(source_path, target_path) + with pytest.raises(Exception) as e_info: + await async_operator.read(source_content) + assert await async_operator.read(target_path) == source_content + await async_operator.delete(target_path) + await async_operator.delete(source_path) diff --git a/bindings/python/tests/test_sync_rename.py b/bindings/python/tests/test_sync_rename.py new file mode 100644 index 000000000000..02def280570f --- /dev/null +++ b/bindings/python/tests/test_sync_rename.py @@ -0,0 +1,105 @@ +# 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. + +import os +from random import randint +from uuid import uuid4 + +import pytest + + +@pytest.mark.need_capability("read", "write", "rename") +def test_sync_rename_file(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}" + operator.rename(source_path, target_path) + with pytest.raises(FileNotFoundError) as e_info: + operator.read(source_path) + assert operator.read(target_path) == content + operator.delete(target_path) + operator.delete(source_path) + + +@pytest.mark.need_capability("read", "write", "rename") +def test_sync_rename_non_exists_file(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(FileNotFoundError) as e_info: + operator.rename(source_path, target_path) + + +@pytest.mark.need_capability("read", "write", "rename") +def test_sync_rename_directory(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}/" + operator.create_dir(source_path) + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(Exception) as e_info: + operator.rename(source_path, target_path) + + +@pytest.mark.need_capability("read", "write", "rename") +def test_sync_rename_file_to_directory(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}/" + with pytest.raises(Exception) as e_info: + operator.rename(source_path, target_path) + operator.delete(source_path) + + +@pytest.mark.need_capability("read", "write", "rename") +def test_sync_rename_self(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + with pytest.raises(Exception) as e_info: + operator.rename(source_path, source_path) + operator.delete(source_path) + + +@pytest.mark.need_capability("read", "write", "rename") +def test_sync_rename_nested(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + target_path = f"random_file_{str(uuid4())}/{str(uuid4())}/{str(uuid4())}" + operator.rename(source_path, target_path) + with pytest.raises(FileNotFoundError) as e_info: + operator.read(source_path) + assert operator.read(target_path) == content + operator.delete(target_path) + operator.delete(source_path) + + +@pytest.mark.need_capability("read", "write", "rename") +def test_sync_rename_overwrite(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + source_content = os.urandom(1024) + target_content = os.urandom(1024) + assert source_content != target_content + operator.write(source_path, source_content) + operator.write(target_path, target_content) + operator.rename(source_path, target_path) + with pytest.raises(Exception) as e_info: + operator.read(source_content) + assert operator.read(target_path) == source_content + operator.delete(target_path) + operator.delete(source_path)