Begin wrapping the component compiler

This commit is contained in:
Simon Hausmann 2022-12-13 20:44:13 +01:00 committed by Simon Hausmann
parent a2054e7ebd
commit 73024beb98
9 changed files with 646 additions and 0 deletions

View file

@ -25,6 +25,8 @@ i-slint-renderer-skia = { version = "=1.4.0", path="../../internal/renderers/ski
i-slint-core = { version = "=1.4.0", path="../../internal/core", features = ["ffi"] }
slint-interpreter = { workspace = true, features = ["default", "display-diagnostics", "internal"] }
pyo3 = { version = "0.20.0", features = ["extension-module", "indexmap"] }
indexmap = { version = "2.1.0" }
spin_on = "0.1"
[build-dependencies]
i-slint-common = { version = "=1.4.0", path="../../internal/common" }

432
api/python/interpreter.rs Normal file
View file

@ -0,0 +1,432 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use std::collections::HashMap;
use std::path::PathBuf;
use indexmap::IndexMap;
use pyo3::prelude::*;
use pyo3::types::PyTuple;
#[pyclass(unsendable)]
pub struct ComponentCompiler {
compiler: slint_interpreter::ComponentCompiler,
}
#[pymethods]
impl ComponentCompiler {
#[new]
fn py_new() -> PyResult<Self> {
Ok(Self { compiler: Default::default() })
}
#[getter]
fn get_include_paths(&self) -> PyResult<Vec<PathBuf>> {
Ok(self.compiler.include_paths().clone())
}
#[setter]
fn set_include_paths(&mut self, paths: Vec<PathBuf>) {
self.compiler.set_include_paths(paths)
}
#[getter]
fn get_style(&self) -> PyResult<Option<String>> {
Ok(self.compiler.style().cloned())
}
#[setter]
fn set_style(&mut self, style: String) {
self.compiler.set_style(style)
}
#[getter]
fn get_library_paths(&self) -> PyResult<HashMap<String, PathBuf>> {
Ok(self.compiler.library_paths().clone())
}
#[setter]
fn set_library_paths(&mut self, libraries: HashMap<String, PathBuf>) {
self.compiler.set_library_paths(libraries)
}
#[getter]
fn get_diagnostics(&self) -> Vec<PyDiagnostic> {
self.compiler.diagnostics().iter().map(|diag| PyDiagnostic(diag.clone())).collect()
}
#[setter]
fn set_translation_domain(&mut self, domain: String) {
self.compiler.set_translation_domain(domain)
}
fn build_from_path(&mut self, path: PathBuf) -> Option<ComponentDefinition> {
spin_on::spin_on(self.compiler.build_from_path(path))
.map(|definition| ComponentDefinition { definition })
}
fn build_from_source(
&mut self,
source_code: String,
path: PathBuf,
) -> Option<ComponentDefinition> {
spin_on::spin_on(self.compiler.build_from_source(source_code, path))
.map(|definition| ComponentDefinition { definition })
}
}
#[derive(Debug, Clone)]
#[pyclass(unsendable)]
pub struct PyDiagnostic(slint_interpreter::Diagnostic);
#[pymethods]
impl PyDiagnostic {
#[getter]
fn level(&self) -> PyDiagnosticLevel {
match self.0.level() {
slint_interpreter::DiagnosticLevel::Error => PyDiagnosticLevel::Error,
slint_interpreter::DiagnosticLevel::Warning => PyDiagnosticLevel::Warning,
_ => unimplemented!(),
}
}
#[getter]
fn message(&self) -> &str {
self.0.message()
}
#[getter]
fn column_number(&self) -> usize {
self.0.line_column().0
}
#[getter]
fn line_number(&self) -> usize {
self.0.line_column().1
}
#[getter]
fn source_file(&self) -> Option<PathBuf> {
self.0.source_file().map(|path| path.to_path_buf())
}
fn __str__(&self) -> String {
self.0.to_string()
}
}
#[pyclass(name = "DiagnosticLevel")]
pub enum PyDiagnosticLevel {
Error,
Warning,
}
#[pyclass(unsendable)]
struct ComponentDefinition {
definition: slint_interpreter::ComponentDefinition,
}
#[pymethods]
impl ComponentDefinition {
#[getter]
fn name(&self) -> &str {
self.definition.name()
}
#[getter]
fn properties(&self) -> IndexMap<String, PyValueType> {
self.definition.properties().map(|(name, ty)| (name, ty.into())).collect()
}
#[getter]
fn callbacks(&self) -> Vec<String> {
self.definition.callbacks().collect()
}
#[getter]
fn globals(&self) -> Vec<String> {
self.definition.globals().collect()
}
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())
}
fn global_callbacks(&self, name: &str) -> Option<Vec<String>> {
self.definition.global_callbacks(name).map(|callbackiter| callbackiter.collect())
}
fn create(&self) -> Result<ComponentInstance, PyPlatformError> {
Ok(ComponentInstance { instance: self.definition.create()? })
}
}
#[pyclass(name = "ValueType")]
pub enum PyValueType {
Void,
Number,
String,
Bool,
Model,
Struct,
Brush,
Image,
}
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,
_ => unimplemented!(),
}
}
}
#[pyclass(unsendable)]
struct ComponentInstance {
instance: slint_interpreter::ComponentInstance,
}
#[pymethods]
impl ComponentInstance {
#[getter]
fn definition(&self) -> ComponentDefinition {
ComponentDefinition { definition: self.instance.definition() }
}
fn get_property(&self, name: &str) -> Result<PyValue, PyGetPropertyError> {
Ok(self.instance.get_property(name)?.into())
}
fn set_property(&self, name: &str, value: &PyAny) -> PyResult<()> {
let pv: PyValue = value.extract()?;
Ok(self.instance.set_property(name, pv.0).map_err(|e| PySetPropertyError(e))?)
}
fn get_global_property(
&self,
global_name: &str,
prop_name: &str,
) -> Result<PyValue, PyGetPropertyError> {
Ok(self.instance.get_global_property(global_name, prop_name)?.into())
}
fn set_global_property(
&self,
global_name: &str,
prop_name: &str,
value: &PyAny,
) -> PyResult<()> {
let pv: PyValue = value.extract()?;
Ok(self
.instance
.set_global_property(global_name, prop_name, pv.0)
.map_err(|e| PySetPropertyError(e))?)
}
#[pyo3(signature = (callback_name, *args))]
fn invoke(&self, callback_name: &str, args: &PyTuple) -> PyResult<PyValue> {
let mut rust_args = vec![];
for arg in args.iter() {
let pv: PyValue = arg.extract()?;
rust_args.push(pv.0)
}
Ok(self.instance.invoke(callback_name, &rust_args).map_err(|e| PyInvokeError(e))?.into())
}
#[pyo3(signature = (global_name, callback_name, *args))]
fn invoke_global(
&self,
global_name: &str,
callback_name: &str,
args: &PyTuple,
) -> PyResult<PyValue> {
let mut rust_args = vec![];
for arg in args.iter() {
let pv: PyValue = arg.extract()?;
rust_args.push(pv.0)
}
Ok(self
.instance
.invoke_global(global_name, callback_name, &rust_args)
.map_err(|e| PyInvokeError(e))?
.into())
}
fn set_callback(
&self,
callback_name: &str,
callable: PyObject,
) -> Result<(), PySetCallbackError> {
Ok(self
.instance
.set_callback(callback_name, move |args| {
Python::with_gil(|py| {
let py_args = PyTuple::new(py, args.iter().map(|v| PyValue(v.clone())));
let result =
callable.call(py, py_args, None).expect("invoking python callback failed");
let pv: PyValue = result.extract(py).expect(
"unable to convert python callback result to slint interpreter value",
);
pv.0
})
})?
.into())
}
fn set_global_callback(
&self,
global_name: &str,
callback_name: &str,
callable: PyObject,
) -> Result<(), PySetCallbackError> {
Ok(self
.instance
.set_global_callback(global_name, callback_name, move |args| {
Python::with_gil(|py| {
let py_args = PyTuple::new(py, args.iter().map(|v| PyValue(v.clone())));
let result =
callable.call(py, py_args, None).expect("invoking python callback failed");
let pv: PyValue = result.extract(py).expect(
"unable to convert python callback result to slint interpreter value",
);
pv.0
})
})?
.into())
}
}
struct PyValue(slint_interpreter::Value);
impl IntoPy<PyObject> for PyValue {
fn into_py(self, py: Python<'_>) -> PyObject {
match self.0 {
slint_interpreter::Value::Void => ().into_py(py),
slint_interpreter::Value::Number(num) => num.into_py(py),
slint_interpreter::Value::String(str) => str.into_py(py),
slint_interpreter::Value::Bool(b) => b.into_py(py),
slint_interpreter::Value::Image(_) => todo!(),
slint_interpreter::Value::Model(_) => todo!(),
slint_interpreter::Value::Struct(_) => todo!(),
slint_interpreter::Value::Brush(_) => todo!(),
_ => todo!(),
}
}
}
impl ToPyObject for PyValue {
fn to_object(&self, py: Python<'_>) -> PyObject {
match &self.0 {
slint_interpreter::Value::Void => ().into_py(py),
slint_interpreter::Value::Number(num) => num.into_py(py),
slint_interpreter::Value::String(str) => str.into_py(py),
slint_interpreter::Value::Bool(b) => b.into_py(py),
slint_interpreter::Value::Image(_) => todo!(),
slint_interpreter::Value::Model(_) => todo!(),
slint_interpreter::Value::Struct(_) => todo!(),
slint_interpreter::Value::Brush(_) => todo!(),
_ => todo!(),
}
}
}
impl FromPyObject<'_> for PyValue {
fn extract(ob: &PyAny) -> PyResult<Self> {
Ok(PyValue(
ob.extract::<bool>()
.map(|b| slint_interpreter::Value::Bool(b))
.or_else(|_| {
ob.extract::<&'_ str>().map(|s| slint_interpreter::Value::String(s.into()))
})
.or_else(|_| {
ob.extract::<f64>().map(|num| slint_interpreter::Value::Number(num))
})?,
))
}
}
impl From<slint_interpreter::Value> for PyValue {
fn from(value: slint_interpreter::Value) -> Self {
Self(value)
}
}
struct PyGetPropertyError(slint_interpreter::GetPropertyError);
impl From<PyGetPropertyError> for PyErr {
fn from(err: PyGetPropertyError) -> Self {
pyo3::exceptions::PyValueError::new_err(err.0.to_string())
}
}
impl From<slint_interpreter::GetPropertyError> for PyGetPropertyError {
fn from(err: slint_interpreter::GetPropertyError) -> Self {
Self(err)
}
}
struct PySetPropertyError(slint_interpreter::SetPropertyError);
impl From<PySetPropertyError> for PyErr {
fn from(err: PySetPropertyError) -> Self {
pyo3::exceptions::PyValueError::new_err(err.0.to_string())
}
}
impl From<slint_interpreter::SetPropertyError> for PySetPropertyError {
fn from(err: slint_interpreter::SetPropertyError) -> Self {
Self(err)
}
}
struct PyPlatformError(slint_interpreter::PlatformError);
impl From<PyPlatformError> for PyErr {
fn from(err: PyPlatformError) -> Self {
pyo3::exceptions::PyRuntimeError::new_err(err.0.to_string())
}
}
impl From<slint_interpreter::PlatformError> for PyPlatformError {
fn from(err: slint_interpreter::PlatformError) -> Self {
Self(err)
}
}
struct PyInvokeError(slint_interpreter::InvokeError);
impl From<PyInvokeError> for PyErr {
fn from(err: PyInvokeError) -> Self {
pyo3::exceptions::PyRuntimeError::new_err(err.0.to_string())
}
}
impl From<slint_interpreter::InvokeError> for PyInvokeError {
fn from(err: slint_interpreter::InvokeError) -> Self {
Self(err)
}
}
struct PySetCallbackError(slint_interpreter::SetCallbackError);
impl From<PySetCallbackError> for PyErr {
fn from(err: PySetCallbackError) -> Self {
pyo3::exceptions::PyRuntimeError::new_err(err.0.to_string())
}
}
impl From<slint_interpreter::SetCallbackError> for PySetCallbackError {
fn from(err: slint_interpreter::SetCallbackError) -> Self {
Self(err)
}
}

