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 ae97d4f784df4897dc1ff69195c54269b378a4ba
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)

Reply via email to