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>,

View file

@ -9,6 +9,8 @@ import copy
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
PrinterQueueItem = slint.loader.ui.printerdemo.PrinterQueueItem
class MainWindow(slint.loader.ui.printerdemo.MainWindow):
def __init__(self):
@ -32,15 +34,15 @@ class MainWindow(slint.loader.ui.printerdemo.MainWindow):
@slint.callback(global_name="PrinterQueue", name="start_job")
def push_job(self, title):
self.printer_queue.append({
"status": "waiting",
"progress": 0,
"title": title,
"owner": "Me",
"pages": 1,
"size": "100kB",
"submission_date": str(datetime.now()),
})
self.printer_queue.append(PrinterQueueItem(
status="waiting",
progress=0,
title=title,
owner="Me",
pages=1,
size="100kB",
submission_date=str(datetime.now()),
))
@slint.callback(global_name="PrinterQueue")
def cancel_job(self, index):

View file

@ -243,6 +243,10 @@ impl Document {
self.exports.iter().filter_map(|e| e.1.as_ref().left()).filter(|c| !c.is_global()).cloned()
}
pub fn exposed_structs_and_enums(&self) -> Vec<Type> {
self.used_types.borrow().structs_and_enums.clone()
}
/// This is the component that is going to be instantiated by the interpreter
pub fn last_exported_component(&self) -> Option<Rc<Component>> {
self.exports

View file

@ -814,6 +814,7 @@ impl Compiler {
return CompilationResult {
components: HashMap::new(),
diagnostics: diagnostics.into_iter().collect(),
structs_and_enums: Vec::new(),
};
}
};
@ -848,6 +849,7 @@ impl Compiler {
pub struct CompilationResult {
pub(crate) components: HashMap<String, ComponentDefinition>,
pub(crate) diagnostics: Vec<Diagnostic>,
pub(crate) structs_and_enums: Vec<LangType>,
}
impl core::fmt::Debug for CompilationResult {
@ -898,6 +900,16 @@ impl CompilationResult {
pub fn component(&self, name: &str) -> Option<ComponentDefinition> {
self.components.get(name).cloned()
}
/// This is an internal function without and ABI or API stability guarantees.
#[doc(hidden)]
#[cfg(feature = "internal")]
pub fn structs_and_enums(
&self,
_: i_slint_core::InternalToken,
) -> impl Iterator<Item = &LangType> {
self.structs_and_enums.iter()
}
}
/// ComponentDefinition is a representation of a compiled component from .slint markup.

View file

@ -844,6 +844,7 @@ pub async fn load(
return CompilationResult {
components: HashMap::new(),
diagnostics: diag.into_iter().collect(),
structs_and_enums: Vec::new(),
};
}
@ -873,7 +874,11 @@ pub async fn load(
diag.push_error_with_span("No component found".into(), Default::default());
};
CompilationResult { diagnostics: diag.into_iter().collect(), components }
CompilationResult {
diagnostics: diag.into_iter().collect(),
components,
structs_and_enums: doc.exposed_structs_and_enums(),
}
}
pub(crate) fn generate_item_tree<'id>(