Skip to content

Commit f9e58ff

Browse files
committed
python: add .slint code generator and typing metadata
- introduce slint.codegen to emit runtime modules and stubs from .slint inputs via CLI or API - expose property/callback metadata in the bindings, extend callback decorator overloads, and add libcst-backed emitters for typed modules - document the workflow, add a counter example plus tests, and keep the legacy import-loader path for now
1 parent 9444b47 commit f9e58ff

24 files changed

+1625
-14
lines changed

api/python/slint/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ spin_on = { workspace = true }
5353
css-color-parser2 = { workspace = true }
5454
pyo3-stub-gen = { version = "0.9.0", default-features = false }
5555
smol = { version = "2.0.0" }
56+
smol_str = { workspace = true }
5657

5758
[package.metadata.maturin]
5859
python-source = "slint"

api/python/slint/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,33 @@ app.run()
7575

7676
5. Run it with `uv run main.py`
7777

78+
## Code Generation CLI
79+
80+
Use the bundled code generator to derive Python bindings and type stubs from your `.slint` files. The generator lives in the `slint.codegen` module and exposes a CLI entry point.
81+
82+
Run it with `uv`:
83+
84+
```bash
85+
uv run -m slint.codegen generate --input path/to/app.slint --output generated
86+
```
87+
88+
When `--output` is omitted the generated files are written next to each input source.
89+
90+
You can also call the generator from Python:
91+
92+
```python
93+
from pathlib import Path
94+
from slint.codegen.generator import generate_project
95+
from slint.codegen.models import GenerationConfig
96+
97+
slint_file = Path("ui/app.slint")
98+
generate_project(
99+
inputs=[slint_file],
100+
output_dir=Path("generated"),
101+
config=GenerationConfig(include_paths=[slint_file.parent], library_paths={}, style=None, translation_domain=None, quiet=True),
102+
)
103+
```
104+
78105
## API Overview
79106

80107
### Instantiating a Component

api/python/slint/interpreter.rs

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use pyo3::gc::PyVisit;
1818
use pyo3::prelude::*;
1919
use pyo3::types::PyTuple;
2020
use pyo3::PyTraverseError;
21+
use smol_str::SmolStr;
2122

