Python: Add support for exporting multiple components

This commit is contained in:
Simon Hausmann 2024-07-02 09:23:34 +02:00 committed by Simon Hausmann
parent a3435d218f
commit 0c7d2062a5
10 changed files with 76 additions and 51 deletions

View file

@ -112,7 +112,7 @@ The exported component is exposed as a Python class. To access this class, you h
options: options:
1. Call `slint.load_file("app.slint")`. The returned object is a [namespace](https://docs.python.org/3/library/types.html#types.SimpleNamespace), 1. Call `slint.load_file("app.slint")`. The returned object is a [namespace](https://docs.python.org/3/library/types.html#types.SimpleNamespace),
that provides the `MainWindow` class: that provides the `MainWindow` class as well as any other explicitly exported component that inherits `Window`:
```python ```python
import slint import slint
components = slint.load_file("app.slint") components = slint.load_file("app.slint")
@ -128,7 +128,8 @@ options:
Any attribute lookup in `slint.loader` is searched for in `sys.path`. If a directory with the name exists, it is returned as a loader object, and subsequent Any attribute lookup in `slint.loader` is searched for in `sys.path`. If a directory with the name exists, it is returned as a loader object, and subsequent
attribute lookups follow the same logic. If the name matches a file with the `.slint` extension, it is automatically loaded with `load_file` and the attribute lookups follow the same logic. If the name matches a file with the `.slint` extension, it is automatically loaded with `load_file` and the
[namespace](https://docs.python.org/3/library/types.html#types.SimpleNamespace) is returned. [namespace](https://docs.python.org/3/library/types.html#types.SimpleNamespace) is returned, which contains classes for each exported component that
inherits `Window`.
### Accessing Properties ### Accessing Properties

View file

@ -20,12 +20,12 @@ use crate::errors::{
use crate::value::PyValue; use crate::value::PyValue;
#[pyclass(unsendable)] #[pyclass(unsendable)]
pub struct ComponentCompiler { pub struct Compiler {
compiler: slint_interpreter::ComponentCompiler, compiler: slint_interpreter::Compiler,
} }
#[pymethods] #[pymethods]
impl ComponentCompiler { impl Compiler {
#[new] #[new]
fn py_new() -> PyResult<Self> { fn py_new() -> PyResult<Self> {
Ok(Self { compiler: Default::default() }) Ok(Self { compiler: Default::default() })
@ -61,28 +61,19 @@ impl ComponentCompiler {
self.compiler.set_library_paths(libraries) self.compiler.set_library_paths(libraries)
} }
#[getter]
fn get_diagnostics(&self) -> Vec<PyDiagnostic> {
self.compiler.diagnostics().iter().map(|diag| PyDiagnostic(diag.clone())).collect()
}
#[setter] #[setter]
fn set_translation_domain(&mut self, domain: String) { fn set_translation_domain(&mut self, domain: String) {
self.compiler.set_translation_domain(domain) self.compiler.set_translation_domain(domain)
} }
fn build_from_path(&mut self, path: PathBuf) -> Option<ComponentDefinition> { fn build_from_path(&mut self, path: PathBuf) -> CompilationResult {
spin_on::spin_on(self.compiler.build_from_path(path)) CompilationResult { result: spin_on::spin_on(self.compiler.build_from_path(path)) }
.map(|definition| ComponentDefinition { definition })
} }
fn build_from_source( fn build_from_source(&mut self, source_code: String, path: PathBuf) -> CompilationResult {
&mut self, CompilationResult {
source_code: String, result: spin_on::spin_on(self.compiler.build_from_source(source_code, path)),
path: PathBuf, }
) -> Option<ComponentDefinition> {
spin_on::spin_on(self.compiler.build_from_source(source_code, path))
.map(|definition| ComponentDefinition { definition })
} }
} }
@ -132,6 +123,28 @@ pub enum PyDiagnosticLevel {
Warning, Warning,
} }
#[pyclass(unsendable)]
pub struct CompilationResult {
result: slint_interpreter::CompilationResult,
}
#[pymethods]
impl CompilationResult {
#[getter]
fn component_names(&self) -> Vec<String> {
self.result.component_names().map(ToString::to_string).collect()
}
fn component(&self, name: &str) -> Option<ComponentDefinition> {
self.result.component(name).map(|definition| ComponentDefinition { definition })
}
#[getter]
fn get_diagnostics(&self) -> Vec<PyDiagnostic> {
self.result.diagnostics().map(|diag| PyDiagnostic(diag.clone())).collect()
}
}
#[pyclass(unsendable)] #[pyclass(unsendable)]
struct ComponentDefinition { struct ComponentDefinition {
definition: slint_interpreter::ComponentDefinition, definition: slint_interpreter::ComponentDefinition,

View file

@ -3,7 +3,7 @@
mod image; mod image;
mod interpreter; mod interpreter;
use interpreter::{ComponentCompiler, PyDiagnostic, PyDiagnosticLevel, PyValueType}; use interpreter::{CompilationResult, Compiler, PyDiagnostic, PyDiagnosticLevel, PyValueType};
mod brush; mod brush;
mod errors; mod errors;
mod models; mod models;
@ -30,7 +30,8 @@ fn slint(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
}) })
.map_err(|e| errors::PyPlatformError(e))?; .map_err(|e| errors::PyPlatformError(e))?;
m.add_class::<ComponentCompiler>()?; m.add_class::<Compiler>()?;
m.add_class::<CompilationResult>()?;
m.add_class::<image::PyImage>()?; m.add_class::<image::PyImage>()?;
m.add_class::<PyValueType>()?; m.add_class::<PyValueType>()?;
m.add_class::<PyDiagnosticLevel>()?; m.add_class::<PyDiagnosticLevel>()?;

View file

@ -188,7 +188,7 @@ def _build_class(compdef):
def load_file(path, quiet=False, style=None, include_paths=None, library_paths=None, translation_domain=None): def load_file(path, quiet=False, style=None, include_paths=None, library_paths=None, translation_domain=None):
compiler = native.ComponentCompiler() compiler = native.Compiler()
if style is not None: if style is not None:
compiler.style = style compiler.style = style
@ -199,9 +199,9 @@ def load_file(path, quiet=False, style=None, include_paths=None, library_paths=N
if translation_domain is not None: if translation_domain is not None:
compiler.translation_domain = translation_domain compiler.translation_domain = translation_domain
compdef = compiler.build_from_path(path) result = compiler.build_from_path(path)
diagnostics = compiler.diagnostics diagnostics = result.diagnostics
if diagnostics: if diagnostics:
if not quiet: if not quiet:
for diag in diagnostics: for diag in diagnostics:
@ -213,10 +213,11 @@ def load_file(path, quiet=False, style=None, include_paths=None, library_paths=N
if errors: if errors:
raise CompileError(f"Could not compile {path}", diagnostics) raise CompileError(f"Could not compile {path}", diagnostics)
wrapper_class = _build_class(compdef)
module = types.SimpleNamespace() module = types.SimpleNamespace()
setattr(module, compdef.name, wrapper_class) for comp_name in result.component_names:
wrapper_class = _build_class(result.component(comp_name))
setattr(module, comp_name, wrapper_class)
return module return module

View file

@ -7,15 +7,15 @@ from slint.slint import ValueType
def test_basic_compiler(): def test_basic_compiler():
compiler = native.ComponentCompiler() compiler = native.Compiler()
assert compiler.include_paths == [] assert compiler.include_paths == []
compiler.include_paths = ["testing"] compiler.include_paths = ["testing"]
assert compiler.include_paths == ["testing"] assert compiler.include_paths == ["testing"]
assert compiler.build_from_source("Garbage", "") == None assert len(compiler.build_from_source("Garbage", "").component_names) == 0
compdef = compiler.build_from_source(""" result = compiler.build_from_source("""
export global TestGlobal { export global TestGlobal {
in property <string> theglobalprop; in property <string> theglobalprop;
callback globallogic(); callback globallogic();
@ -36,6 +36,9 @@ def test_basic_compiler():
public function ff() {} public function ff() {}
} }
""", "") """, "")
assert result.component_names == ["Test"]
compdef = result.component("Test")
assert compdef != None assert compdef != None
assert compdef.name == "Test" assert compdef.name == "Test"
@ -64,12 +67,12 @@ def test_basic_compiler():
def test_compiler_build_from_path(): def test_compiler_build_from_path():
compiler = native.ComponentCompiler() compiler = native.Compiler()
assert len(compiler.diagnostics) == 0 result = compiler.build_from_path("Nonexistent.slint")
assert len(result.component_names) == 0
assert compiler.build_from_path("Nonexistent.slint") == None diags = result.diagnostics
diags = compiler.diagnostics
assert len(diags) == 1 assert len(diags) == 1
assert diags[0].level == native.DiagnosticLevel.Error assert diags[0].level == native.DiagnosticLevel.Error

View file

@ -7,14 +7,14 @@ import gc
def test_callback_gc(): def test_callback_gc():
compiler = native.ComponentCompiler() compiler = native.Compiler()
compdef = compiler.build_from_source(""" compdef = compiler.build_from_source("""
export component Test { export component Test {
out property <string> test-value: "Ok"; out property <string> test-value: "Ok";
callback test-callback(string) -> string; callback test-callback(string) -> string;
} }
""", "") """, "").component("Test")
assert compdef != None assert compdef != None
instance = compdef.create() instance = compdef.create()

View file

@ -11,7 +11,7 @@ Brush = native.PyBrush
def test_property_access(): def test_property_access():
compiler = native.ComponentCompiler() compiler = native.Compiler()
compdef = compiler.build_from_source(""" compdef = compiler.build_from_source("""
export global TestGlobal { export global TestGlobal {
@ -41,7 +41,7 @@ def test_property_access():
callback test-callback(); callback test-callback();
} }
""", os.path.join(os.path.dirname(__file__), "main.slint")) """, os.path.join(os.path.dirname(__file__), "main.slint")).component("Test")
assert compdef != None assert compdef != None
instance = compdef.create() instance = compdef.create()
@ -121,7 +121,7 @@ def test_property_access():
def test_callbacks(): def test_callbacks():
compiler = native.ComponentCompiler() compiler = native.Compiler()
compdef = compiler.build_from_source(""" compdef = compiler.build_from_source("""
export global TestGlobal { export global TestGlobal {
@ -138,7 +138,7 @@ def test_callbacks():
} }
callback void-callback(); callback void-callback();
} }
""", "") """, "").component("Test")
assert compdef != None assert compdef != None
instance = compdef.create() instance = compdef.create()

View file

@ -12,9 +12,13 @@ def test_load_file(caplog):
assert "The property 'color' has been deprecated. Please use 'background' instead" in caplog.text assert "The property 'color' has been deprecated. Please use 'background' instead" in caplog.text
assert list(module.__dict__.keys()) == ["App"] assert len(list(module.__dict__.keys())) == 2
assert "App" in module.__dict__
assert "Diag" in module.__dict__
instance = module.App() instance = module.App()
del instance del instance
instance = module.Diag()
del instance
def test_load_file_fail(): def test_load_file_fail():

View file

@ -9,7 +9,7 @@ export global MyGlobal {
} }
} }
export component App { export component App inherits Window {
in-out property <string> hello: "World"; in-out property <string> hello: "World";
callback say-hello(string) -> string; callback say-hello(string) -> string;
callback say_hello_again(string) -> string; callback say_hello_again(string) -> string;
@ -37,3 +37,5 @@ export component App {
color: red; color: red;
} }
} }
export component Diag inherits Window { }

