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..964b35ff7dfc 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,21 @@ 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 + .py() + .import("os")? + .getattr("fspath")? + .call1((&value,))? + .extract::() + })?; + Ok((key, value)) + }) + .collect::>>()?; Ok(Operator { core: build_blocking_operator(&scheme, map.clone())?, @@ -947,15 +964,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 +987,21 @@ 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 + .py() + .import("os")? + .getattr("fspath")? + .call1((&value,))? + .extract::() + })?; + 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 new file mode 100644 index 000000000000..52a9973d4605 --- /dev/null +++ b/bindings/python/tests/test_operator_construction.py @@ -0,0 +1,46 @@ +# 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 pytest + +import opendal + + +@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 +@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]) +@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=root)