View file

@ -1,9 +1,16 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
mod interpreter;
use interpreter::{ComponentCompiler, PyDiagnostic, PyDiagnosticLevel, PyValueType};
use pyo3::prelude::*;
#[pymodule]
fn slint(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<ComponentCompiler>()?;
m.add_class::<PyValueType>()?;
m.add_class::<PyDiagnosticLevel>()?;
m.add_class::<PyDiagnostic>()?;
Ok(())
}

10
api/python/noxfile.py Normal file
View file

@ -0,0 +1,10 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import nox
@nox.session
def python(session: nox.Session):
session.env["MATURIN_PEP517_ARGS"] = "--profile=dev"
session.install(".[dev]")
session.run("pytest")

19
api/python/pyproject.toml Normal file
View file

@ -0,0 +1,19 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
[build-system]
requires = ["maturin>=1,<2"]
build-backend = "maturin"
[project]
name = "pyslint"
version = "1.4.0"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Rust",
]
[project.optional-dependencies]
dev = ["pytest"]

View file

@ -0,0 +1,66 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import pytest
import slint
from slint import ValueType;
def test_basic_compiler():
compiler = slint.ComponentCompiler()
assert compiler.include_paths == []
compiler.include_paths = ["testing"]
assert compiler.include_paths == ["testing"]
assert compiler.build_from_source("Garbage", "") == None
compdef = compiler.build_from_source("""
export global TestGlobal {
in property <string> theglobalprop;
callback globallogic();
}
export component Test {
in property <string> strprop;
in property <int> intprop;
in property <float> floatprop;
in property <bool> boolprop;
in property <image> imgprop;
in property <brush> brushprop;
in property <color> colprop;
in property <[string]> modelprop;
callback test-callback();
}
""", "")
assert compdef != None
assert compdef.name == "Test"
props = [(name, type) for name, type in compdef.properties.items()]
assert props == [('boolprop', ValueType.Bool), ('brushprop', ValueType.Brush), ('colprop', ValueType.Brush), ('floatprop', ValueType.Number), ('imgprop', ValueType.Image), ('intprop', ValueType.Number), ('modelprop', ValueType.Model), ('strprop', ValueType.String)]
assert compdef.callbacks == ["test-callback"]
assert compdef.globals == ["TestGlobal"]
assert compdef.global_properties("Garbage") == None
assert [(name, type) for name, type in compdef.global_properties("TestGlobal").items()] == [('theglobalprop', ValueType.String)]
assert compdef.global_callbacks("Garbage") == None
assert compdef.global_callbacks("TestGlobal") == ["globallogic"]
instance = compdef.create()
assert instance != None
def test_compiler_build_from_path():
compiler = slint.ComponentCompiler()
assert len(compiler.diagnostics) == 0
assert compiler.build_from_path("Nonexistent.slint") == None
diags = compiler.diagnostics
assert len(diags) == 1
assert diags[0].level == slint.DiagnosticLevel.Error
assert diags[0].message.startswith("Could not load Nonexistent.slint:")

