diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index a818b092..bef0553e 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -55,7 +55,7 @@ jobs: run: dir ..\..\target\${{ matrix.settings.target }}\release - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ matrix.settings.target != 'x86_64-pc-windows-msvc' }} with: name: ${{ matrix.settings.target }} @@ -63,7 +63,7 @@ jobs: if-no-files-found: error - name: Upload artifact Windows - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ matrix.settings.target == 'x86_64-pc-windows-msvc' }} with: name: ${{ matrix.settings.target }} @@ -76,7 +76,7 @@ jobs: - uses: actions/checkout@v3 - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: common path: bindings/c/zen_engine.h @@ -95,7 +95,7 @@ jobs: persist-credentials: false token: ${{ secrets.PAT }} - name: Download all artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index bd97e652..983bae83 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -160,7 +160,7 @@ jobs: shell: bash - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: bindings/nodejs/*.node @@ -203,7 +203,7 @@ jobs: run: yarn install --immutable --mode=skip-build - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: artifacts @@ -245,7 +245,7 @@ jobs: run: yarn install --immutable --mode=skip-build - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-x86_64-unknown-linux-gnu path: artifacts @@ -296,7 +296,7 @@ jobs: run: yarn install --immutable --mode=skip-build - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-x86_64-unknown-linux-musl path: artifacts @@ -354,7 +354,7 @@ jobs: - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-aarch64-unknown-linux-musl path: artifacts @@ -399,7 +399,7 @@ jobs: # pwd # # - name: Download artifacts - # uses: actions/download-artifact@v3 + # uses: actions/download-artifact@v4 # with: # name: bindings-aarch64-unknown-linux-gnu # path: artifacts @@ -461,7 +461,7 @@ jobs: run: yarn install --immutable --mode=skip-build - name: Download all artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index c69492c6..355967a2 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -31,13 +31,12 @@ jobs: working-directory: ${{ env.WORKING_DIRECTORY }} strategy: matrix: -# target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] - target: [x86_64, aarch64] + target: [ x86_64, aarch64 ] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.13' - name: Build wheels uses: stefan-gorules/maturin-action@main @@ -50,10 +49,10 @@ jobs: apt install -y patch - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels - path: dist + name: wheels-linux-${{ matrix.target }} + path: wheels windows: runs-on: windows-latest @@ -62,12 +61,12 @@ jobs: working-directory: ${{ env.WORKING_DIRECTORY }} strategy: matrix: - target: [x64] + target: [ x64 ] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.13' architecture: ${{ matrix.target }} - name: Build wheels uses: PyO3/maturin-action@v1 @@ -76,10 +75,10 @@ jobs: args: --release --out dist --find-interpreter -m bindings/python/Cargo.toml sccache: 'true' - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels - path: dist + name: wheels-windows-${{ matrix.target }} + path: wheels macos: runs-on: macos-latest @@ -88,12 +87,12 @@ jobs: working-directory: ${{ env.WORKING_DIRECTORY }} strategy: matrix: - target: [x86_64, aarch64] + target: [ x86_64, aarch64 ] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.13' - name: Build wheels uses: PyO3/maturin-action@v1 with: @@ -101,10 +100,10 @@ jobs: args: --release --out dist --find-interpreter -m bindings/python/Cargo.toml sccache: 'true' - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels - path: dist + name: wheels-macos-${{ matrix.target }} + path: wheels sdist: runs-on: ubuntu-latest @@ -113,6 +112,9 @@ jobs: working-directory: ${{ env.WORKING_DIRECTORY }} steps: - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.13' - name: Build sdist uses: PyO3/maturin-action@v1 @@ -120,10 +122,10 @@ jobs: command: sdist args: --out dist -m bindings/python/Cargo.toml - name: Upload sdist - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: wheels - path: dist + name: wheels-sdist + path: wheels release: name: Release @@ -131,14 +133,12 @@ jobs: defaults: run: working-directory: ${{ env.WORKING_DIRECTORY }} -# if: "startsWith(github.ref, 'refs/tags/')" - needs: [linux, windows, macos, sdist] -# needs: [linux, sdist] -# needs: [linux] + needs: [ linux, windows, macos, sdist ] steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: wheels + path: wheels + - name: Publish to PyPI if: "startsWith(github.event.head_commit.message, 'chore(release): publish python')" uses: PyO3/maturin-action@v1 diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index ec31684a..e823bea3 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -12,9 +12,9 @@ crate-type = ["cdylib"] [dependencies] anyhow = { workspace = true } either = "1.13" -pyo3 = { version = "0.20", features = ["anyhow", "serde"] } -pyo3-asyncio = { version = "0.20", features = ["tokio-runtime"] } -pythonize = "0.20" +pyo3 = { version = "0.23", features = ["anyhow", "serde"] } +pyo3-async-runtimes = { version = "0.23", features = ["tokio-runtime"] } +pythonize = "0.23" json_dotpath = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 9ba1ff59..ef9b8542 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=0.14,<0.15"] +requires = ["maturin>=1,<2"] build-backend = "maturin" [project] diff --git a/bindings/python/src/custom_node.rs b/bindings/python/src/custom_node.rs index 293e4399..86ce9192 100644 --- a/bindings/python/src/custom_node.rs +++ b/bindings/python/src/custom_node.rs @@ -1,8 +1,8 @@ use anyhow::anyhow; use either::Either; use pyo3::types::PyDict; -use pyo3::{PyObject, PyResult, Python}; -use pyo3_asyncio::tokio; +use pyo3::{Bound, IntoPyObjectExt, Py, PyAny, PyObject, PyResult, Python}; +use pyo3_async_runtimes::tokio; use pythonize::depythonize; use zen_engine::handler::custom_node_adapter::{CustomNodeAdapter, CustomNodeRequest}; @@ -11,10 +11,10 @@ use zen_engine::handler::node::{NodeResponse, NodeResult}; use crate::types::PyNodeRequest; #[derive(Default)] -pub(crate) struct PyCustomNode(Option); +pub(crate) struct PyCustomNode(Option>); -impl From for PyCustomNode { - fn from(value: PyObject) -> Self { +impl From> for PyCustomNode { + fn from(value: Py) -> Self { Self(Some(value)) } } @@ -26,8 +26,8 @@ impl From> for PyCustomNode { } fn extract_custom_node_response(py: Python<'_>, result: PyObject) -> NodeResult { - let dict = result.extract::<&PyDict>(py)?; - let response: NodeResponse = depythonize(dict)?; + let dict = result.extract::>(py)?; + let response: NodeResponse = depythonize(&dict)?; Ok(response) } @@ -45,7 +45,7 @@ impl CustomNodeAdapter for PyCustomNode { return Ok(Either::Left(extract_custom_node_response(py, result))); } - let result_future = tokio::into_future(result.as_ref(py))?; + let result_future = tokio::into_future(result.into_bound_py_any(py)?)?; return Ok(Either::Right(result_future)); }); diff --git a/bindings/python/src/decision.rs b/bindings/python/src/decision.rs index 32f7fa54..f00c4088 100644 --- a/bindings/python/src/decision.rs +++ b/bindings/python/src/decision.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use anyhow::{anyhow, Context}; use pyo3::types::PyDict; -use pyo3::{pyclass, pymethods, PyAny, PyObject, PyResult, Python, ToPyObject}; -use pyo3_asyncio::tokio; +use pyo3::{pyclass, pymethods, Bound, IntoPyObjectExt, Py, PyAny, PyResult, Python}; +use pyo3_async_runtimes::tokio; use pythonize::depythonize; use serde_json::Value; use zen_engine::{Decision, EvaluationOptions}; @@ -25,7 +25,13 @@ impl From> for PyZenDecision { #[pymethods] impl PyZenDecision { - pub fn evaluate(&self, py: Python, ctx: &PyDict, opts: Option<&PyDict>) -> PyResult { + #[pyo3(signature = (ctx, opts=None))] + pub fn evaluate( + &self, + py: Python, + ctx: &Bound<'_, PyDict>, + opts: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { let context: Value = depythonize(ctx).context("Failed to convert dict")?; let options: PyZenEvaluateOptions = if let Some(op) = opts { depythonize(op).context("Failed to convert dict")? @@ -46,15 +52,16 @@ impl PyZenDecision { })?; let value = serde_json::to_value(&result).context("Fail")?; - Ok(PyValue(value).to_object(py)) + PyValue(value).into_py_any(py) } + #[pyo3(signature = (ctx, opts=None))] pub fn async_evaluate<'py>( &'py self, py: Python<'py>, - ctx: &PyDict, - opts: Option<&PyDict>, - ) -> PyResult<&PyAny> { + ctx: &Bound<'_, PyDict>, + opts: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { let context: Value = depythonize(ctx).context("Failed to convert dict")?; let options: PyZenEvaluateOptions = if let Some(op) = opts { depythonize(op).context("Failed to convert dict")? @@ -63,7 +70,7 @@ impl PyZenDecision { }; let decision = self.0.clone(); - tokio::future_into_py(py, async move { + let result = tokio::future_into_py(py, async move { let result = futures::executor::block_on(decision.evaluate_with_opts( context.into(), EvaluationOptions { @@ -77,8 +84,10 @@ impl PyZenDecision { let value = serde_json::to_value(result).context("Failed to serialize result")?; - Python::with_gil(|py| Ok(PyValue(value).to_object(py))) - }) + Python::with_gil(|py| PyValue(value).into_py_any(py)) + })?; + + Ok(result.unbind()) } pub fn validate(&self) -> PyResult<()> { diff --git a/bindings/python/src/engine.rs b/bindings/python/src/engine.rs index f61f226c..409b68e0 100644 --- a/bindings/python/src/engine.rs +++ b/bindings/python/src/engine.rs @@ -1,9 +1,10 @@ use std::sync::Arc; use anyhow::{anyhow, Context}; +use pyo3::prelude::PyDictMethods; use pyo3::types::PyDict; -use pyo3::{pyclass, pymethods, PyAny, PyObject, PyResult, Python, ToPyObject}; -use pyo3_asyncio::tokio; +use pyo3::{pyclass, pymethods, Bound, IntoPyObjectExt, Py, PyAny, PyResult, Python}; +use pyo3_async_runtimes::tokio; use pythonize::depythonize; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -51,18 +52,19 @@ impl Default for PyZenEngine { #[pymethods] impl PyZenEngine { #[new] - pub fn new(maybe_options: Option<&PyDict>) -> PyResult { + #[pyo3(signature = (maybe_options=None))] + pub fn new(maybe_options: Option<&Bound<'_, PyDict>>) -> PyResult { let Some(options) = maybe_options else { return Ok(Default::default()); }; let loader = match options.get_item("loader")? { - Some(loader) => Some(Python::with_gil(|py| loader.to_object(py))), + Some(loader) => Some(Python::with_gil(|py| loader.into_py_any(py))?), None => None, }; let custom_node = match options.get_item("customHandler")? { - Some(custom_node) => Some(Python::with_gil(|py| custom_node.to_object(py))), + Some(custom_node) => Some(Python::with_gil(|py| custom_node.into_py_any(py))?), None => None, }; @@ -75,13 +77,14 @@ impl PyZenEngine { }) } + #[pyo3(signature = (key, ctx, opts=None))] pub fn evaluate( &self, py: Python, key: String, - ctx: &PyDict, - opts: Option<&PyDict>, - ) -> PyResult { + ctx: &Bound<'_, PyDict>, + opts: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { let context: Value = depythonize(ctx).context("Failed to convert dict")?; let options: PyZenEvaluateOptions = if let Some(op) = opts { depythonize(op).context("Failed to convert dict")? @@ -103,16 +106,17 @@ impl PyZenEngine { })?; let value = serde_json::to_value(&result).context("Failed to serialize result")?; - Ok(PyValue(value).to_object(py)) + PyValue(value).into_py_any(py) } + #[pyo3(signature = (key, ctx, opts=None))] pub fn async_evaluate<'py>( &'py self, py: Python<'py>, key: String, - ctx: &PyDict, - opts: Option<&PyDict>, - ) -> PyResult<&PyAny> { + ctx: &Bound<'_, PyDict>, + opts: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { let context: Value = depythonize(ctx).context("Failed to convert dict")?; let options: PyZenEvaluateOptions = if let Some(op) = opts { depythonize(op).context("Failed to convert dict")? @@ -121,7 +125,7 @@ impl PyZenEngine { }; let graph = self.graph.clone(); - tokio::future_into_py(py, async move { + let result = tokio::future_into_py(py, async move { let result = futures::executor::block_on(graph.evaluate_with_opts( key, context.into(), @@ -136,8 +140,10 @@ impl PyZenEngine { let value = serde_json::to_value(result).context("Failed to serialize result")?; - Python::with_gil(|py| Ok(PyValue(value).to_object(py))) - }) + Python::with_gil(|py| PyValue(value).into_py_any(py)) + })?; + + Ok(result.unbind()) } pub fn create_decision(&self, content: String) -> PyResult { diff --git a/bindings/python/src/expression.rs b/bindings/python/src/expression.rs index 0520ef17..1b142c3d 100644 --- a/bindings/python/src/expression.rs +++ b/bindings/python/src/expression.rs @@ -1,17 +1,18 @@ use anyhow::{anyhow, Context}; use pyo3::types::PyDict; -use pyo3::{pyfunction, PyObject, PyResult, Python, ToPyObject}; +use pyo3::{pyfunction, Bound, IntoPyObjectExt, Py, PyAny, PyResult, Python}; use pythonize::depythonize; use serde_json::Value; use crate::value::PyValue; #[pyfunction] +#[pyo3(signature = (expression, ctx=None))] pub fn evaluate_expression( py: Python, expression: String, - ctx: Option<&PyDict>, -) -> PyResult { + ctx: Option<&Bound<'_, PyDict>>, +) -> PyResult> { let context = ctx .map(|ctx| depythonize(ctx)) .transpose() @@ -21,11 +22,11 @@ pub fn evaluate_expression( let result = zen_expression::evaluate_expression(expression.as_str(), context.into()) .map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?; - Ok(PyValue(result.to_value()).to_object(py)) + PyValue(result.to_value()).into_py_any(py) } #[pyfunction] -pub fn evaluate_unary_expression(expression: String, ctx: &PyDict) -> PyResult { +pub fn evaluate_unary_expression(expression: String, ctx: &Bound<'_, PyDict>) -> PyResult { let context: Value = depythonize(ctx).context("Failed to convert context")?; let result = zen_expression::evaluate_unary_expression(expression.as_str(), context.into()) @@ -35,11 +36,15 @@ pub fn evaluate_unary_expression(expression: String, ctx: &PyDict) -> PyResult PyResult { +pub fn render_template( + py: Python, + template: String, + ctx: &Bound<'_, PyDict>, +) -> PyResult> { let context: Value = depythonize(ctx).context("Failed to convert context")?; let result = zen_tmpl::render(template.as_str(), context.into()) .map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?; - Ok(PyValue(result.to_value()).to_object(py)) + PyValue(result.to_value()).into_py_any(py) } diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 29b377a4..5cf6ef70 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -1,8 +1,9 @@ use crate::decision::PyZenDecision; use crate::engine::PyZenEngine; use crate::expression::{evaluate_expression, evaluate_unary_expression, render_template}; +use pyo3::prelude::PyModuleMethods; use pyo3::types::PyModule; -use pyo3::{pymodule, wrap_pyfunction, PyResult, Python}; +use pyo3::{pymodule, wrap_pyfunction, Bound, PyResult, Python}; mod custom_node; mod decision; @@ -13,7 +14,7 @@ mod types; mod value; #[pymodule] -fn zen(_py: Python, m: &PyModule) -> PyResult<()> { +fn zen(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(evaluate_expression, m)?)?; diff --git a/bindings/python/src/loader.rs b/bindings/python/src/loader.rs index 704559b9..2d949e05 100644 --- a/bindings/python/src/loader.rs +++ b/bindings/python/src/loader.rs @@ -2,13 +2,13 @@ use std::future::Future; use std::sync::Arc; use anyhow::anyhow; -use pyo3::{PyObject, Python}; +use pyo3::{Py, PyAny, PyObject, Python}; use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResponse}; use zen_engine::model::DecisionContent; #[derive(Default)] -pub(crate) struct PyDecisionLoader(Option); +pub(crate) struct PyDecisionLoader(Option>); impl From for PyDecisionLoader { fn from(value: PyObject) -> Self { @@ -16,8 +16,8 @@ impl From for PyDecisionLoader { } } -impl From> for PyDecisionLoader { - fn from(value: Option) -> Self { +impl From>> for PyDecisionLoader { + fn from(value: Option>) -> Self { Self(value) } } diff --git a/bindings/python/src/types.rs b/bindings/python/src/types.rs index 95691f4a..6253611f 100644 --- a/bindings/python/src/types.rs +++ b/bindings/python/src/types.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Context}; use json_dotpath::DotPaths; -use pyo3::{pyclass, pymethods, PyObject, PyResult, Python, ToPyObject}; +use pyo3::{pyclass, pymethods, IntoPyObjectExt, Py, PyAny, PyResult, Python}; use serde::Serialize; use serde_json::Value; use std::sync::Arc; @@ -37,9 +37,9 @@ pub struct PyNodeRequest { inner_input: Value, #[pyo3(get)] - pub input: PyObject, + pub input: Py, #[pyo3(get)] - pub node: PyObject, + pub node: Py, } impl PyNodeRequest { @@ -48,8 +48,8 @@ impl PyNodeRequest { let node_val = serde_json::to_value(&inner_node).unwrap(); Ok(Self { - input: value_to_object(py, &value.input.to_value()), - node: value_to_object(py, &node_val), + input: value_to_object(py, &value.input.to_value())?.unbind(), + node: value_to_object(py, &node_val)?.unbind(), inner_input: value.input.to_value(), inner_node, @@ -59,7 +59,7 @@ impl PyNodeRequest { #[pymethods] impl PyNodeRequest { - fn get_field(&self, py: Python, path: String) -> PyResult { + fn get_field(&self, py: Python, path: String) -> PyResult> { let node_config = &self.inner_node.config; let selected_value: Value = node_config @@ -68,16 +68,16 @@ impl PyNodeRequest { .flatten() .context("Failed to find JSON path")?; let Value::String(template) = selected_value else { - return Ok(PyValue(selected_value).to_object(py)); + return PyValue(selected_value).into_py_any(py); }; let template_value = zen_tmpl::render(template.as_str(), Variable::from(&self.inner_input)) .map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?; - Ok(PyValue(template_value.to_value()).to_object(py)) + PyValue(template_value.to_value()).into_py_any(py) } - fn get_field_raw(&self, py: Python, path: String) -> PyResult { + fn get_field_raw(&self, py: Python, path: String) -> PyResult> { let node_config = &self.inner_node.config; let selected_value: Value = node_config @@ -86,7 +86,7 @@ impl PyNodeRequest { .flatten() .context("Failed to find JSON path")?; - Ok(PyValue(selected_value).to_object(py)) + PyValue(selected_value).into_py_any(py) } } diff --git a/bindings/python/src/value.rs b/bindings/python/src/value.rs index fca04171..13bd030d 100644 --- a/bindings/python/src/value.rs +++ b/bindings/python/src/value.rs @@ -1,35 +1,48 @@ -use pyo3::{PyObject, Python, ToPyObject}; +use pyo3::prelude::{PyDictMethods, PyListMethods}; +use pyo3::types::{PyDict, PyList}; +use pyo3::{Bound, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python}; use serde_json::Value; -use std::collections::HashMap; #[repr(transparent)] #[derive(Clone, Debug)] pub struct PyValue(pub Value); -pub fn value_to_object(py: Python<'_>, val: &Value) -> PyObject { +pub fn value_to_object<'py>(py: Python<'py>, val: &Value) -> PyResult> { match val { - Value::Null => py.None(), - Value::Bool(b) => b.to_object(py), + Value::Null => py.None().into_bound_py_any(py), + Value::Bool(b) => b.into_bound_py_any(py), Value::Number(n) => { - let oi64 = n.as_i64().map(|i| i.to_object(py)); - let ou64 = n.as_u64().map(|i| i.to_object(py)); - let of64 = n.as_f64().map(|i| i.to_object(py)); + let oi64 = n.as_i64().map(|i| i.into_bound_py_any(py)); + let ou64 = n.as_u64().map(|i| i.into_bound_py_any(py)); + let of64 = n.as_f64().map(|i| i.into_bound_py_any(py)); oi64.or(ou64).or(of64).expect("number too large") } - Value::String(s) => s.to_object(py), + Value::String(s) => s.into_bound_py_any(py), Value::Array(v) => { - let inner: Vec<_> = v.iter().map(|x| value_to_object(py, x)).collect(); - inner.to_object(py) + let list = PyList::empty(py); + for item in v { + list.append(value_to_object(py, item)?)?; + } + + list.into_bound_py_any(py) } Value::Object(m) => { - let inner: HashMap<_, _> = m.iter().map(|(k, v)| (k, value_to_object(py, v))).collect(); - inner.to_object(py) + let dict = PyDict::new(py); + for (key, value) in m { + dict.set_item(key, value_to_object(py, value)?)?; + } + + dict.into_bound_py_any(py) } } } -impl ToPyObject for PyValue { - fn to_object(&self, py: Python<'_>) -> PyObject { +impl<'py> IntoPyObject<'py> for PyValue { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { value_to_object(py, &self.0) } }