Skip to content

Commit 8121f34

Browse files
authored
fix: nested types, use dataclass_wizard to deserialize/serialize types (#21)
* fix: nested types, use dataclass_wizard to deserialize/serialize types * fix: get imports json serialization working again, reflectJsonObject tests now pass * fix: get all bindgen tests running * chore: remove usage of extism.JSONEncoder, we no longer rely on it * fix: handling buffers as str instead of bytes * build: use uv to fetch deps instead of curl * build: only specify direct dependencies
1 parent d9b45db commit 8121f34

8 files changed

Lines changed: 99 additions & 17 deletions

File tree

bindgen-test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ case $1 in
3333
echo "building '$PLUGIN_NAME'..."
3434
xtp plugin build --path $PLUGIN_NAME
3535
echo "testing '$PLUGIN_NAME'..."
36-
xtp plugin test $PLUGIN_NAME/plugin.wasm --with test.wasm --mock-host mock.wasm
36+
EXTISM_ENABLE_WASI_OUTPUT=1 xtp plugin test $PLUGIN_NAME/plugin.wasm --with test.wasm --mock-host mock.wasm
3737
;;
3838
esac

src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
EnumType,
77
ArrayType,
88
XtpNormalizedType,
9-
XtpTyped
9+
XtpTyped,
10+
Parameter
1011
} from "@dylibso/xtp-bindgen"
1112

1213
function pythonTypeName(s: string): string {
@@ -78,6 +79,14 @@ function toPythonParamType(property: XtpTyped, required?: boolean): string {
7879
return t;
7980
}
8081

82+
function toPythonHostParamType(param: Parameter): string {
83+
if (param.contentType === 'application/x-binary') {
84+
return 'bytes'
85+
} else {
86+
return 'str'
87+
}
88+
}
89+
8190
export function render() {
8291
const tmpl = Host.inputString();
8392
const ctx = {
@@ -86,7 +95,8 @@ export function render() {
8695
toPythonType,
8796
toPythonParamType,
8897
pythonTypeName,
89-
pythonFunctionName
98+
pythonFunctionName,
99+
toPythonHostParamType
90100
};
91101

92102
const output = ejs.render(tmpl, ctx);

template/plugin/__init__.py.ejs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import Optional, List # noqa: F401
44
from datetime import datetime # noqa: F401
5+
import json
56
import extism # pyright: ignore
67
import plugin
78

@@ -21,7 +22,7 @@ from pdk_types import <%- Object.values(schema.schemas).map(schema => pythonType
2122
# And it returns an output <%- toPythonParamType(imp.output) %> (<%- formatCommentLine(imp.output.description) %>)
2223
<% } -%>
2324
@extism.import_fn("extism:host/user", "<%- imp.name %>")
24-
def <%- pythonFunctionName(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonParamType(imp.input) %><%} -%>) <% if (imp.output) { %> -> <%- toPythonParamType(imp.output) %><% } %>: # pyright: ignore [reportReturnType]
25+
def _<%- pythonFunctionName(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonHostParamType(imp.input) %> <%} -%>) <% if (imp.output) { %> -> <%- toPythonHostParamType(imp.output) %> <% } %>: # pyright: ignore [reportReturnType]
2526
pass
2627
<% }) %>
2728

@@ -34,7 +35,28 @@ def <%- pythonFunctionName(imp.name) %>(<% if (imp.input) { -%>input: <%- toPyth
3435
<% } -%>
3536
@extism.plugin_fn
3637
def <%- ex.name %>():
37-
res = plugin.<%- pythonFunctionName(ex.name) %>(<% if (ex.input) { %> extism.input(<%- toPythonParamType(ex.input) %>) <% } %>)
38-
extism.output(res)
39-
40-
<% }) %>
38+
<% if (ex.input) { -%>
39+
input = <% if (ex.input.contentType === 'application/x-binary') { %> extism.input_bytes() <% } else { %> extism.input_str() <% } %>
40+
<% if (ex.input.contentType === 'application/json') { -%>
41+
<% if (ex.input.xtpType.kind === 'object') { -%>
42+
input = <%- toPythonParamType(ex.input) %>.from_json(input)
43+
<% } else { -%>
44+
input = json.loads(input)
45+
<% } -%>
46+
<% } -%>
47+
<% } -%>
48+
res = plugin.<%- pythonFunctionName(ex.name) %>(<% if (ex.input) { %> input <% } %>)
49+
<% if (ex.output) { -%>
50+
<% if (ex.output.contentType === 'application/x-binary') { %> extism.output_bytes( <% } else { %> extism.output_str( <% } %>
51+
<% if (ex.output.contentType === 'application/json') { -%>
52+
<% if (ex.output.xtpType.kind === 'object') { -%>
53+
res.to_json()
54+
<% } else { -%>
55+
json.dumps(res)
56+
<% } %>
57+
<% } else { -%>
58+
res
59+
<% } -%>
60+
)
61+
<% } -%>
62+
<% }) -%>

template/plugin/pdk_imports.py.ejs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional, List # noqa: F401
44
from datetime import datetime # noqa: F401
55
import extism # noqa: F401 # pyright: ignore
6+
import json
67

