This is an automated email from the ASF dual-hosted git repository. manjusaka pushed a commit to branch manjusaka/python-support-copy in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
commit f47d4a821a66a7876c91d7e75c7c75a1e822e383 Author: Manjusaka <[email protected]> AuthorDate: Wed Nov 1 19:21:49 2023 +0800 feat(binding/python): Support Copy operation for Python binding Signed-off-by: Manjusaka <[email protected]> --- bindings/python/python/opendal/__init__.pyi | 2 + bindings/python/src/asyncio.rs | 9 +++ bindings/python/src/lib.rs | 5 ++ bindings/python/tests/test_async_copy.py | 114 ++++++++++++++++++++++++++++ bindings/python/tests/test_sync_copy.py | 107 ++++++++++++++++++++++++++ 5 files changed, 237 insertions(+) diff --git a/bindings/python/python/opendal/__init__.pyi b/bindings/python/python/opendal/__init__.pyi index 55164c816..df8134e3e 100644 --- a/bindings/python/python/opendal/__init__.pyi +++ b/bindings/python/python/opendal/__init__.pyi @@ -39,6 +39,7 @@ class Operator: def list(self, path: str) -> Iterable[Entry]: ... def scan(self, path: str) -> Iterable[Entry]: ... def capability(self) -> Capability: ... + def copy(self, source: str, target: str): ... class AsyncOperator: def __init__(self, scheme: str, **kwargs): ... @@ -65,6 +66,7 @@ class AsyncOperator: self, path: str, expire_second: int ) -> PresignedRequest: ... def capability(self) -> Capability: ... + async def copy(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 1cedac75e..9f0cf6a18 100644 --- a/bindings/python/src/asyncio.rs +++ b/bindings/python/src/asyncio.rs @@ -139,6 +139,15 @@ impl AsyncOperator { }) } + /// Copy from `src` to `dst`. + + pub fn copy<'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.copy(&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 1ee0884f9..4106ec492 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -188,6 +188,11 @@ impl Operator { self.0.stat(path).map_err(format_pyerr).map(Metadata) } + /// Copy src to dst. + pub fn copy(&self, source: &str, target: &str) -> PyResult<()> { + self.0.copy(source, target).map_err(format_pyerr) + } + /// Create a dir at given path. /// /// # Notes diff --git a/bindings/python/tests/test_async_copy.py b/bindings/python/tests/test_async_copy.py new file mode 100644 index 000000000..dadff6401 --- /dev/null +++ b/bindings/python/tests/test_async_copy.py @@ -0,0 +1,114 @@ +# 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 uuid import uuid4 +from random import randint + +import pytest + + [email protected] [email protected]_capability("read", "write", "copy") +async def test_async_copy(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.copy(source_path, target_path) + read_content = await async_operator.read(target_path) + assert read_content is not None + assert read_content == content + await async_operator.delete(source_path) + await async_operator.delete(target_path) + + [email protected] [email protected]_capability("read", "write", "copy") +async def test_async_copy_non_exist(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(Exception) as e_info: + await async_operator.copy(source_path, target_path) + + [email protected] [email protected]_capability("read", "write", "copy") +async def test_async_copy_source_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.copy(source_path, target_path) + + [email protected] [email protected]_capability("read", "write", "copy") +async def test_async_copy_target_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())}/" + await async_operator.create_dir(target_path) + with pytest.raises(Exception) as e_info: + await async_operator.copy(source_path, target_path) + await async_operator.delete(source_path) + await async_operator.delete(target_path) + + [email protected] [email protected]_capability("read", "write", "copy") +async def test_async_copy_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.copy(source_path, source_path) + await async_operator.delete(source_path) + + [email protected] [email protected]_capability("read", "write", "copy") +async def test_async_copy_nested(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}/{str(uuid4())}/{str(uuid4())}" + content = os.urandom(1024) + await async_operator.write(source_path, content) + await async_operator.copy(source_path, target_path) + target_content = await async_operator.read(target_path) + assert target_content is not None + assert target_content == content + await async_operator.delete(source_path) + await async_operator.delete(target_path) + + [email protected] [email protected]_capability("read", "write", "copy") +async def test_async_copy_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.copy(source_path, target_path) + target_content = await async_operator.read(target_path) + assert target_content is not None + assert target_content == source_content + await async_operator.delete(source_path) + await async_operator.delete(target_path) diff --git a/bindings/python/tests/test_sync_copy.py b/bindings/python/tests/test_sync_copy.py new file mode 100644 index 000000000..0a29da976 --- /dev/null +++ b/bindings/python/tests/test_sync_copy.py @@ -0,0 +1,107 @@ +# 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 uuid import uuid4 +from random import randint + +import pytest + + [email protected]_capability("read", "write", "copy") +def test_sync_copy(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.copy(source_path, target_path) + read_content = operator.read(target_path) + assert read_content is not None + assert read_content == content + operator.delete(source_path) + operator.delete(target_path) + + [email protected]_capability("read", "write", "copy") +def test_sync_copy_non_exist(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}" + with pytest.raises(Exception) as e_info: + operator.copy(source_path, target_path) + + [email protected]_capability("read", "write", "copy") +def test_sync_copy_source_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.copy(source_path, target_path) + + [email protected]_capability("read", "write", "copy") +def test_sync_copy_target_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())}/" + operator.create_dir(target_path) + with pytest.raises(Exception) as e_info: + operator.copy(source_path, target_path) + operator.delete(source_path) + operator.delete(target_path) + + [email protected]_capability("read", "write", "copy") +def test_sync_copy_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.copy(source_path, source_path) + operator.delete(source_path) + + [email protected]_capability("read", "write", "copy") +def test_sync_copy_nested(service_name, operator, async_operator): + source_path = f"random_file_{str(uuid4())}" + target_path = f"random_file_{str(uuid4())}/{str(uuid4())}/{str(uuid4())}" + content = os.urandom(1024) + operator.write(source_path, content) + operator.copy(source_path, target_path) + target_content = operator.read(target_path) + assert target_content is not None + assert target_content == content + operator.delete(source_path) + operator.delete(target_path) + + [email protected]_capability("read", "write", "copy") +def test_sync_copy_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.copy(source_path, target_path) + target_content = operator.read(target_path) + assert target_content is not None + assert target_content == source_content + operator.delete(source_path) + operator.delete(target_path)
