Skip to content

Commit

Permalink
fix: support buffers/DataView in props
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
maartenbreddels committed Oct 2, 2024
1 parent b48b045 commit 4998292
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 16 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 24 additions & 16 deletions src/widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions tests/ui/serialize_test.py
Original file line number Diff line number Diff line change
@@ -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 <div>{`v:${floatArray[0]}`}</div>
};
"""


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

0 comments on commit 4998292

Please sign in to comment.