78
<% if (Object.values(schema.schemas).length > 0) { %>
89
from pdk_types import <%- Object.values(schema.schemas).map(schema => pythonTypeName(schema.name)).join(", ") %> # noqa: F401
@@ -18,8 +19,39 @@ from pdk_types import <%- Object.values(schema.schemas).map(schema => pythonType
1819
# And it returns an output <%- toPythonParamType(imp.output) %> (<%- formatCommentLine(imp.output.description) %>)
1920
<% } -%>
2021
@extism.import_fn("extism:host/user", "<%- imp.name %>")
21-
def <%- pythonFunctionName(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonParamType(imp.input) %><%} -%>) <% if (imp.output) { %> -> <%- toPythonParamType(imp.output) %><% } %>: # pyright: ignore [reportReturnType]
22+
def _<%- pythonFunctionName(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonHostParamType(imp.input) %> <%} -%>) <% if (imp.output) { %> -> <%- toPythonHostParamType(imp.output) %> <% } %>: # pyright: ignore [reportReturnType]
2223
pass
24+
25+
def <%- pythonFunctionName(imp.name) %>(<% if (imp.input) { -%>input: <%- toPythonParamType(imp.input) %><%} -%>) <% if (imp.output) { %> -> <%- toPythonParamType(imp.output) %><% } %>: # pyright: ignore [reportReturnType]
26+
<% if (imp.output) { %>
27+
return (
28+
<% if (imp.output.contentType === 'application/json') { %>
29+
<% if (imp.output.xtpType.kind === 'object') { %>
30+
<%- toPythonParamType(imp.output) %>.from_json(
31+
<% } else { %>
32+
json.loads(
33+
<% } -%>
34+
<% } -%>
35+
<% } -%>
36+
_<%- pythonFunctionName(imp.name) %>(
37+
<% if (imp.input) { -%>
38+
<% if (imp.input.contentType === 'application/json') { %>
39+
<% if (imp.input.xtpType.kind === 'object') { %>
40+
input.to_json()
41+
<% } else { %>
42+
json.dumps(input)
43+
<% } -%>
44+
<% } else { %>
45+
input
46+
<% } -%>
47+
<% } -%>
48+
)
49+
<% if (imp.output) { %>
50+
)
51+
<% if (imp.output.contentType === 'application/json') { %>
52+
)
53+
<% } -%>
54+
<% } -%>
2355
<% }) %>
2456
2557

template/plugin/pdk_types.py.ejs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ from enum import Enum # noqa: F401
55
from typing import Optional, List # noqa: F401
66
from datetime import datetime # noqa: F401
77
from dataclasses import dataclass # noqa: F401
8-
9-
import extism # noqa: F401 # pyright: ignore
8+
from dataclass_wizard import JSONWizard # noqa: F401
9+
from dataclass_wizard.type_def import JSONObject
10+
from base64 import b64encode, b64decode
1011

1112
<% Object.values(schema.schemas).forEach(schema => { %>
1213
<% if (schema.enum) { %>
@@ -16,7 +17,7 @@ class <%- pythonTypeName(schema.name) %>(Enum):
1617
<% }) %>
1718
<% } else { %>
1819
@dataclass
19-
class <%- pythonTypeName(schema.name) %>(extism.Json):
20+
class <%- pythonTypeName(schema.name) %>(JSONWizard):
2021
<% schema.properties.forEach(p => { -%>
2122
<% if (!p.nullable && p.required) {%>
2223
<% if (p.description) { -%>
@@ -35,6 +36,23 @@ class <%- pythonTypeName(schema.name) %>(extism.Json):
3536
<% } %>
3637
<% }) %>
3738
39+
@classmethod
40+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
41+
<% schema.properties.forEach(p => { -%>
42+
<% if (p.xtpType.kind === 'buffer') {%>
43+
o['<%- p.name %>'] = b64decode(o['<%- p.name %>'])
44+
<% } -%>
45+
<% }) -%>
46+
return o
47+
48+
def _pre_dict(self):
49+
<% schema.properties.forEach(p => { -%>
50+
<% if (p.xtpType.kind === 'buffer') {%>
51+
self.<%- p.name %> = b64encode(self.<%- p.name %>).decode("utf-8")
52+
<% } -%>
53+
<% }) -%>
54+
return
55+
3856
<% } %>
3957
<% }); %>
4058

template/prepare.sh

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,3 @@ if ! command_exists extism-py; then
4040
sleep 2
4141
exit 1
4242
fi
43-
44-

template/pyproject.toml.ejs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ name = "<%= project.name %>"
33
version = "0.1.0"
44
description = "Add your description here"
55
readme = "README.md"
6-
requires-python = ">=3.12"
7-
dependencies = []
6+
requires-python = ">=3.13,<3.14"
7+
dependencies = [
8+
"dataclass-wizard>=0.33.0,<0.34.0"
9+
]
810
911
[tool.uv]
1012
dev-dependencies = [

template/xtp.toml.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ name = "<%= project.name %>"
88

99
[scripts]
1010
# xtp plugin build runs this script to generate the wasm file
11-
build = "PYTHONPATH=./plugin extism-py -o plugin.wasm plugin/__init__.py"
11+
build = "PYTHONPATH=./plugin:./.venv/lib/python3.13/site-packages extism-py -o plugin.wasm plugin/__init__.py"
1212

1313
# xtp plugin init runs this script to format the code
1414
format = "uv run ruff format plugin/*.py"

0 commit comments

Comments
 (0)