2223
use crate::errors::{
2324
PyGetPropertyError, PyInvokeError, PyPlatformError, PySetCallbackError, PySetPropertyError,
@@ -255,6 +256,39 @@ impl ComponentDefinition {
255256
self.definition.globals().collect()
256257
}
257258

259+
fn property_infos(&self) -> Vec<PyPropertyInfo> {
260+
self.definition
261+
.properties_and_callbacks()
262+
.filter_map(|(name, (ty, _))| {
263+
if ty.is_property_type() {
264+
Some(PyPropertyInfo::new(name, &ty))
265+
} else {
266+
None
267+
}
268+
})
269+
.collect()
270+
}
271+
272+
fn callback_infos(&self) -> Vec<PyCallbackInfo> {
273+
self.definition
274+
.properties_and_callbacks()
275+
.filter_map(|(name, (ty, _))| match ty {
276+
Type::Callback(function) => Some(PyCallbackInfo::new(name, &function)),
277+
_ => None,
278+
})
279+
.collect()
280+
}
281+
282+
fn function_infos(&self) -> Vec<PyFunctionInfo> {
283+
self.definition
284+
.properties_and_callbacks()
285+
.filter_map(|(name, (ty, _))| match ty {
286+
Type::Function(function) => Some(PyFunctionInfo::new(name, &function)),
287+
_ => None,
288+
})
289+
.collect()
290+
}
291+
258292
fn global_properties(&self, name: &str) -> Option<IndexMap<String, PyValueType>> {
259293
self.definition.global_properties_and_callbacks(name).map(|propiter| {
260294
propiter
@@ -271,6 +305,39 @@ impl ComponentDefinition {
271305
self.definition.global_functions(name).map(|functioniter| functioniter.collect())
272306
}
273307

308+
fn global_property_infos(&self, global_name: &str) -> Option<Vec<PyPropertyInfo>> {
309+
self.definition.global_properties_and_callbacks(global_name).map(|iter| {
310+
iter.filter_map(|(name, (ty, _))| {
311+
if ty.is_property_type() {
312+
Some(PyPropertyInfo::new(name, &ty))
313+
} else {
314+
None
315+
}
316+
})
317+
.collect()
318+
})
319+
}
320+
321+
fn global_callback_infos(&self, global_name: &str) -> Option<Vec<PyCallbackInfo>> {
322+
self.definition.global_properties_and_callbacks(global_name).map(|iter| {
323+
iter.filter_map(|(name, (ty, _))| match ty {
324+
Type::Callback(function) => Some(PyCallbackInfo::new(name, &function)),
325+
_ => None,
326+
})
327+
.collect()
328+
})
329+
}
330+
331+
fn global_function_infos(&self, global_name: &str) -> Option<Vec<PyFunctionInfo>> {
332+
self.definition.global_properties_and_callbacks(global_name).map(|iter| {
333+
iter.filter_map(|(name, (ty, _))| match ty {
334+
Type::Function(function) => Some(PyFunctionInfo::new(name, &function)),
335+
_ => None,
336+
})
337+
.collect()
338+
})
339+
}
340+
274341
fn callback_returns_void(&self, callback_name: &str) -> bool {
275342
let callback_name = normalize_identifier(callback_name);
276343
self.definition
@@ -357,6 +424,183 @@ impl From<i_slint_compiler::langtype::Type> for PyValueType {
357424
}
358425
}
359426

427+
fn python_identifier(name: &str) -> String {
428+
if name.is_empty() {
429+
return String::new();
430+
}
431+
let mut result = name.replace('-', "_");
432+
if result.chars().next().is_some_and(|c| c.is_ascii_digit()) {
433+
result.insert(0, '_');
434+
}
435+
result
436+
}
437+
438+
fn type_to_python_hint(ty: &i_slint_compiler::langtype::Type) -> String {
439+
use i_slint_compiler::langtype::Type::*;
440+
441+
match ty {
442+
Void => "None".into(),
443+
Bool => "bool".into(),
444+
Int32 => "int".into(),
445+
Float32 | Duration | PhysicalLength | LogicalLength | Rem | Angle | Percent
446+
| UnitProduct(_) => "float".into(),
447+
String => "str".into(),
448+
Brush | Color => "slint.Brush".into(),
449+
Image => "slint.Image".into(),
450+
Model => "slint.Model".into(),
451+
Array(inner) => format!("slint.ListModel[{}]", type_to_python_hint(inner)),
452+
Struct(struct_ty) => struct_to_python_hint(struct_ty),
453+
// Enumeration(enum_ty) => {
454+
// let name = enum_ty.name.as_str();
455+
// let tail = name.rsplit("::").next().unwrap_or(name);
456+
// format!("slint.{}", python_identifier(tail))
457+
// }
458+
Callback(function) | Function(function) => function_to_python_hint(function),
459+
// ComponentFactory => "slint.ComponentFactory".into(), // TODO
460+
// PathData | Easing | ElementReference | LayoutCache | InferredProperty
461+
// | InferredCallback | Invalid => "Any".into(),
462+
_ => "Any".into(),
463+
}
464+
}
465+
466+
fn struct_to_python_hint(struct_ty: &Rc<i_slint_compiler::langtype::Struct>) -> String {
467+
if let Some(inner_ty) = optional_struct_inner(struct_ty) {
468+
return format!("Optional[{}]", type_to_python_hint(inner_ty));
469+
}
470+
471+
if let Some(name) = &struct_ty.name {
472+
let full = name.as_str();
473+
let tail = full.rsplit("::").next().unwrap_or(full);
474+
if full.starts_with("slint::") {
475+
return format!("slint.{}", python_identifier(tail));
476+
}
477+
return python_identifier(tail);
478+
}
479+
480+
"dict[str, Any]".into()
481+
}
482+
483+
fn optional_struct_inner(
484+
struct_ty: &Rc<i_slint_compiler::langtype::Struct>,
485+
) -> Option<&i_slint_compiler::langtype::Type> {
486+
let name = struct_ty.name.as_ref()?;
487+
let tail = name.as_str().rsplit("::").next().unwrap_or(name.as_str());
488+
let tail_lower = tail.to_ascii_lowercase();
489+
if !tail_lower.starts_with("optional") {
490+
return None;
491+
}
492+
493+
if let Some(value_ty) =
494+
struct_ty.fields.get("value").or_else(|| struct_ty.fields.get("maybe_value"))
495+
{
496+
return Some(value_ty);
497+
}
498+
499+
struct_ty.fields.values().next()
500+
}
501+
502+
fn function_to_python_hint(function: &Rc<i_slint_compiler::langtype::Function>) -> String {
503+
let args: Vec<String> = function.args.iter().map(type_to_python_hint).collect();
504+
let return_type = type_to_python_hint(&function.return_type);
505+
506+
if args.is_empty() {
507+
if function.return_type == i_slint_compiler::langtype::Type::Void {
508+
"Callable[..., Any]".into()
509+
} else {
510+
format!("Callable[[], {}]", return_type)
511+
}
512+
} else {
513+
format!("Callable[[{}], {}]", args.join(", "), return_type)
514+
}
515+
}
516+
517+
#[gen_stub_pyclass]
518+
#[pyclass(module = "slint")]
519+
#[derive(Clone)]
520+
pub struct PyPropertyInfo {
521+
#[pyo3(get)]
522+
pub name: String,
523+
#[pyo3(get)]
524+
pub python_type: String,
525+
}
526+
527+
impl PyPropertyInfo {
528+
fn new(name: String, ty: &i_slint_compiler::langtype::Type) -> Self {
529+
Self { name, python_type: type_to_python_hint(ty) }
530+
}
531+
}
532+
533+
#[gen_stub_pyclass]
534+
#[pyclass(module = "slint")]
535+
#[derive(Clone)]
536+
pub struct PyCallbackParameter {
537+
#[pyo3(get)]
538+
pub name: Option<String>,
539+
#[pyo3(get)]
540+
pub python_type: String,
541+
}
542+
543+
impl PyCallbackParameter {
544+
fn new(name: Option<SmolStr>, ty: &i_slint_compiler::langtype::Type) -> Self {
545+
let name = name.and_then(|n| if n.is_empty() { None } else { Some(n.into()) });
546+
Self { name, python_type: type_to_python_hint(ty) }
547+
}
548+
}
549+
550+
#[gen_stub_pyclass]
551+
#[pyclass(module = "slint")]
552+
#[derive(Clone)]
553+
pub struct PyCallbackInfo {
554+
#[pyo3(get)]
555+
pub name: String,
556+
#[pyo3(get)]
557+
pub parameters: Vec<PyCallbackParameter>,
558+
#[pyo3(get)]
559+
pub return_type: String,
560+
}
561+
562+
impl PyCallbackInfo {
563+
fn new(name: String, function: &Rc<i_slint_compiler::langtype::Function>) -> Self {
564+
let mut parameters = Vec::with_capacity(function.args.len());
565+
for (idx, arg_ty) in function.args.iter().enumerate() {
566+
let arg_name = function.arg_names.get(idx).cloned();
567+
parameters.push(PyCallbackParameter::new(arg_name, arg_ty));
568+
}
569+
Self {
570+
name,
571+
parameters,
572+
return_type: type_to_python_hint(&function.return_type),
573+
}
574+
}
575+
}
576+
577+
#[gen_stub_pyclass]
578+
#[pyclass(module = "slint")]
579+
#[derive(Clone)]
580+
pub struct PyFunctionInfo {
581+
#[pyo3(get)]
582+
pub name: String,
583+
#[pyo3(get)]
584+
pub parameters: Vec<PyCallbackParameter>,
585+
#[pyo3(get)]
586+
pub return_type: String,
587+
}
588+
589+
impl PyFunctionInfo {
590+
fn new(name: String, function: &Rc<i_slint_compiler::langtype::Function>) -> Self {
591+
let mut parameters = Vec::with_capacity(function.args.len());
592+
for (idx, arg_ty) in function.args.iter().enumerate() {
593+
let arg_name = function.arg_names.get(idx).cloned();
594+
parameters.push(PyCallbackParameter::new(arg_name, arg_ty));
595+
}
596+
Self {
597+
name,
598+
parameters,
599+
return_type: type_to_python_hint(&function.return_type),
600+
}
601+
}
602+
}
603+
360604
#[gen_stub_pyclass]
361605
#[pyclass(unsendable, weakref)]
362606
pub struct ComponentInstance {

api/python/slint/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ fn slint(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
180180
m.add_class::<brush::PyBrush>()?;
181181
m.add_class::<models::PyModelBase>()?;
182182
m.add_class::<value::PyStruct>()?;
183+
m.add_class::<interpreter::PyPropertyInfo>()?;
184+
m.add_class::<interpreter::PyCallbackParameter>()?;
185+
m.add_class::<interpreter::PyCallbackInfo>()?;
186+
m.add_class::<interpreter::PyFunctionInfo>()?;
183187
m.add_class::<async_adapter::AsyncAdapter>()?;
184188
m.add_function(wrap_pyfunction!(run_event_loop, m)?)?;
185189
m.add_function(wrap_pyfunction!(quit_event_loop, m)?)?;

api/python/slint/pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ classifiers = [
2424
"Topic :: Software Development :: Libraries :: Application Frameworks",
2525
"Topic :: Software Development :: User Interfaces",
2626
"Topic :: Software Development :: Widget Sets",
27-
"Programming Language :: Python",
2827
"Programming Language :: Python :: 3",
2928
"Programming Language :: Python :: 3.10",
3029
]
30+
dependencies = [
31+
"libcst>=1.8.5",
32+
]
3133

3234
[project.urls]
3335
Homepage = "https://slint.dev"

0 commit comments

Comments
 (0)