From 499829247797d31cc950286cb053f4f382c27613 Mon Sep 17 00:00:00 2001 From: "Maarten A. Breddels" Date: Wed, 2 Oct 2024 10:04:30 +0200 Subject: [PATCH] fix: support buffers/DataView in props This allows us to send numpy arrays efficiently. Before, a buffer (from the Python side) was transformed into a empty object, now it is a DataView object. --- README.md | 5 +++++ src/widget.tsx | 40 +++++++++++++++++++++++--------------- tests/ui/serialize_test.py | 27 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 16 deletions(-) create mode 100644 tests/ui/serialize_test.py diff --git a/README.md b/README.md index b1d775f..2672f4f 100644 --- a/README.md +++ b/README.md @@ -421,6 +421,11 @@ the `install` command every time that you rebuild your extension. For certain in you might also need another flag instead of `--sys-prefix`, but we won't cover the meaning of those flags here. +## Binary data transport + +Binary data such as NumPy arrays, or Arrow data can be efficiently transported to the frontend. +Props support object that support the buffer interface. See [this test as an example](https://github.com/widgetti/ipyreact/tree/master/tests/ui/serialize_test.py). + ### How to see your changes #### Typescript: diff --git a/src/widget.tsx b/src/widget.tsx index bac27e9..123a0fc 100644 --- a/src/widget.tsx +++ b/src/widget.tsx @@ -181,6 +181,9 @@ const widgetToReactComponent = async (widget: WidgetModel) => { } }; +const isPlainObject = (value: any) => + value && [undefined, Object].includes(value.constructor); + const entriesToObj = (acc: any, [key, value]: any[]) => { acc[key] = value; return acc; @@ -210,15 +213,17 @@ async function replaceWidgetWithComponent( data.map(async (d) => replaceWidgetWithComponent(d, get_model)), ); } - - return ( - await Promise.all( - Object.entries(data).map(async ([key, value]) => [ - key, - await replaceWidgetWithComponent(value, get_model), - ]), - ) - ).reduce(entriesToObj, {}); + if (isPlainObject(data)) { + return ( + await Promise.all( + Object.entries(data).map(async ([key, value]) => [ + key, + await replaceWidgetWithComponent(value, get_model), + ]), + ) + ).reduce(entriesToObj, {}); + } + return data; } function replaceComponentWithElement(data: any, view: DOMWidgetView): any { @@ -235,13 +240,16 @@ function replaceComponentWithElement(data: any, view: DOMWidgetView): any { if (Array.isArray(data)) { return data.map((d) => replaceComponentWithElement(d, view)); } - const entriesToObj = (acc: any, [key, value]: any[]) => { - acc[key] = value; - return acc; - }; - return Object.entries(data) - .map(([key, value]) => [key, replaceComponentWithElement(value, view)]) - .reduce(entriesToObj, {}); + if (isPlainObject(data)) { + const entriesToObj = (acc: any, [key, value]: any[]) => { + acc[key] = value; + return acc; + }; + return Object.entries(data) + .map(([key, value]) => [key, replaceComponentWithElement(value, view)]) + .reduce(entriesToObj, {}); + } + return data; } export class Module extends WidgetModel { diff --git a/tests/ui/serialize_test.py b/tests/ui/serialize_test.py new file mode 100644 index 0000000..8c156cc --- /dev/null +++ b/tests/ui/serialize_test.py @@ -0,0 +1,27 @@ +import numpy as np +import playwright.sync_api +from IPython.display import display + +import ipyreact + +code = """ +import {Button} from '@mui/material'; +import * as React from "react"; + +export default function({floatArrayDataView}) { + const floatArray = new Float32Array(floatArrayDataView.buffer); + console.log({floatArray, floatArrayDataView}); + return
{`v:${floatArray[0]}`}
+}; +""" + + +def test_material_ui(solara_test, assert_solara_snapshot, page_session: playwright.sync_api.Page): + class SerializeTest(ipyreact.ReactWidget): + _esm = code + + ar = np.array([42.0], dtype=np.float32) + b = SerializeTest(props={"floatArrayDataView": memoryview(ar)}) + display(b) + + page_session.locator("text=42").wait_for()