Python: Expose Slint structs

Structs declared and exported in Slint are now available in the module namespace
with a constructor.

Fixes #5708
This commit is contained in:
Simon Hausmann 2024-07-08 22:20:29 +02:00 committed by Simon Hausmann
parent 048c0eaf08
commit 1e3f05c983
11 changed files with 144 additions and 12 deletions

View file

@ -40,6 +40,7 @@ accessibility = ["slint-interpreter/accessibility"]
i-slint-backend-selector = { workspace = true }
i-slint-core = { workspace = true }
slint-interpreter = { workspace = true, features = ["default", "display-diagnostics", "internal"] }
i-slint-compiler = { workspace = true }
pyo3 = { version = "0.21.0", features = ["extension-module", "indexmap", "chrono", "abi3-py310"] }
indexmap = { version = "2.1.0" }
chrono = "0.4"

View file

@ -264,3 +264,34 @@ When sub-classing `slint.Model`, provide the following methods:
When adding/inserting rows, call `notify_row_added(row, count)` on the super class. Similarly, removal
requires notifying Slint by calling `notify_row_removed(row, count)`.
### Structs
Structs declared in Slint and exposed to Python via `export` are accessible in the namespace returned
when [instantiating a component](#instantiating-a-component).
**`app.slint`**
```slint
export struct MyData {
name: string,
age: int
}
export component MainWindow inherits Window {
in-out property <MyData> data;
}
```
**`main.py`**
The exported `MyData` struct can be constructed
```python
import slint
# Look for for `app.slint` in `sys.path`:
main_window = slint.loader.app.MainWindow()
data = slint.loader.app.MyData(name = "Simon")
data.age = 10
main_window.data = data
```

View file

@ -8,6 +8,8 @@ use std::rc::Rc;
use slint_interpreter::{ComponentHandle, Value};
use i_slint_compiler::langtype::Type;
use indexmap::IndexMap;
use pyo3::gc::PyVisit;
use pyo3::prelude::*;
@ -17,7 +19,7 @@ use pyo3::PyTraverseError;
use crate::errors::{
PyGetPropertyError, PyInvokeError, PyPlatformError, PySetCallbackError, PySetPropertyError,
};
use crate::value::PyValue;
use crate::value::{PyStruct, PyValue};
#[pyclass(unsendable)]
pub struct Compiler {
@ -143,6 +145,40 @@ impl CompilationResult {
fn get_diagnostics(&self) -> Vec<PyDiagnostic> {
self.result.diagnostics().map(|diag| PyDiagnostic(diag.clone())).collect()
}
#[getter]
fn structs_and_enums(&self, py: Python<'_>) -> HashMap<String, PyObject> {
let structs_and_enums =
self.result.structs_and_enums(i_slint_core::InternalToken {}).collect::<Vec<_>>();
fn convert_type(py: Python<'_>, ty: &Type) -> Option<(String, PyObject)> {
match ty {
Type::Struct { fields, name: Some(name), node: Some(_), .. } => {
let struct_instance = PyStruct::from(slint_interpreter::Struct::from_iter(
fields.iter().map(|(name, field_type)| {
(
name.to_string(),
slint_interpreter::default_value_for_type(field_type),
)
}),
));
return Some((name.to_string(), struct_instance.into_py(py)));
}
Type::Enumeration(_en) => {
// TODO
}
_ => {}
}
None
}
structs_and_enums
.iter()
.filter_map(|ty| convert_type(py, ty))
.into_iter()
.collect::<HashMap<String, PyObject>>()
}
}
#[pyclass(unsendable)]

View file

@ -8,6 +8,7 @@ from . import slint as native
import types
import logging
import importlib
import copy
from . import models
@ -191,6 +192,23 @@ def _build_class(compdef):
return type("SlintClassWrapper", (Component,), properties_and_callbacks)
def _build_struct(name, struct_prototype):
def new_struct(cls, *args, **kwargs):
inst = copy.copy(struct_prototype)
for prop, val in kwargs.items():
setattr(inst, prop, val)
return inst
type_dict = {
"__new__": new_struct,
}
return type(name, (), type_dict)
def load_file(path, quiet=False, style=None, include_paths=None, library_paths=None, translation_domain=None):
compiler = native.Compiler()
@ -223,6 +241,10 @@ def load_file(path, quiet=False, style=None, include_paths=None, library_paths=N
setattr(module, comp_name, wrapper_class)
for name, struct_or_enum_prototype in result.structs_and_enums.items():
struct_wrapper = _build_struct(name, struct_or_enum_prototype)
setattr(module, name, struct_wrapper)
return module

View file

@ -12,14 +12,22 @@ def test_load_file(caplog):
assert "The property 'color' has been deprecated. Please use 'background' instead" in caplog.text
assert len(list(module.__dict__.keys())) == 2
assert len(list(module.__dict__.keys())) == 3
assert "App" in module.__dict__
assert "Diag" in module.__dict__
assert "MyData" in module.__dict__
instance = module.App()
del instance
instance = module.Diag()
del instance
struct_instance = module.MyData()
struct_instance.name = "Test"
struct_instance.age = 42
struct_instance = module.MyData(name="testing")
assert struct_instance.name == "testing"
def test_load_file_fail():
with pytest.raises(CompileError, match="Could not compile non-existent.slint"):

View file

@ -13,6 +13,11 @@ export global SecondGlobal {
out property <string> second: "second";
}
export struct MyData {
name: string,
age: int
}
export component App inherits Window {
in-out property <string> hello: "World";
callback say-hello(string) -> string;

View file

@ -165,6 +165,12 @@ impl PyStruct {
}
}
impl From<slint_interpreter::Struct> for PyStruct {
fn from(data: slint_interpreter::Struct) -> Self {
Self { data }
}
}
#[pyclass(unsendable)]
struct PyStructFieldIterator {
inner: std::collections::hash_map::IntoIter<String, slint_interpreter::Value>,