View file

@ -6,7 +6,7 @@ from slint import models as models
def test_model_notify(): def test_model_notify():
compiler = native.ComponentCompiler() compiler = native.Compiler()
compdef = compiler.build_from_source(""" compdef = compiler.build_from_source("""
export component App { export component App {
@ -28,7 +28,7 @@ def test_model_notify():
} }
} }
""", "") """, "").component("App")
assert compdef != None assert compdef != None
instance = compdef.create() instance = compdef.create()
@ -52,13 +52,13 @@ def test_model_notify():
def test_model_from_list(): def test_model_from_list():
compiler = native.ComponentCompiler() compiler = native.Compiler()
compdef = compiler.build_from_source(""" compdef = compiler.build_from_source("""
export component App { export component App {
in-out property<[int]> data: [1, 2, 3, 4]; in-out property<[int]> data: [1, 2, 3, 4];
} }
""", "") """, "").component("App")
assert compdef != None assert compdef != None
instance = compdef.create() instance = compdef.create()
@ -97,13 +97,13 @@ def test_python_model_iterable():
def test_rust_model_sequence(): def test_rust_model_sequence():
compiler = native.ComponentCompiler() compiler = native.Compiler()
compdef = compiler.build_from_source(""" compdef = compiler.build_from_source("""
export component App { export component App {
in-out property<[int]> data: [1, 2, 3, 4, 5]; in-out property<[int]> data: [1, 2, 3, 4, 5];
} }
""", "") """, "").component("App")
assert compdef != None assert compdef != None
instance = compdef.create() instance = compdef.create()
@ -117,7 +117,7 @@ def test_rust_model_sequence():
def test_model_writeback(): def test_model_writeback():
compiler = native.ComponentCompiler() compiler = native.Compiler()
compdef = compiler.build_from_source(""" compdef = compiler.build_from_source("""
export component App { export component App {
@ -131,7 +131,7 @@ def test_model_writeback():
} }
} }
""", "") """, "").component("App")
assert compdef != None assert compdef != None
instance = compdef.create() instance = compdef.create()