View file

@ -0,0 +1,108 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
import pytest
import slint
from slint import ValueType;
def test_property_access():
compiler = slint.ComponentCompiler()
compdef = compiler.build_from_source("""
export global TestGlobal {
in property <string> theglobalprop: "Hey";
callback globallogic();
}
export component Test {
in property <string> strprop: "Hello";
in property <int> intprop: 42;
in property <float> floatprop: 100;
in property <bool> boolprop: true;
in property <image> imgprop;
in property <brush> brushprop;
in property <color> colprop;
in property <[string]> modelprop;
callback test-callback();
}
""", "")
assert compdef != None
instance = compdef.create()
assert instance != None
with pytest.raises(ValueError, match="no such property"):
instance.set_property("nonexistent", 42)
assert instance.get_property("strprop") == "Hello"
instance.set_property("strprop", "World")
assert instance.get_property("strprop") == "World"
with pytest.raises(ValueError, match="wrong type"):
instance.set_property("strprop", 42)
assert instance.get_property("intprop") == 42
instance.set_property("intprop", 100)
assert instance.get_property("intprop") == 100
with pytest.raises(ValueError, match="wrong type"):
instance.set_property("intprop", False)
assert instance.get_property("floatprop") == 100
instance.set_property("floatprop", 42)
assert instance.get_property("floatprop") == 42
with pytest.raises(ValueError, match="wrong type"):
instance.set_property("floatprop", "Blah")
assert instance.get_property("boolprop") == True
instance.set_property("boolprop", False)
assert instance.get_property("boolprop") == False
with pytest.raises(ValueError, match="wrong type"):
instance.set_property("boolprop", 0)
with pytest.raises(ValueError, match="no such property"):
instance.set_global_property("nonexistent", "theglobalprop", 42)
with pytest.raises(ValueError, match="no such property"):
instance.set_global_property("TestGlobal", "nonexistent", 42)
assert instance.get_global_property("TestGlobal", "theglobalprop") == "Hey"
instance.set_global_property("TestGlobal", "theglobalprop", "Ok")
assert instance.get_global_property("TestGlobal", "theglobalprop") == "Ok"
def test_callbacks():
compiler = slint.ComponentCompiler()
compdef = compiler.build_from_source("""
export global TestGlobal {
callback globallogic(string) -> string;
globallogic(value) => {
return "global " + value;
}
}
export component Test {
callback test-callback(string) -> string;
test-callback(value) => {
return "local " + value;
}
}
""", "")
assert compdef != None
instance = compdef.create()
assert instance != None
assert instance.invoke("test-callback", "foo") == "local foo"
assert instance.invoke_global("TestGlobal", "globallogic", "foo") == "global foo"
with pytest.raises(RuntimeError, match="no such callback"):
instance.set_callback("non-existent", lambda x: x)
instance.set_callback("test-callback", lambda x: "python " + x)
assert instance.invoke("test-callback", "foo") == "python foo"
with pytest.raises(RuntimeError, match="no such callback"):
instance.set_global_callback("TestGlobal", "non-existent", lambda x: x)
instance.set_global_callback("TestGlobal", "globallogic", lambda x: "python global " + x)
assert instance.invoke_global("TestGlobal", "globallogic", "foo") == "python global foo"