From 9333909397a2b921f5f335344591c7e920afc3ea Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 8 Aug 2025 02:58:16 +0900 Subject: [PATCH 1/5] try fix --- bindings/python/src/operator.rs | 13 ++++----- .../tests/test_operator_construction.py | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 bindings/python/tests/test_operator_construction.py diff --git a/bindings/python/src/operator.rs b/bindings/python/src/operator.rs index 0c00d2e92f0a..325962b30c77 100644 --- a/bindings/python/src/operator.rs +++ b/bindings/python/src/operator.rs @@ -336,12 +336,13 @@ impl AsyncOperator { .set_source(err) }) .map_err(format_pyerr)?; - let map = map - .map(|v| { - v.extract::>() - .expect("must be valid hashmap") - }) - .unwrap_or_default(); + let map = if let Some(v) = map { + v.iter() + .map(|(k, v)| Ok((k.extract::()?, v.str()?.to_string()))) + .collect::>>()? + } else { + HashMap::default() + }; Ok(AsyncOperator { core: build_operator(scheme, map.clone())?, diff --git a/bindings/python/tests/test_operator_construction.py b/bindings/python/tests/test_operator_construction.py new file mode 100644 index 000000000000..1cc14eed71cb --- /dev/null +++ b/bindings/python/tests/test_operator_construction.py @@ -0,0 +1,27 @@ +# 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. + +from pathlib import Path +import opendal +import pytest + + +def test_operator_new_with_path(): + # Test with string parameters (the most common case) + op = opendal.Operator("memory", root=Path("/tmp")) + assert op is not None + assert op.capability().read From 8b100f1642d4ce3eaa140f87240c6b89e27726d8 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 8 Aug 2025 03:09:12 +0900 Subject: [PATCH 2/5] try --- bindings/python/src/operator.rs | 13 +++++++------ bindings/python/tests/test_operator_construction.py | 1 - 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bindings/python/src/operator.rs b/bindings/python/src/operator.rs index 325962b30c77..32b0102c422d 100644 --- a/bindings/python/src/operator.rs +++ b/bindings/python/src/operator.rs @@ -70,12 +70,13 @@ impl Operator { .set_source(err) }) .map_err(format_pyerr)?; - let map = map - .map(|v| { - v.extract::>() - .expect("must be valid hashmap") - }) - .unwrap_or_default(); + let map = if let Some(v) = map { + v.iter() + .map(|(k, v)| Ok((k.extract::()?, v.str()?.to_string()))) + .collect::>>()? + } else { + HashMap::default() + }; Ok(Operator { core: build_blocking_operator(scheme, map.clone())?, diff --git a/bindings/python/tests/test_operator_construction.py b/bindings/python/tests/test_operator_construction.py index 1cc14eed71cb..65f13f7a90a7 100644 --- a/bindings/python/tests/test_operator_construction.py +++ b/bindings/python/tests/test_operator_construction.py @@ -21,7 +21,6 @@ def test_operator_new_with_path(): - # Test with string parameters (the most common case) op = opendal.Operator("memory", root=Path("/tmp")) assert op is not None assert op.capability().read From 4846eb3b32023d01331fd3f455a21a09049dd455 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 8 Aug 2025 03:23:15 +0900 Subject: [PATCH 3/5] async --- bindings/python/tests/test_operator_construction.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/python/tests/test_operator_construction.py b/bindings/python/tests/test_operator_construction.py index 65f13f7a90a7..a8e0b6736db8 100644 --- a/bindings/python/tests/test_operator_construction.py +++ b/bindings/python/tests/test_operator_construction.py @@ -24,3 +24,10 @@ def test_operator_new_with_path(): op = opendal.Operator("memory", root=Path("/tmp")) assert op is not None assert op.capability().read + + +@pytest.mark.asyncio +def test_async_operator_new_with_path(): + op = opendal.AsyncOperator("memory", root=Path("/tmp")) + assert op is not None + assert op.capability().read From d02f35901d9b6a68d9c7441752c0fe16b27fbf47 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Sun, 1 Nov 2026 10:04:09 +0800 Subject: [PATCH 4/5] fix(bindings/python): accept path-like constructor options --- bindings/python/python/opendal/operator.pyi | 12 +++-- bindings/python/src/operator.rs | 44 +++++++++++++++---- .../tests/test_operator_construction.py | 32 +++++++++----- 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/bindings/python/python/opendal/operator.pyi b/bindings/python/python/opendal/operator.pyi index 4fddebd0c8b4..80c627dc70c2 100644 --- a/bindings/python/python/opendal/operator.pyi +++ b/bindings/python/python/opendal/operator.pyi @@ -42,7 +42,9 @@ class AsyncOperator: Operator """ - def __new__(cls, /, scheme: str | Scheme, **kwargs: str) -> AsyncOperator: + def __new__( + cls, /, scheme: str | Scheme, **kwargs: str | PathLike[str] + ) -> AsyncOperator: """ Create a new `AsyncOperator`. @@ -51,7 +53,7 @@ class AsyncOperator: scheme : str | Scheme The scheme of the service. **kwargs : dict - The options for the service. + The options for the service. Path-like values are converted to strings. Returns ------- @@ -722,7 +724,9 @@ class Operator: AsyncOperator """ - def __new__(cls, /, scheme: str | Scheme, **kwargs: str) -> Operator: + def __new__( + cls, /, scheme: str | Scheme, **kwargs: str | PathLike[str] + ) -> Operator: """ Create a new blocking `Operator`. @@ -731,7 +735,7 @@ class Operator: scheme : str | Scheme The scheme of the service. **kwargs : dict - The options for the service. + The options for the service. Path-like values are converted to strings. Returns ------- diff --git a/bindings/python/src/operator.rs b/bindings/python/src/operator.rs index 98ee37fbd80f..2c9b1ffa4b86 100644 --- a/bindings/python/src/operator.rs +++ b/bindings/python/src/operator.rs @@ -181,15 +181,18 @@ impl Operator { /// scheme : str | Scheme /// The scheme of the service. /// **kwargs : dict - /// The options for the service. + /// The options for the service. Path-like values are converted to strings. /// /// Returns /// ------- /// Operator /// The new operator. #[new] - #[pyo3(signature = (scheme: "str | Scheme", *, **kwargs: "str"))] - pub fn new(scheme: Bound, kwargs: Option>) -> PyResult { + #[pyo3(signature = (scheme: "str | Scheme", *, **kwargs: "str | PathLike[str]"))] + pub fn new( + scheme: Bound, + kwargs: Option>>, + ) -> PyResult { let scheme = if let Ok(scheme_str) = scheme.extract::<&str>() { scheme_str.to_string() } else if let Ok(py_scheme) = scheme.extract::() { @@ -200,7 +203,18 @@ impl Operator { )); }; let scheme = normalize_scheme(&scheme); - let map = kwargs.unwrap_or_default(); + let map = kwargs + .unwrap_or_default() + .into_iter() + .map(|(key, value)| { + let value = value.extract::().or_else(|_| { + value + .extract::() + .map(|path| path.to_string_lossy().into_owned()) + })?; + Ok((key, value)) + }) + .collect::>>()?; Ok(Operator { core: build_blocking_operator(&scheme, map.clone())?, @@ -947,15 +961,18 @@ impl AsyncOperator { /// scheme : str | Scheme /// The scheme of the service. /// **kwargs : dict - /// The options for the service. + /// The options for the service. Path-like values are converted to strings. /// /// Returns /// ------- /// AsyncOperator /// The new async operator. #[new] - #[pyo3(signature = (scheme: "str | Scheme", * ,**kwargs: "str"))] - pub fn new(scheme: Bound, kwargs: Option>) -> PyResult { + #[pyo3(signature = (scheme: "str | Scheme", *, **kwargs: "str | PathLike[str]"))] + pub fn new( + scheme: Bound, + kwargs: Option>>, + ) -> PyResult { let scheme = if let Ok(scheme_str) = scheme.extract::<&str>() { scheme_str.to_string() } else if let Ok(py_scheme) = scheme.extract::() { @@ -967,7 +984,18 @@ impl AsyncOperator { }; let scheme = normalize_scheme(&scheme); - let map = kwargs.unwrap_or_default(); + let map = kwargs + .unwrap_or_default() + .into_iter() + .map(|(key, value)| { + let value = value.extract::().or_else(|_| { + value + .extract::() + .map(|path| path.to_string_lossy().into_owned()) + })?; + Ok((key, value)) + }) + .collect::>>()?; Ok(AsyncOperator { core: build_operator(&scheme, map.clone())?, diff --git a/bindings/python/tests/test_operator_construction.py b/bindings/python/tests/test_operator_construction.py index a8e0b6736db8..38f2428ca044 100644 --- a/bindings/python/tests/test_operator_construction.py +++ b/bindings/python/tests/test_operator_construction.py @@ -15,19 +15,31 @@ # specific language governing permissions and limitations # under the License. -from pathlib import Path -import opendal import pytest +import opendal + -def test_operator_new_with_path(): - op = opendal.Operator("memory", root=Path("/tmp")) - assert op is not None - assert op.capability().read +@pytest.mark.parametrize("path_like", [False, True]) +def test_operator_new_with_path(tmp_path, path_like): + root = tmp_path if path_like else str(tmp_path) + op = opendal.Operator("fs", root=root) + op.write("constructor.txt", b"sync constructor") + assert op.read("constructor.txt") == b"sync constructor" + assert (tmp_path / "constructor.txt").read_bytes() == b"sync constructor" @pytest.mark.asyncio -def test_async_operator_new_with_path(): - op = opendal.AsyncOperator("memory", root=Path("/tmp")) - assert op is not None - assert op.capability().read +@pytest.mark.parametrize("path_like", [False, True]) +async def test_async_operator_new_with_path(tmp_path, path_like): + root = tmp_path if path_like else str(tmp_path) + op = opendal.AsyncOperator("fs", root=root) + await op.write("constructor.txt", b"async constructor") + assert await op.read("constructor.txt") == b"async constructor" + assert (tmp_path / "constructor.txt").read_bytes() == b"async constructor" + + +@pytest.mark.parametrize("operator_type", [opendal.Operator, opendal.AsyncOperator]) +def test_constructor_rejects_non_path_root(operator_type): + with pytest.raises(TypeError): + operator_type("fs", root=True) From b509fa920283a02277e780d2346ba57e30744aad Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Sun, 1 Nov 2026 10:11:49 +0800 Subject: [PATCH 5/5] fix(bindings/python): preserve string path option boundaries --- bindings/python/src/operator.rs | 14 ++++++++++---- .../python/tests/test_operator_construction.py | 5 +++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/bindings/python/src/operator.rs b/bindings/python/src/operator.rs index 2c9b1ffa4b86..964b35ff7dfc 100644 --- a/bindings/python/src/operator.rs +++ b/bindings/python/src/operator.rs @@ -209,8 +209,11 @@ impl Operator { .map(|(key, value)| { let value = value.extract::().or_else(|_| { value - .extract::() - .map(|path| path.to_string_lossy().into_owned()) + .py() + .import("os")? + .getattr("fspath")? + .call1((&value,))? + .extract::() })?; Ok((key, value)) }) @@ -990,8 +993,11 @@ impl AsyncOperator { .map(|(key, value)| { let value = value.extract::().or_else(|_| { value - .extract::() - .map(|path| path.to_string_lossy().into_owned()) + .py() + .import("os")? + .getattr("fspath")? + .call1((&value,))? + .extract::() })?; Ok((key, value)) }) diff --git a/bindings/python/tests/test_operator_construction.py b/bindings/python/tests/test_operator_construction.py index 38f2428ca044..52a9973d4605 100644 --- a/bindings/python/tests/test_operator_construction.py +++ b/bindings/python/tests/test_operator_construction.py @@ -40,6 +40,7 @@ async def test_async_operator_new_with_path(tmp_path, path_like): @pytest.mark.parametrize("operator_type", [opendal.Operator, opendal.AsyncOperator]) -def test_constructor_rejects_non_path_root(operator_type): +@pytest.mark.parametrize("root", [True, b"bytes-root"]) +def test_constructor_rejects_non_string_root(operator_type, root): with pytest.raises(TypeError): - operator_type("fs", root=True) + operator_type("fs", root=root)