Python: Add support for enums

Support for enums is three-fold:

- Enums are dynamically created by invoking `enum.Enum("NameOfEnum",
  [("Variant1", "Variant1"), ("Variant2", "Variant2"), ...])` and the
  exposed in the module (next to structs).
- They're converted back to `Value::EnumerationValue(enum_name,
  enum_value)` by checking if a supplied value is an instance of
  `enum.Enum` and then querying the name and `__class__.name` fields.
- When coverting a `Value::EnumerationValue` to Python, the hard work
  begins: We now pass along a TypeCollection through all phases of the
  type conversion, which knows about the enums created earlier and tries
  to locate the correct type class and then variant.

Fixes #5828
This commit is contained in:
Simon Hausmann 2025-07-07 17:55:44 +02:00 committed by Simon Hausmann
parent 14e1ae3bc0
commit 82ae731fcb
10 changed files with 504 additions and 183 deletions

View file

@ -295,6 +295,37 @@ data.age = 10
main_window.data = data
```
### Enums
Enums declared in Slint and exposed to Python via `export` are then accessible in the namespace that is returned
when [instantiating a component](#instantiating-a-component). The enums are subclasses of [enum.Enum](https://docs.python.org/3/library/enum.html).
**`app.slint`**
```slint
export enum MyOption {
Variant1,
Variant2
}
export component MainWindow inherits Window {
in-out property <MyOption> data;
}
```
**`main.py`**
Variants of the exported `MyOption` enum can be constructed as follows:
```python
import slint
# Look for for `app.slint` in `sys.path`:
main_window = slint.loader.app.MainWindow()
value = slint.loader.app.MyOption.Variant2
main_window.data = value
```
## Third-Party Licenses
For a list of the third-party licenses of all dependencies, see the separate [Third-Party Licenses page](thirdparty.html).

View file

@ -21,7 +21,7 @@ use pyo3::PyTraverseError;
use crate::errors::{
PyGetPropertyError, PyInvokeError, PyPlatformError, PySetCallbackError, PySetPropertyError,
};
use crate::value::{PyStruct, PyToSlintValue, SlintToPyValue};
use crate::value::{SlintToPyValue, TypeCollection};
#[gen_stub_pyclass]
#[pyclass(unsendable)]
@ -72,14 +72,20 @@ impl Compiler {
self.compiler.set_translation_domain(domain)
}
fn build_from_path(&mut self, path: PathBuf) -> CompilationResult {
CompilationResult { result: spin_on::spin_on(self.compiler.build_from_path(path)) }
fn build_from_path(&mut self, py: Python<'_>, path: PathBuf) -> CompilationResult {
CompilationResult::new(spin_on::spin_on(self.compiler.build_from_path(path)), py)
}
fn build_from_source(&mut self, source_code: String, path: PathBuf) -> CompilationResult {
CompilationResult {
result: spin_on::spin_on(self.compiler.build_from_source(source_code, path)),
}
fn build_from_source(
&mut self,
py: Python<'_>,
source_code: String,
path: PathBuf,
) -> CompilationResult {
CompilationResult::new(
spin_on::spin_on(self.compiler.build_from_source(source_code, path)),
py,
)
}
}
@ -137,6 +143,14 @@ pub enum PyDiagnosticLevel {
#[pyclass(unsendable)]
pub struct CompilationResult {
result: slint_interpreter::CompilationResult,
type_collection: TypeCollection,
}
impl CompilationResult {
fn new(result: slint_interpreter::CompilationResult, py: Python<'_>) -> Self {
let type_collection = TypeCollection::new(&result, py);
Self { result, type_collection }
}
}
#[gen_stub_pymethods]
@ -148,7 +162,10 @@ impl CompilationResult {
}
fn component(&self, name: &str) -> Option<ComponentDefinition> {
self.result.component(name).map(|definition| ComponentDefinition { definition })
self.result.component(name).map(|definition| ComponentDefinition {
definition,
type_collection: self.type_collection.clone(),
})
}
#[getter]
@ -157,40 +174,41 @@ impl CompilationResult {
}
#[getter]
fn structs_and_enums<'py>(&self, py: Python<'py>) -> HashMap<String, Bound<'py, PyAny>> {
let structs_and_enums =
self.result.structs_and_enums(i_slint_core::InternalToken {}).collect::<Vec<_>>();
fn structs_and_enums<'py>(
&self,
py: Python<'py>,
) -> (HashMap<String, Bound<'py, PyAny>>, HashMap<String, Bound<'py, PyAny>>) {
let mut structs = HashMap::new();
fn convert_type<'py>(py: Python<'py>, ty: &Type) -> Option<(String, Bound<'py, PyAny>)> {
match ty {
for struct_or_enum in self.result.structs_and_enums(i_slint_core::InternalToken {}) {
match struct_or_enum {
Type::Struct(s) if s.name.is_some() && s.node.is_some() => {
let struct_instance = PyStruct::from(slint_interpreter::Struct::from_iter(
s.fields.iter().map(|(name, field_type)| {
(
name.to_string(),
slint_interpreter::default_value_for_type(field_type),
)
}),
));
let struct_instance =
self.type_collection.struct_to_py(slint_interpreter::Struct::from_iter(
s.fields.iter().map(|(name, field_type)| {
(
name.to_string(),
slint_interpreter::default_value_for_type(field_type),
)
}),
));
return Some((
structs.insert(
s.name.as_ref().unwrap().to_string(),
struct_instance.into_bound_py_any(py).unwrap(),
));
}
Type::Enumeration(_en) => {
// TODO
);
}
_ => {}
}
None
}
structs_and_enums
.iter()
.filter_map(|ty| convert_type(py, ty))
.into_iter()
.collect::<HashMap<String, Bound<'py, PyAny>>>()
(
structs,
self.type_collection
.enums()
.map(|(name, enum_cls)| (name.clone(), enum_cls.into_bound_py_any(py).unwrap()))
.collect(),
)
}
#[getter]
@ -203,6 +221,7 @@ impl CompilationResult {
#[pyclass(unsendable)]
pub struct ComponentDefinition {
definition: slint_interpreter::ComponentDefinition,
type_collection: TypeCollection,
}
#[pymethods]
@ -214,7 +233,10 @@ impl ComponentDefinition {
#[getter]
fn properties(&self) -> IndexMap<String, PyValueType> {
self.definition.properties().map(|(name, ty)| (name, ty.into())).collect()
self.definition
.properties_and_callbacks()
.filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (name, ty.into())))
.collect()
}
#[getter]
@ -233,9 +255,11 @@ impl ComponentDefinition {
}
fn global_properties(&self, name: &str) -> Option<IndexMap<String, PyValueType>> {
self.definition
.global_properties(name)
.map(|propiter| propiter.map(|(name, ty)| (name, ty.into())).collect())
self.definition.global_properties_and_callbacks(name).map(|propiter| {
propiter
.filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (name, ty.into())))
.collect()
})
}
fn global_callbacks(&self, name: &str) -> Option<Vec<String>> {
@ -249,8 +273,12 @@ impl ComponentDefinition {
fn create(&self) -> Result<ComponentInstance, crate::errors::PyPlatformError> {
Ok(ComponentInstance {
instance: self.definition.create()?,
callbacks: Default::default(),
callbacks: GcVisibleCallbacks {
callables: Default::default(),
type_collection: self.type_collection.clone(),
},
global_callbacks: Default::default(),
type_collection: self.type_collection.clone(),
})
}
}
@ -267,19 +295,29 @@ pub enum PyValueType {
Struct,
Brush,
Image,
Enumeration,
}
impl From<slint_interpreter::ValueType> for PyValueType {
fn from(value: slint_interpreter::ValueType) -> Self {
match value {
slint_interpreter::ValueType::Bool => PyValueType::Bool,
slint_interpreter::ValueType::Void => PyValueType::Void,
slint_interpreter::ValueType::Number => PyValueType::Number,
slint_interpreter::ValueType::String => PyValueType::String,
slint_interpreter::ValueType::Model => PyValueType::Model,
slint_interpreter::ValueType::Struct => PyValueType::Struct,
slint_interpreter::ValueType::Brush => PyValueType::Brush,
slint_interpreter::ValueType::Image => PyValueType::Image,
impl From<i_slint_compiler::langtype::Type> for PyValueType {
fn from(ty: i_slint_compiler::langtype::Type) -> Self {
match ty {
i_slint_compiler::langtype::Type::Bool => PyValueType::Bool,
i_slint_compiler::langtype::Type::Void => PyValueType::Void,
i_slint_compiler::langtype::Type::Float32
| i_slint_compiler::langtype::Type::Int32
| i_slint_compiler::langtype::Type::Duration
| i_slint_compiler::langtype::Type::Angle
| i_slint_compiler::langtype::Type::PhysicalLength
| i_slint_compiler::langtype::Type::LogicalLength
| i_slint_compiler::langtype::Type::Percent
| i_slint_compiler::langtype::Type::UnitProduct(_) => PyValueType::Number,
i_slint_compiler::langtype::Type::String => PyValueType::String,
i_slint_compiler::langtype::Type::Array(..) => PyValueType::Model,
i_slint_compiler::langtype::Type::Struct { .. } => PyValueType::Struct,
i_slint_compiler::langtype::Type::Brush => PyValueType::Brush,
i_slint_compiler::langtype::Type::Color => PyValueType::Brush,
i_slint_compiler::langtype::Type::Image => PyValueType::Image,
i_slint_compiler::langtype::Type::Enumeration(..) => PyValueType::Enumeration,
_ => unimplemented!(),
}
}
@ -291,22 +329,27 @@ pub struct ComponentInstance {
instance: slint_interpreter::ComponentInstance,
callbacks: GcVisibleCallbacks,
global_callbacks: HashMap<String, GcVisibleCallbacks>,
type_collection: TypeCollection,
}
#[pymethods]
impl ComponentInstance {
#[getter]
fn definition(&self) -> ComponentDefinition {
ComponentDefinition { definition: self.instance.definition() }
ComponentDefinition {
definition: self.instance.definition(),
type_collection: self.type_collection.clone(),
}
}
fn get_property(&self, name: &str) -> Result<SlintToPyValue, PyGetPropertyError> {
Ok(self.instance.get_property(name)?.into())
Ok(self.type_collection.to_py_value(self.instance.get_property(name)?))
}
fn set_property(&self, name: &str, value: Bound<'_, PyAny>) -> PyResult<()> {
let pv: PyToSlintValue = value.extract()?;
Ok(self.instance.set_property(name, pv.0).map_err(|e| PySetPropertyError(e))?)
let pv =
TypeCollection::slint_value_from_py_value_bound(&value, Some(&self.type_collection))?;
Ok(self.instance.set_property(name, pv).map_err(|e| PySetPropertyError(e))?)
}
fn get_global_property(
@ -314,7 +357,9 @@ impl ComponentInstance {
global_name: &str,
prop_name: &str,
) -> Result<SlintToPyValue, PyGetPropertyError> {
Ok(self.instance.get_global_property(global_name, prop_name)?.into())
Ok(self
.type_collection
.to_py_value(self.instance.get_global_property(global_name, prop_name)?))
}
fn set_global_property(
@ -323,10 +368,11 @@ impl ComponentInstance {
prop_name: &str,
value: Bound<'_, PyAny>,
) -> PyResult<()> {
let pv: PyToSlintValue = value.extract()?;
let pv =
TypeCollection::slint_value_from_py_value_bound(&value, Some(&self.type_collection))?;
Ok(self
.instance
.set_global_property(global_name, prop_name, pv.0)
.set_global_property(global_name, prop_name, pv)
.map_err(|e| PySetPropertyError(e))?)
}
@ -334,10 +380,13 @@ impl ComponentInstance {
fn invoke(&self, callback_name: &str, args: Bound<'_, PyTuple>) -> PyResult<SlintToPyValue> {
let mut rust_args = vec![];
for arg in args.iter() {
let pv: PyToSlintValue = arg.extract()?;
rust_args.push(pv.0)
let pv =
TypeCollection::slint_value_from_py_value_bound(&arg, Some(&self.type_collection))?;
rust_args.push(pv)
}
Ok(self.instance.invoke(callback_name, &rust_args).map_err(|e| PyInvokeError(e))?.into())
Ok(self.type_collection.to_py_value(
self.instance.invoke(callback_name, &rust_args).map_err(|e| PyInvokeError(e))?,
))
}
#[pyo3(signature = (global_name, callback_name, *args))]
@ -349,14 +398,15 @@ impl ComponentInstance {
) -> PyResult<SlintToPyValue> {
let mut rust_args = vec![];
for arg in args.iter() {
let pv: PyToSlintValue = arg.extract()?;
rust_args.push(pv.0)
let pv =
TypeCollection::slint_value_from_py_value_bound(&arg, Some(&self.type_collection))?;
rust_args.push(pv)
}
Ok(self
.instance
.invoke_global(global_name, callback_name, &rust_args)
.map_err(|e| PyInvokeError(e))?
.into())
Ok(self.type_collection.to_py_value(
self.instance
.invoke_global(global_name, callback_name, &rust_args)
.map_err(|e| PyInvokeError(e))?,
))
}
fn set_callback(&self, name: &str, callable: PyObject) -> Result<(), PySetCallbackError> {
@ -373,7 +423,10 @@ impl ComponentInstance {
let rust_cb = self
.global_callbacks
.entry(global_name.to_string())
.or_default()
.or_insert_with(|| GcVisibleCallbacks {
callables: Default::default(),
type_collection: self.type_collection.clone(),
})
.register(callback_name.to_string(), callable);
Ok(self.instance.set_global_callback(global_name, callback_name, rust_cb)?.into())
}
@ -404,9 +457,9 @@ impl ComponentInstance {
}
}
#[derive(Default)]
struct GcVisibleCallbacks {
callables: Rc<RefCell<HashMap<String, PyObject>>>,
type_collection: TypeCollection,
}
impl GcVisibleCallbacks {
@ -414,13 +467,15 @@ impl GcVisibleCallbacks {
self.callables.borrow_mut().insert(name.clone(), callable);
let callables = self.callables.clone();
let type_collection = self.type_collection.clone();
move |args| {
let callables = callables.borrow();
let callable = callables.get(&name).unwrap();
Python::with_gil(|py| {
let py_args =
PyTuple::new(py, args.iter().map(|v| SlintToPyValue(v.clone()))).unwrap();
PyTuple::new(py, args.iter().map(|v| type_collection.to_py_value(v.clone())))
.unwrap();
let result = match callable.call(py, py_args, None) {
Ok(result) => result,
Err(err) => {
@ -430,14 +485,19 @@ impl GcVisibleCallbacks {
return Value::Void;
}
};
let pv: PyToSlintValue = match result.extract(py) {
let pv = match TypeCollection::slint_value_from_py_value(
py,
&result,
Some(&type_collection),
) {
Ok(value) => value,
Err(err) => {
eprintln!("Python: Unable to convert return value of Python callback for {name} to Slint value: {err}");
return Value::Void;
}
};
pv.0
pv
})
}
}

View file

@ -11,11 +11,24 @@ use pyo3::gc::PyVisit;
use pyo3::prelude::*;
use pyo3::PyTraverseError;
use crate::value::{PyToSlintValue, SlintToPyValue};
use crate::value::{SlintToPyValue, TypeCollection};
pub struct PyModelShared {
notify: ModelNotify,
self_ref: RefCell<Option<PyObject>>,
/// The type collection is needed when calling a Python implementation of set_row_data and
/// the model data provided (for example from within a .slint file) contains an enum. Then
/// we need to know how to map it to the correct Python enum. This field is lazily set, whenever
/// time the Python model is exposed to Slint.
type_collection: RefCell<Option<TypeCollection>>,
}
impl PyModelShared {
pub fn apply_type_collection(&self, type_collection: &TypeCollection) {
if let Ok(mut type_collection_ref) = self.type_collection.try_borrow_mut() {
*type_collection_ref = Some(type_collection.clone());
}
}
}
#[derive(Clone)]
@ -38,6 +51,7 @@ impl PyModelBase {
inner: Rc::new(PyModelShared {
notify: Default::default(),
self_ref: RefCell::new(None),
type_collection: RefCell::new(None),
}),
}
}
@ -119,8 +133,12 @@ impl i_slint_core::model::Model for PyModelShared {
}
};
match result.extract::<PyToSlintValue>(py) {
Ok(pv) => Some(pv.0),
match TypeCollection::slint_value_from_py_value(
py,
&result,
self.type_collection.borrow().as_ref(),
) {
Ok(pv) => Some(pv),
Err(err) => {
eprintln!("Python: Model implementation of row_data() returned value that cannot be converted to Rust: {err}");
None
@ -137,8 +155,15 @@ impl i_slint_core::model::Model for PyModelShared {
return;
};
let Some(type_collection) = self.type_collection.borrow().as_ref().cloned() else {
eprintln!(
"Python: Model implementation is lacking type collection (in set_row_data)"
);
return;
};
if let Err(err) =
obj.call_method1(py, "set_row_data", (row, SlintToPyValue::from(data)))
obj.call_method1(py, "set_row_data", (row, type_collection.to_py_value(data)))
{
eprintln!(
"Python: Model implementation of set_row_data() threw an exception: {err}"
@ -168,16 +193,19 @@ impl PyModelShared {
}
#[pyclass(unsendable)]
pub struct ReadOnlyRustModel(pub ModelRc<slint_interpreter::Value>);
pub struct ReadOnlyRustModel {
pub model: ModelRc<slint_interpreter::Value>,
pub type_collection: TypeCollection,
}
#[pymethods]
impl ReadOnlyRustModel {
fn row_count(&self) -> usize {
self.0.row_count()
self.model.row_count()
}
fn row_data(&self, row: usize) -> Option<SlintToPyValue> {
self.0.row_data(row).map(|value| value.into())
self.model.row_data(row).map(|value| self.type_collection.to_py_value(value))
}
fn __len__(&self) -> usize {
@ -185,7 +213,11 @@ impl ReadOnlyRustModel {
}
fn __iter__(slf: PyRef<'_, Self>) -> ReadOnlyRustModelIterator {
ReadOnlyRustModelIterator { model: slf.0.clone(), row: 0 }
ReadOnlyRustModelIterator {
model: slf.model.clone(),
row: 0,
type_collection: slf.type_collection.clone(),
}
}
fn __getitem__(&self, index: usize) -> Option<SlintToPyValue> {
@ -193,16 +225,11 @@ impl ReadOnlyRustModel {
}
}
impl From<&ModelRc<slint_interpreter::Value>> for ReadOnlyRustModel {
fn from(model: &ModelRc<slint_interpreter::Value>) -> Self {
Self(model.clone())
}
}
#[pyclass(unsendable)]
struct ReadOnlyRustModelIterator {
model: ModelRc<slint_interpreter::Value>,
row: usize,
type_collection: TypeCollection,
}
#[pymethods]
@ -217,6 +244,6 @@ impl ReadOnlyRustModelIterator {
}
let row = self.row;
self.row += 1;
self.model.row_data(row).map(|value| value.into())
self.model.row_data(row).map(|value| self.type_collection.to_py_value(value))
}
}

View file

@ -300,11 +300,17 @@ def load_file(
setattr(module, comp_name, wrapper_class)
for name, struct_or_enum_prototype in result.structs_and_enums.items():
structs, enums = result.structs_and_enums
for name, struct_prototype in structs.items():
name = _normalize_prop(name)
struct_wrapper = _build_struct(name, struct_or_enum_prototype)
struct_wrapper = _build_struct(name, struct_prototype)
setattr(module, name, struct_wrapper)
for name, enum_class in enums.items():
name = _normalize_prop(name)
setattr(module, name, enum_class)
for orig_name, new_name in result.named_exports:
orig_name = _normalize_prop(orig_name)
new_name = _normalize_prop(new_name)

View file

@ -171,7 +171,7 @@ class CompilationResult:
component_names: list[str]
diagnostics: list[PyDiagnostic]
named_exports: list[typing.Tuple[str, str]]
structs_and_enums: typing.Dict[str, PyStruct]
structs_and_enums: typing.Tuple[typing.Dict[str, PyStruct], typing.Dict[str, Enum]]
def component(self, name: str) -> ComponentDefinition: ...
class Compiler:

View file

@ -24,6 +24,11 @@ struct Secret-Struct {
export { Secret-Struct as Public-Struct }
export enum TestEnum {
Variant1,
Variant2,
}
export component App inherits Window {
in-out property <string> hello: "World";
callback say-hello(string) -> string;
@ -33,21 +38,21 @@ export component App inherits Window {
invoke-say-hello(arg) => {
return self.say-hello(arg);
}
callback invoke-say-hello-again(string) -> string;
invoke-say-hello-again(arg) => {
return self.say-hello-again(arg);
}
callback invoke-global-callback(string) -> string;
invoke-global-callback(arg) => {
return MyGlobal.global-callback(arg);
}
public function plus-one(value: int) {
return value + 1;
}
in-out property <TestEnum> enum-property: TestEnum.Variant2;
in-out property <[TestEnum]> model-with-enums: [TestEnum.Variant2, TestEnum.Variant1];
Rectangle {
color: red;
}

View file

@ -0,0 +1,49 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
import pytest
from slint import load_file, ListModel
from pathlib import Path
def base_dir() -> Path:
origin = __spec__.origin
assert origin is not None
base_dir = Path(origin).parent
assert base_dir is not None
return base_dir
def test_enums() -> None:
module = load_file(base_dir() / "test-load-file.slint", quiet=False)
TestEnum = module.TestEnum
assert TestEnum.Variant1.name == "Variant1"
assert TestEnum.Variant1.value == "Variant1"
assert TestEnum.Variant2.name == "Variant2"
assert TestEnum.Variant2.value == "Variant2"
with pytest.raises(
AttributeError, match="type object 'TestEnum' has no attribute 'Variant3'"
):
TestEnum.Variant3
instance = module.App()
assert instance.enum_property == TestEnum.Variant2
assert instance.enum_property.__class__ is TestEnum
instance.enum_property = TestEnum.Variant1
assert instance.enum_property == TestEnum.Variant1
assert instance.enum_property.__class__ is TestEnum
model_with_enums = instance.model_with_enums
assert len(model_with_enums) == 2
assert model_with_enums[0] == TestEnum.Variant2
assert model_with_enums[1] == TestEnum.Variant1
assert model_with_enums[0].__class__ is TestEnum
model_with_enums = None
instance.model_with_enums = ListModel([TestEnum.Variant1, TestEnum.Variant2])
assert len(instance.model_with_enums) == 2
assert instance.model_with_enums[0] == TestEnum.Variant1
assert instance.model_with_enums[1] == TestEnum.Variant2
assert instance.model_with_enums[0].__class__ is TestEnum

View file

@ -22,13 +22,14 @@ def test_load_file(caplog: pytest.LogCaptureFixture) -> None:
in caplog.text
)
assert len(list(module.__dict__.keys())) == 6
assert len(list(module.__dict__.keys())) == 7
assert "App" in module.__dict__
assert "Diag" in module.__dict__
assert "MyDiag" in module.__dict__
assert "MyData" in module.__dict__
assert "Secret_Struct" in module.__dict__
assert "Public_Struct" in module.__dict__
assert "TestEnum" in module.__dict__
instance = module.App()
del instance
instance = module.MyDiag()

View file

@ -6,13 +6,19 @@ use pyo3::types::PyDict;
use pyo3::IntoPyObjectExt;
use pyo3_stub_gen::{derive::gen_stub_pyclass, derive::gen_stub_pymethods};
use std::cell::OnceCell;
use std::collections::HashMap;
use std::rc::Rc;
use i_slint_compiler::langtype::Type;
use i_slint_core::model::{Model, ModelRc};
#[gen_stub_pyclass]
pub struct PyToSlintValue(pub slint_interpreter::Value);
#[gen_stub_pyclass]
pub struct SlintToPyValue(pub slint_interpreter::Value);
pub struct SlintToPyValue {
pub slint_value: slint_interpreter::Value,
pub type_collection: TypeCollection,
}
impl<'py> IntoPyObject<'py> for SlintToPyValue {
type Target = PyAny;
@ -20,7 +26,8 @@ impl<'py> IntoPyObject<'py> for SlintToPyValue {
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match self.0 {
let type_collection = self.type_collection;
match self.slint_value {
slint_interpreter::Value::Void => ().into_bound_py_any(py),
slint_interpreter::Value::Number(num) => num.into_bound_py_any(py),
slint_interpreter::Value::String(str) => str.into_bound_py_any(py),
@ -30,16 +37,19 @@ impl<'py> IntoPyObject<'py> for SlintToPyValue {
}
slint_interpreter::Value::Model(model) => {
crate::models::PyModelShared::rust_into_py_model(&model, py).map_or_else(
|| crate::models::ReadOnlyRustModel::from(&model).into_bound_py_any(py),
|| type_collection.model_to_py(&model).into_bound_py_any(py),
|m| Ok(m),
)
}
slint_interpreter::Value::Struct(structval) => {
PyStruct { data: structval }.into_bound_py_any(py)
type_collection.struct_to_py(structval).into_bound_py_any(py)
}
slint_interpreter::Value::Brush(brush) => {
crate::brush::PyBrush::from(brush).into_bound_py_any(py)
}
slint_interpreter::Value::EnumerationValue(enum_name, enum_value) => {
type_collection.enum_to_py(&enum_name, &enum_value, py)?.into_bound_py_any(py)
}
v @ _ => {
eprintln!("Python: conversion from slint to python needed for {v:#?} and not implemented yet");
().into_bound_py_any(py)
@ -48,10 +58,175 @@ impl<'py> IntoPyObject<'py> for SlintToPyValue {
}
}
impl<'py> FromPyObject<'py> for PyToSlintValue {
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
#[gen_stub_pyclass]
#[pyclass(subclass, unsendable)]
#[derive(Clone)]
pub struct PyStruct {
pub data: slint_interpreter::Struct,
pub type_collection: TypeCollection,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyStruct {
fn __getattr__(&self, key: &str) -> PyResult<SlintToPyValue> {
self.data.get_field(key).map_or_else(
|| {
Err(pyo3::exceptions::PyAttributeError::new_err(format!(
"Python: No such field {key} on PyStruct"
)))
},
|value| Ok(self.type_collection.to_py_value(value.clone())),
)
}
fn __setattr__(&mut self, py: Python<'_>, key: String, value: PyObject) -> PyResult<()> {
let pv =
TypeCollection::slint_value_from_py_value(py, &value, Some(&self.type_collection))?;
self.data.set_field(key, pv);
Ok(())
}
fn __iter__(slf: PyRef<'_, Self>) -> PyStructFieldIterator {
PyStructFieldIterator {
inner: slf
.data
.iter()
.map(|(name, val)| (name.to_string(), val.clone()))
.collect::<HashMap<_, _>>()
.into_iter(),
type_collection: slf.type_collection.clone(),
}
}
fn __copy__(&self) -> Self {
self.clone()
}
}
#[gen_stub_pyclass]
#[pyclass(unsendable)]
struct PyStructFieldIterator {
inner: std::collections::hash_map::IntoIter<String, slint_interpreter::Value>,
type_collection: TypeCollection,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyStructFieldIterator {
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}
fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(String, SlintToPyValue)> {
slf.inner.next().map(|(name, val)| (name, slf.type_collection.to_py_value(val)))
}
}
thread_local! {
static ENUM_CLASS: OnceCell<PyObject> = OnceCell::new();
}
pub fn enum_class(py: Python) -> PyObject {
ENUM_CLASS.with(|cls| {
cls.get_or_init(|| -> PyObject {
let enum_module = py.import("enum").unwrap();
enum_module.getattr("Enum").unwrap().into()
})
.clone_ref(py)
})
}
#[derive(Clone)]
/// Struct that knows about the enums (and maybe other types) exported by
/// a `.slint` file loaded with load_file. This is used to map enums
/// provided by Slint to the correct python enum classes.
pub struct TypeCollection {
enum_classes: Rc<HashMap<String, PyObject>>,
}
impl TypeCollection {
pub fn new(result: &slint_interpreter::CompilationResult, py: Python<'_>) -> Self {
let mut enum_classes = HashMap::new();
let enum_ctor = crate::value::enum_class(py);
for struct_or_enum in result.structs_and_enums(i_slint_core::InternalToken {}) {
match struct_or_enum {
Type::Enumeration(en) => {
let enum_type = enum_ctor
.call(
py,
(
en.name.to_string(),
en.values
.iter()
.map(|val| {
let val = val.to_string();
(val.clone(), val)
})
.collect::<Vec<_>>(),
),
None,
)
.unwrap();
enum_classes.insert(en.name.to_string(), enum_type);
}
_ => {}
}
}
let enum_classes = Rc::new(enum_classes);
Self { enum_classes }
}
pub fn to_py_value(&self, value: slint_interpreter::Value) -> SlintToPyValue {
SlintToPyValue { slint_value: value, type_collection: self.clone() }
}
pub fn struct_to_py(&self, s: slint_interpreter::Struct) -> PyStruct {
PyStruct { data: s, type_collection: self.clone() }
}
pub fn enum_to_py(
&self,
enum_name: &str,
enum_value: &str,
py: Python<'_>,
) -> Result<PyObject, PyErr> {
let enum_cls = self.enum_classes.get(enum_name).ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyTypeError, _>(format!(
"Slint provided enum {enum_name} is unknown"
))
})?;
enum_cls.getattr(py, enum_value)
}
pub fn model_to_py(
&self,
model: &ModelRc<slint_interpreter::Value>,
) -> crate::models::ReadOnlyRustModel {
crate::models::ReadOnlyRustModel { model: model.clone(), type_collection: self.clone() }
}
pub fn enums(&self) -> impl Iterator<Item = (&String, &PyObject)> {
self.enum_classes.iter()
}
pub fn slint_value_from_py_value(
py: Python<'_>,
ob: &PyObject,
type_collection: Option<&Self>,
) -> PyResult<slint_interpreter::Value> {
Self::slint_value_from_py_value_bound(&ob.bind(py), type_collection)
}
pub fn slint_value_from_py_value_bound(
ob: &Bound<'_, PyAny>,
type_collection: Option<&Self>,
) -> PyResult<slint_interpreter::Value> {
if ob.is_none() {
return Ok(Self(slint_interpreter::Value::Void));
return Ok(slint_interpreter::Value::Void);
}
let interpreter_val = ob
@ -74,26 +249,53 @@ impl<'py> FromPyObject<'py> for PyToSlintValue {
.map(|pycolor| slint_interpreter::Value::Brush(pycolor.color.clone().into()))
})
.or_else(|_| {
ob.extract::<PyRef<'_, crate::models::PyModelBase>>()
.map(|pymodel| slint_interpreter::Value::Model(pymodel.as_model()))
ob.extract::<PyRef<'_, crate::models::PyModelBase>>().map(|pymodel| {
slint_interpreter::Value::Model(Self::apply(
type_collection,
pymodel.as_model(),
))
})
})
.or_else(|_| {
ob.extract::<PyRef<'_, crate::models::ReadOnlyRustModel>>()
.map(|rustmodel| slint_interpreter::Value::Model(rustmodel.0.clone()))
ob.extract::<PyRef<'_, crate::models::ReadOnlyRustModel>>().map(|rustmodel| {
slint_interpreter::Value::Model(Self::apply(
type_collection,
rustmodel.model.clone(),
))
})
})
.or_else(|_| {
ob.extract::<PyRef<'_, PyStruct>>().and_then(|pystruct| {
Ok(slint_interpreter::Value::Struct(pystruct.data.clone()))
})
})
.or_else(|_| {
ob.is_instance(&enum_class(ob.py()).into_bound(ob.py())).and_then(|r| {
r.then(|| {
let enum_name =
ob.getattr("__class__").and_then(|cls| cls.getattr("__name__"))?;
let enum_value = ob.getattr("name")?;
Ok(slint_interpreter::Value::EnumerationValue(
enum_name.to_string(),
enum_value.to_string(),
))
})
.unwrap_or_else(|| {
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
"Object to convert is not an enum",
))
})
})
})
.or_else(|_| {
let dict = ob.downcast::<PyDict>()?;
let dict_items: Result<Vec<(String, slint_interpreter::Value)>, PyErr> = dict
.iter()
.map(|(name, pyval)| {
let name = name.extract::<&str>()?.to_string();
let slintval = PyToSlintValue::extract_bound(&pyval)?;
Ok((name, slintval.0))
let slintval =
Self::slint_value_from_py_value_bound(&pyval, type_collection)?;
Ok((name, slintval))
})
.collect::<Result<Vec<(_, _)>, PyErr>>();
Ok::<_, PyErr>(slint_interpreter::Value::Struct(
@ -101,83 +303,19 @@ impl<'py> FromPyObject<'py> for PyToSlintValue {
))
})?;
Ok(PyToSlintValue(interpreter_val))
}
}
impl From<slint_interpreter::Value> for SlintToPyValue {
fn from(value: slint_interpreter::Value) -> Self {
Self(value)
}
}
#[gen_stub_pyclass]
#[pyclass(subclass, unsendable)]
#[derive(Clone, Default)]
pub struct PyStruct {
data: slint_interpreter::Struct,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyStruct {
#[new]
fn new() -> Self {
Default::default()
Ok(interpreter_val)
}
fn __getattr__(&self, key: &str) -> PyResult<SlintToPyValue> {
self.data.get_field(key).map_or_else(
|| {
Err(pyo3::exceptions::PyAttributeError::new_err(format!(
"Python: No such field {key} on PyStruct"
)))
},
|value| Ok(value.clone().into()),
)
}
fn __setattr__(&mut self, py: Python<'_>, key: String, value: PyObject) -> PyResult<()> {
let pv: PyToSlintValue = value.extract(py)?;
self.data.set_field(key, pv.0);
Ok(())
}
fn __iter__(slf: PyRef<'_, Self>) -> PyStructFieldIterator {
PyStructFieldIterator {
inner: slf
.data
.iter()
.map(|(name, val)| (name.to_string(), val.clone()))
.collect::<HashMap<_, _>>()
.into_iter(),
fn apply(
type_collection: Option<&Self>,
model: ModelRc<slint_interpreter::Value>,
) -> ModelRc<slint_interpreter::Value> {
let Some(type_collection) = type_collection else {
return model;
};
if let Some(rust_model) = model.as_any().downcast_ref::<crate::models::PyModelShared>() {
rust_model.apply_type_collection(type_collection);
}
}
fn __copy__(&self) -> Self {
self.clone()
}
}
impl From<slint_interpreter::Struct> for PyStruct {
fn from(data: slint_interpreter::Struct) -> Self {
Self { data }
}
}
#[gen_stub_pyclass]
#[pyclass(unsendable)]
struct PyStructFieldIterator {
inner: std::collections::hash_map::IntoIter<String, slint_interpreter::Value>,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyStructFieldIterator {
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}
fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(String, SlintToPyValue)> {
slf.inner.next().map(|(name, val)| (name, SlintToPyValue(val)))
model
}
}