From f9e58ff3a90bd194818c91bb5d8e10f4f6faa368 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Fri, 24 Oct 2025 01:01:57 +0800 Subject: [PATCH 01/52] 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 --- api/python/slint/Cargo.toml | 1 + api/python/slint/README.md | 27 ++ api/python/slint/interpreter.rs | 244 ++++++++++++ api/python/slint/lib.rs | 4 + api/python/slint/pyproject.toml | 4 +- api/python/slint/slint/__init__.py | 41 +- api/python/slint/slint/codegen/__init__.py | 5 + api/python/slint/slint/codegen/__main__.py | 9 + api/python/slint/slint/codegen/cli.py | 140 +++++++ api/python/slint/slint/codegen/emitters.py | 356 ++++++++++++++++++ api/python/slint/slint/codegen/generator.py | 288 ++++++++++++++ api/python/slint/slint/codegen/models.py | 98 +++++ api/python/slint/slint/codegen/utils.py | 17 + api/python/slint/slint/slint.pyi | 24 ++ .../slint/tests/codegen/examples/__init__.py | 0 .../tests/codegen/examples/counter/.gitignore | 1 + .../tests/codegen/examples/counter/README.md | 32 ++ .../codegen/examples/counter/__init__.py | 0 .../codegen/examples/counter/counter.slint | 26 ++ .../codegen/examples/counter/generate.py | 25 ++ .../tests/codegen/examples/counter/main.py | 27 ++ .../tests/codegen/examples/struct_test.slint | 7 + .../slint/tests/codegen/test_generator.py | 248 ++++++++++++ api/python/slint/tests/codegen/test_utils.py | 15 + 24 files changed, 1625 insertions(+), 14 deletions(-) create mode 100644 api/python/slint/slint/codegen/__init__.py create mode 100644 api/python/slint/slint/codegen/__main__.py create mode 100644 api/python/slint/slint/codegen/cli.py create mode 100644 api/python/slint/slint/codegen/emitters.py create mode 100644 api/python/slint/slint/codegen/generator.py create mode 100644 api/python/slint/slint/codegen/models.py create mode 100644 api/python/slint/slint/codegen/utils.py create mode 100644 api/python/slint/tests/codegen/examples/__init__.py create mode 100644 api/python/slint/tests/codegen/examples/counter/.gitignore create mode 100644 api/python/slint/tests/codegen/examples/counter/README.md create mode 100644 api/python/slint/tests/codegen/examples/counter/__init__.py create mode 100644 api/python/slint/tests/codegen/examples/counter/counter.slint create mode 100644 api/python/slint/tests/codegen/examples/counter/generate.py create mode 100644 api/python/slint/tests/codegen/examples/counter/main.py create mode 100644 api/python/slint/tests/codegen/examples/struct_test.slint create mode 100644 api/python/slint/tests/codegen/test_generator.py create mode 100644 api/python/slint/tests/codegen/test_utils.py diff --git a/api/python/slint/Cargo.toml b/api/python/slint/Cargo.toml index 7a2d3117cd6..5aebefa7f7f 100644 --- a/api/python/slint/Cargo.toml +++ b/api/python/slint/Cargo.toml @@ -53,6 +53,7 @@ spin_on = { workspace = true } css-color-parser2 = { workspace = true } pyo3-stub-gen = { version = "0.9.0", default-features = false } smol = { version = "2.0.0" } +smol_str = { workspace = true } [package.metadata.maturin] python-source = "slint" diff --git a/api/python/slint/README.md b/api/python/slint/README.md index db725e9efc3..4a45cc38b04 100644 --- a/api/python/slint/README.md +++ b/api/python/slint/README.md @@ -75,6 +75,33 @@ app.run() 5. Run it with `uv run main.py` +## Code Generation CLI + +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. + +Run it with `uv`: + +```bash +uv run -m slint.codegen generate --input path/to/app.slint --output generated +``` + +When `--output` is omitted the generated files are written next to each input source. + +You can also call the generator from Python: + +```python +from pathlib import Path +from slint.codegen.generator import generate_project +from slint.codegen.models import GenerationConfig + +slint_file = Path("ui/app.slint") +generate_project( + inputs=[slint_file], + output_dir=Path("generated"), + config=GenerationConfig(include_paths=[slint_file.parent], library_paths={}, style=None, translation_domain=None, quiet=True), +) +``` + ## API Overview ### Instantiating a Component diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index 04a2dccb228..b9d207c1a6a 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -18,6 +18,7 @@ use pyo3::gc::PyVisit; use pyo3::prelude::*; use pyo3::types::PyTuple; use pyo3::PyTraverseError; +use smol_str::SmolStr; use crate::errors::{ PyGetPropertyError, PyInvokeError, PyPlatformError, PySetCallbackError, PySetPropertyError, @@ -255,6 +256,39 @@ impl ComponentDefinition { self.definition.globals().collect() } + fn property_infos(&self) -> Vec { + self.definition + .properties_and_callbacks() + .filter_map(|(name, (ty, _))| { + if ty.is_property_type() { + Some(PyPropertyInfo::new(name, &ty)) + } else { + None + } + }) + .collect() + } + + fn callback_infos(&self) -> Vec { + self.definition + .properties_and_callbacks() + .filter_map(|(name, (ty, _))| match ty { + Type::Callback(function) => Some(PyCallbackInfo::new(name, &function)), + _ => None, + }) + .collect() + } + + fn function_infos(&self) -> Vec { + self.definition + .properties_and_callbacks() + .filter_map(|(name, (ty, _))| match ty { + Type::Function(function) => Some(PyFunctionInfo::new(name, &function)), + _ => None, + }) + .collect() + } + fn global_properties(&self, name: &str) -> Option> { self.definition.global_properties_and_callbacks(name).map(|propiter| { propiter @@ -271,6 +305,39 @@ impl ComponentDefinition { self.definition.global_functions(name).map(|functioniter| functioniter.collect()) } + fn global_property_infos(&self, global_name: &str) -> Option> { + self.definition.global_properties_and_callbacks(global_name).map(|iter| { + iter.filter_map(|(name, (ty, _))| { + if ty.is_property_type() { + Some(PyPropertyInfo::new(name, &ty)) + } else { + None + } + }) + .collect() + }) + } + + fn global_callback_infos(&self, global_name: &str) -> Option> { + self.definition.global_properties_and_callbacks(global_name).map(|iter| { + iter.filter_map(|(name, (ty, _))| match ty { + Type::Callback(function) => Some(PyCallbackInfo::new(name, &function)), + _ => None, + }) + .collect() + }) + } + + fn global_function_infos(&self, global_name: &str) -> Option> { + self.definition.global_properties_and_callbacks(global_name).map(|iter| { + iter.filter_map(|(name, (ty, _))| match ty { + Type::Function(function) => Some(PyFunctionInfo::new(name, &function)), + _ => None, + }) + .collect() + }) + } + fn callback_returns_void(&self, callback_name: &str) -> bool { let callback_name = normalize_identifier(callback_name); self.definition @@ -357,6 +424,183 @@ impl From for PyValueType { } } +fn python_identifier(name: &str) -> String { + if name.is_empty() { + return String::new(); + } + let mut result = name.replace('-', "_"); + if result.chars().next().is_some_and(|c| c.is_ascii_digit()) { + result.insert(0, '_'); + } + result +} + +fn type_to_python_hint(ty: &i_slint_compiler::langtype::Type) -> String { + use i_slint_compiler::langtype::Type::*; + + match ty { + Void => "None".into(), + Bool => "bool".into(), + Int32 => "int".into(), + Float32 | Duration | PhysicalLength | LogicalLength | Rem | Angle | Percent + | UnitProduct(_) => "float".into(), + String => "str".into(), + Brush | Color => "slint.Brush".into(), + Image => "slint.Image".into(), + Model => "slint.Model".into(), + Array(inner) => format!("slint.ListModel[{}]", type_to_python_hint(inner)), + Struct(struct_ty) => struct_to_python_hint(struct_ty), + // Enumeration(enum_ty) => { + // let name = enum_ty.name.as_str(); + // let tail = name.rsplit("::").next().unwrap_or(name); + // format!("slint.{}", python_identifier(tail)) + // } + Callback(function) | Function(function) => function_to_python_hint(function), + // ComponentFactory => "slint.ComponentFactory".into(), // TODO + // PathData | Easing | ElementReference | LayoutCache | InferredProperty + // | InferredCallback | Invalid => "Any".into(), + _ => "Any".into(), + } +} + +fn struct_to_python_hint(struct_ty: &Rc) -> String { + if let Some(inner_ty) = optional_struct_inner(struct_ty) { + return format!("Optional[{}]", type_to_python_hint(inner_ty)); + } + + if let Some(name) = &struct_ty.name { + let full = name.as_str(); + let tail = full.rsplit("::").next().unwrap_or(full); + if full.starts_with("slint::") { + return format!("slint.{}", python_identifier(tail)); + } + return python_identifier(tail); + } + + "dict[str, Any]".into() +} + +fn optional_struct_inner( + struct_ty: &Rc, +) -> Option<&i_slint_compiler::langtype::Type> { + let name = struct_ty.name.as_ref()?; + let tail = name.as_str().rsplit("::").next().unwrap_or(name.as_str()); + let tail_lower = tail.to_ascii_lowercase(); + if !tail_lower.starts_with("optional") { + return None; + } + + if let Some(value_ty) = + struct_ty.fields.get("value").or_else(|| struct_ty.fields.get("maybe_value")) + { + return Some(value_ty); + } + + struct_ty.fields.values().next() +} + +fn function_to_python_hint(function: &Rc) -> String { + let args: Vec = function.args.iter().map(type_to_python_hint).collect(); + let return_type = type_to_python_hint(&function.return_type); + + if args.is_empty() { + if function.return_type == i_slint_compiler::langtype::Type::Void { + "Callable[..., Any]".into() + } else { + format!("Callable[[], {}]", return_type) + } + } else { + format!("Callable[[{}], {}]", args.join(", "), return_type) + } +} + +#[gen_stub_pyclass] +#[pyclass(module = "slint")] +#[derive(Clone)] +pub struct PyPropertyInfo { + #[pyo3(get)] + pub name: String, + #[pyo3(get)] + pub python_type: String, +} + +impl PyPropertyInfo { + fn new(name: String, ty: &i_slint_compiler::langtype::Type) -> Self { + Self { name, python_type: type_to_python_hint(ty) } + } +} + +#[gen_stub_pyclass] +#[pyclass(module = "slint")] +#[derive(Clone)] +pub struct PyCallbackParameter { + #[pyo3(get)] + pub name: Option, + #[pyo3(get)] + pub python_type: String, +} + +impl PyCallbackParameter { + fn new(name: Option, ty: &i_slint_compiler::langtype::Type) -> Self { + let name = name.and_then(|n| if n.is_empty() { None } else { Some(n.into()) }); + Self { name, python_type: type_to_python_hint(ty) } + } +} + +#[gen_stub_pyclass] +#[pyclass(module = "slint")] +#[derive(Clone)] +pub struct PyCallbackInfo { + #[pyo3(get)] + pub name: String, + #[pyo3(get)] + pub parameters: Vec, + #[pyo3(get)] + pub return_type: String, +} + +impl PyCallbackInfo { + fn new(name: String, function: &Rc) -> Self { + let mut parameters = Vec::with_capacity(function.args.len()); + for (idx, arg_ty) in function.args.iter().enumerate() { + let arg_name = function.arg_names.get(idx).cloned(); + parameters.push(PyCallbackParameter::new(arg_name, arg_ty)); + } + Self { + name, + parameters, + return_type: type_to_python_hint(&function.return_type), + } + } +} + +#[gen_stub_pyclass] +#[pyclass(module = "slint")] +#[derive(Clone)] +pub struct PyFunctionInfo { + #[pyo3(get)] + pub name: String, + #[pyo3(get)] + pub parameters: Vec, + #[pyo3(get)] + pub return_type: String, +} + +impl PyFunctionInfo { + fn new(name: String, function: &Rc) -> Self { + let mut parameters = Vec::with_capacity(function.args.len()); + for (idx, arg_ty) in function.args.iter().enumerate() { + let arg_name = function.arg_names.get(idx).cloned(); + parameters.push(PyCallbackParameter::new(arg_name, arg_ty)); + } + Self { + name, + parameters, + return_type: type_to_python_hint(&function.return_type), + } + } +} + #[gen_stub_pyclass] #[pyclass(unsendable, weakref)] pub struct ComponentInstance { diff --git a/api/python/slint/lib.rs b/api/python/slint/lib.rs index 2489a32572d..a16763c625b 100644 --- a/api/python/slint/lib.rs +++ b/api/python/slint/lib.rs @@ -180,6 +180,10 @@ fn slint(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(run_event_loop, m)?)?; m.add_function(wrap_pyfunction!(quit_event_loop, m)?)?; diff --git a/api/python/slint/pyproject.toml b/api/python/slint/pyproject.toml index 244b10053d2..c154ff2a9af 100644 --- a/api/python/slint/pyproject.toml +++ b/api/python/slint/pyproject.toml @@ -24,10 +24,12 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets", - "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", ] +dependencies = [ + "libcst>=1.8.5", +] [project.urls] Homepage = "https://slint.dev" diff --git a/api/python/slint/slint/__init__.py b/api/python/slint/slint/__init__.py index b90e134339c..9ea19cee199 100644 --- a/api/python/slint/slint/__init__.py +++ b/api/python/slint/slint/__init__.py @@ -12,7 +12,7 @@ import logging import copy import typing -from typing import Any +from typing import Any, Callable, TypeVar, overload import pathlib from .models import ListModel, Model from .slint import Image, Color, Brush, Timer, TimerMode @@ -418,10 +418,21 @@ def run_as_task(*args, **kwargs) -> None: # type: ignore return callable +_T_Callback = TypeVar("_T_Callback", bound=Callable[..., Any]) + +@overload +def callback(__func: _T_Callback, /) -> _T_Callback: ... + +@overload +def callback(*, global_name: str | None = ..., name: str | None = ...) -> Callable[[ _T_Callback ], _T_Callback]: ... + +@overload +def callback(__func: _T_Callback, /, *, global_name: str | None = ..., name: str | None = ...) -> _T_Callback: ... + def callback( - global_name: str | None = None, name: str | None = None -) -> typing.Callable[..., Any]: + __func: _T_Callback | None = None, /, *, global_name: str | None = None, name: str | None = None +) -> typing.Union[_T_Callback, typing.Callable[[_T_Callback], _T_Callback]]: """Use the callback decorator to mark a method as a callback that can be invoked from the Slint component. For the decorator to work, the method must be a member of a class that is Slint component. @@ -449,16 +460,20 @@ def button_clicked(self): This is only supported for callbacks that don't return any value, and requires Python >= 3.13. """ - if callable(global_name): - callback = global_name - return _callback_decorator(callback, {}) - else: - info = {} - if name: - info["name"] = name - if global_name: - info["global_name"] = global_name - return lambda callback: _callback_decorator(callback, info) + # If used as @callback without args: __func is the callable + if __func is not None and callable(__func): + return _callback_decorator(__func, {}) + + info: dict[str, str] = {} + if name: + info["name"] = name + if global_name: + info["global_name"] = global_name + + def _wrapper(fn: _T_Callback) -> _T_Callback: + return typing.cast(_T_Callback, _callback_decorator(fn, info)) + + return _wrapper def set_xdg_app_id(app_id: str) -> None: diff --git a/api/python/slint/slint/codegen/__init__.py b/api/python/slint/slint/codegen/__init__.py new file mode 100644 index 00000000000..a9b50fb8802 --- /dev/null +++ b/api/python/slint/slint/codegen/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from .cli import main + +__all__ = ["main"] diff --git a/api/python/slint/slint/codegen/__main__.py b/api/python/slint/slint/codegen/__main__.py new file mode 100644 index 00000000000..35aa8e81ab9 --- /dev/null +++ b/api/python/slint/slint/codegen/__main__.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import sys + +from .cli import main + + +if __name__ == "__main__": # pragma: no cover - CLI entry point + sys.exit(main()) diff --git a/api/python/slint/slint/codegen/cli.py b/api/python/slint/slint/codegen/cli.py new file mode 100644 index 00000000000..ad5ef2ba6cd --- /dev/null +++ b/api/python/slint/slint/codegen/cli.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from .generator import generate_project +from .models import GenerationConfig + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="slint.codegen", + description=( + "Generate Python source and stub files from Slint .slint inputs." + ), + ) + + subparsers = parser.add_subparsers(dest="command", required=False) + + gen_parser = subparsers.add_parser( + "generate", + help="Generate Python modules for the provided .slint inputs.", + ) + _add_generate_arguments(gen_parser) + + # Allow invoking the root command without specifying the subcommand by + # mirroring the generate options onto the root parser. This keeps the CLI + # ergonomic (`python -m slint.codegen --input ...`). + _add_generate_arguments(parser) + + return parser + + +def _add_generate_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--input", + "-i", + dest="inputs", + action="append", + type=Path, + required=False, + help=( + "Path to a .slint file or directory containing .slint files. " + "May be supplied multiple times. Defaults to the current working " + "directory if omitted." + ), + ) + parser.add_argument( + "--output", + "-o", + dest="output", + type=Path, + default=None, + help=( + "Directory that will receive the generated Python sources. " + "When omitted, files are generated next to each input .slint file." + ), + ) + parser.add_argument( + "--include", + dest="include_paths", + action="append", + type=Path, + default=None, + help=( + "Additional include paths to pass to the Slint compiler. " + "May be provided multiple times." + ), + ) + parser.add_argument( + "--library", + dest="library_paths", + action="append", + default=None, + metavar="NAME=PATH", + help=( + "Library import mapping passed to the Slint compiler in the form " + "@mylib=path/to/lib." + ), + ) + parser.add_argument( + "--style", + dest="style", + default=None, + help="Widget style to apply when compiling (for example, 'material').", + ) + parser.add_argument( + "--translation-domain", + dest="translation_domain", + default=None, + help="Translation domain to embed into generated modules.", + ) + parser.add_argument( + "--quiet", + dest="quiet", + action="store_true", + help="Suppress compiler warnings during generation.", + ) + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + # The CLI accepts either the explicit "generate" subcommand or the root + # invocation. Determine which parser branch was taken. + command = getattr(args, "command", None) + if command not in (None, "generate"): + parser.error(f"Unknown command: {command}") + + inputs: list[Path] = args.inputs or [Path.cwd()] + config = GenerationConfig( + include_paths=args.include_paths or [], + library_paths=_parse_library_paths(args.library_paths or []), + style=args.style, + translation_domain=args.translation_domain, + quiet=bool(args.quiet), + ) + + generate_project(inputs=inputs, output_dir=args.output, config=config) + return 0 + + +def _parse_library_paths(values: list[str]) -> dict[str, Path]: + mapping: dict[str, Path] = {} + for raw in values: + if "=" not in raw: + raise SystemExit( + f"Library mapping '{raw}' must be provided in the form NAME=PATH" + ) + name, path_str = raw.split("=", maxsplit=1) + name = name.strip() + if not name: + raise SystemExit("Library mapping requires a non-empty name before '='") + path = Path(path_str.strip()) + mapping[name] = path + return mapping + + +__all__ = ["main", "build_parser"] diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py new file mode 100644 index 00000000000..cb78bd26e89 --- /dev/null +++ b/api/python/slint/slint/codegen/emitters.py @@ -0,0 +1,356 @@ +from __future__ import annotations + +import os +from pathlib import Path + +import libcst as cst + +from .models import GenerationConfig, ModuleArtifacts, CallbackMeta +from .utils import normalize_identifier, path_literal + + +def module_relative_path_expr(module_dir: Path, target: Path) -> str: + try: + rel = os.path.relpath(target, module_dir) + except ValueError: + return f"Path({path_literal(target)})" + + if rel in (".", ""): + return "_MODULE_DIR" + + parts = Path(rel).parts + expr = "_MODULE_DIR" + for part in parts: + if part == ".": + continue + expr += f" / {repr(part)}" + return expr + + +def write_python_module( + path: Path, + *, + source_relative: str, + resource_name: str, + config: GenerationConfig, + artifacts: ModuleArtifacts, +) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + module_dir = path.parent + + include_exprs = [ + module_relative_path_expr(module_dir, include_path) + for include_path in config.include_paths + ] + include_expr_code = f"[{', '.join(include_exprs)}]" if include_exprs else "None" + + library_items = [ + f"{path_literal(name)}: {module_relative_path_expr(module_dir, lib_path)}" + for name, lib_path in config.library_paths.items() + ] + library_expr_code = f"{{{', '.join(library_items)}}}" if library_items else "None" + + style_expr = path_literal(config.style) if config.style else "None" + domain_expr = path_literal(config.translation_domain) if config.translation_domain else "None" + + export_bindings: dict[str, str] = {} + for component in artifacts.components: + export_bindings[component.name] = component.py_name + for struct in artifacts.structs: + export_bindings[struct.name] = struct.py_name + for enum in artifacts.enums: + export_bindings[enum.name] = enum.py_name + + export_items = list(export_bindings.values()) + [ + normalize_identifier(alias) for _, alias in artifacts.named_exports + ] + + header: list[cst.CSTNode] = [ + cst.EmptyLine(comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}")) + ] + + body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + cst.EmptyLine(), + _stmt("import importlib.resources as _resources"), + _stmt("import os"), + _stmt("import types"), + _stmt("from contextlib import nullcontext as _nullcontext"), + _stmt("from pathlib import Path"), + _stmt("from typing import Any"), + cst.EmptyLine(), + _stmt("import slint"), + cst.EmptyLine(), + ] + + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(item))) for item in export_items] + ) + body.append( + cst.SimpleStatementLine( + [cst.Assign(targets=[cst.AssignTarget(cst.Name("__all__"))], value=all_list)] + ) + ) + body.append(cst.EmptyLine()) + + body.append(_stmt("_MODULE_DIR = Path(__file__).parent")) + body.append(cst.EmptyLine()) + + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("_SLINT_RESOURCE"))], + value=cst.SimpleString(repr(resource_name)), + ) + ] + ) + ) + body.append(cst.EmptyLine()) + + load_source = ( + "def _load() -> types.SimpleNamespace:\n" + " \"\"\"Load the compiled Slint module for this package.\"\"\"\n" + " package = __package__ or (__spec__.parent if __spec__ else None)\n" + " if package:\n" + " ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE))\n" + " else:\n" + " ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE))\n" + " with ctx as slint_path:\n" + f" include_paths: list[os.PathLike[Any] | Path] = {include_expr_code}\n" + f" library_paths: dict[str, os.PathLike[Any] | Path] | None = {library_expr_code}\n" + " return slint.load_file(\n" + " path=slint_path,\n" + " quiet=True,\n" + f" style={style_expr},\n" + " include_paths=include_paths,\n" + " library_paths=library_paths,\n" + f" translation_domain={domain_expr},\n" + " )\n" + ) + + load_func = cst.parse_module(load_source).body[0] + + body.append(load_func) + body.append(cst.EmptyLine()) + body.append(_stmt("_module = _load()")) + body.append(cst.EmptyLine()) + + for original, binding in export_bindings.items(): + body.append(_stmt(f"{binding} = _module.{original}")) + for orig, alias in artifacts.named_exports: + alias_name = normalize_identifier(alias) + target = export_bindings.get(orig, normalize_identifier(orig)) + body.append(_stmt(f"{alias_name} = {target}")) + + module = cst.Module(body=header + body) # type: ignore[arg-type] + path.write_text(module.code, encoding="utf-8") + + +def write_stub_module(path: Path, *, artifacts: ModuleArtifacts) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + typing_imports = {"Any", "Callable"} + + def register_type(type_str: str) -> None: + if "Optional[" in type_str: + typing_imports.add("Optional") + if "Literal[" in type_str: + typing_imports.add("Literal") + if "Union[" in type_str: + typing_imports.add("Union") + + preamble: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + cst.EmptyLine(), + _stmt("import enum"), + cst.EmptyLine(), + ] + + post_body: list[cst.CSTNode] = [] + + export_names = [component.py_name for component in artifacts.components] + export_names += [struct.py_name for struct in artifacts.structs] + export_names += [enum.py_name for enum in artifacts.enums] + export_names += [ + normalize_identifier(alias) for _, alias in artifacts.named_exports + ] + if export_names: + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] + ) + post_body.append( + cst.SimpleStatementLine( + [cst.Assign(targets=[cst.AssignTarget(cst.Name("__all__"))], value=all_list)] + ) + ) + post_body.append(cst.EmptyLine()) + + def ann_assign(name: str, type_expr: str) -> cst.BaseStatement: + return cst.SimpleStatementLine( + [ + cst.AnnAssign( + target=cst.Name(name), + annotation=cst.Annotation(annotation=cst.parse_expression(type_expr)), + value=None, + ) + ] + ) + + def ellipsis_line() -> cst.BaseStatement: + return cst.SimpleStatementLine([cst.Expr(value=cst.Ellipsis())]) + + def init_stub() -> cst.FunctionDef: + return cst.FunctionDef( + name=cst.Name("__init__"), + params=cst.Parameters( + params=[cst.Param(name=cst.Name("self"))], + star_kwarg=cst.Param( + name=cst.Name("kwargs"), + annotation=cst.Annotation(annotation=cst.Name("Any")), + ), + ), + returns=cst.Annotation(annotation=cst.Name("None")), + body=cst.IndentedBlock(body=[ellipsis_line()]), + ) + + for struct in artifacts.structs: + struct_body: list[cst.BaseStatement] = [init_stub()] + if struct.fields: + for field in struct.fields: + register_type(field.type_hint) + struct_body.append(ann_assign(field.py_name, field.type_hint)) + else: + struct_body.append(ellipsis_line()) + post_body.append( + cst.ClassDef( + name=cst.Name(struct.py_name), + bases=[], + body=cst.IndentedBlock(body=struct_body), + ) + ) + post_body.append(cst.EmptyLine()) + + for enum_meta in artifacts.enums: + enum_body: list[cst.BaseStatement] = [] + if enum_meta.values: + for value in enum_meta.values: + enum_body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name(value.py_name))], + value=cst.SimpleString(repr(value.value)), + ) + ] + ) + ) + else: + enum_body.append(ellipsis_line()) + post_body.append( + cst.ClassDef( + name=cst.Name(enum_meta.py_name), + bases=[cst.Arg(value=cst.Attribute(value=cst.Name("enum"), attr=cst.Name("Enum")))], + body=cst.IndentedBlock(body=enum_body), + ) + ) + post_body.append(cst.EmptyLine()) + + for component in artifacts.components: + component_body: list[cst.BaseStatement] = [init_stub()] + for prop in component.properties: + register_type(prop.type_hint) + component_body.append(ann_assign(prop.py_name, prop.type_hint)) + for callback in component.callbacks: + annotation = format_callable_annotation(callback) + register_type(annotation) + component_body.append(ann_assign(callback.py_name, annotation)) + for fn in component.functions: + annotation = format_callable_annotation(fn) + register_type(annotation) + component_body.append(ann_assign(fn.py_name, annotation)) + for global_meta in component.globals: + component_body.append( + ann_assign(global_meta.py_name, f"{component.py_name}.{global_meta.py_name}") + ) + + for global_meta in component.globals: + inner_body: list[cst.BaseStatement] = [] + if not (global_meta.properties or global_meta.callbacks or global_meta.functions): + inner_body.append(ellipsis_line()) + else: + for prop in global_meta.properties: + register_type(prop.type_hint) + inner_body.append(ann_assign(prop.py_name, prop.type_hint)) + for callback in global_meta.callbacks: + annotation = format_callable_annotation(callback) + register_type(annotation) + inner_body.append(ann_assign(callback.py_name, annotation)) + for fn in global_meta.functions: + annotation = format_callable_annotation(fn) + register_type(annotation) + inner_body.append(ann_assign(fn.py_name, annotation)) + component_body.append( + cst.ClassDef( + name=cst.Name(global_meta.py_name), + bases=[], + body=cst.IndentedBlock(body=inner_body), + ) + ) + + post_body.append( + cst.ClassDef( + name=cst.Name(component.py_name), + bases=[ + cst.Arg( + value=cst.Attribute(value=cst.Name("slint"), attr=cst.Name("Component")) + ) + ], + body=cst.IndentedBlock(body=component_body), + ) + ) + post_body.append(cst.EmptyLine()) + + bindings: dict[str, str] = {} + for component in artifacts.components: + bindings[component.name] = component.py_name + for struct in artifacts.structs: + bindings[struct.name] = struct.py_name + for enum_meta in artifacts.enums: + bindings[enum_meta.name] = enum_meta.py_name + + for orig, alias in artifacts.named_exports: + alias_name = normalize_identifier(alias) + target = bindings.get(orig, normalize_identifier(orig)) + post_body.append(_stmt(f"{alias_name} = {target}")) + post_body.append(cst.EmptyLine()) + + typing_alias = ", ".join(sorted(typing_imports)) + preamble.append(_stmt(f"from typing import {typing_alias}")) + preamble.append(cst.EmptyLine()) + preamble.append(_stmt("import slint")) + preamble.append(cst.EmptyLine()) + + body = preamble + post_body + + module = cst.Module(body=body) # type: ignore[arg-type] + path.write_text(module.code, encoding="utf-8") + + +def format_callable_annotation(callback: "CallbackMeta") -> str: # type: ignore[name-defined] + args = callback.arg_types + return_type = callback.return_type + + args_literal = ", ".join(args) + arg_repr = f"[{args_literal}]" if args_literal else "[]" + return f"Callable[{arg_repr}, {return_type}]" + + +__all__ = [ + "module_relative_path_expr", + "write_python_module", + "write_stub_module", +] diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py new file mode 100644 index 00000000000..3c637e260f0 --- /dev/null +++ b/api/python/slint/slint/codegen/generator.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +import shutil +from pathlib import Path +from typing import Iterable, TYPE_CHECKING + +from slint import slint as native + +from .emitters import write_python_module, write_stub_module +from .models import ( + CallbackMeta, + ComponentMeta, + EnumMeta, + EnumValueMeta, + GenerationConfig, + GlobalMeta, + ModuleArtifacts, + PropertyMeta, + StructFieldMeta, + StructMeta, +) +from .utils import normalize_identifier + +if TYPE_CHECKING: + from slint.slint import CallbackInfo, FunctionInfo, PyDiagnostic + + +def generate_project( + *, + inputs: Iterable[Path], + output_dir: Path | None, + config: GenerationConfig, +) -> None: + source_roots = [Path(p).resolve() for p in inputs] + files = list(_discover_slint_files(source_roots)) + if not files: + raise SystemExit("No .slint files found in the supplied inputs") + + if output_dir is not None: + output_dir.mkdir(parents=True, exist_ok=True) + + compiler = native.Compiler() + if config.style: + compiler.style = config.style + if config.include_paths: + compiler.include_paths = config.include_paths.copy() # type: ignore[assignment] + if config.library_paths: + compiler.library_paths = config.library_paths.copy() # type: ignore[assignment] + if config.translation_domain: + compiler.translation_domain = config.translation_domain + + for source_path, root in files: + compilation = _compile_slint(compiler, source_path, config) + if compilation is None: + continue + + artifacts = _collect_metadata(compilation) + relative = source_path.relative_to(root) + + if output_dir is None: + module_dir = source_path.parent + target_stem = module_dir / source_path.stem + copy_slint = False + slint_destination = source_path + resource_name = source_path.name + source_descriptor = source_path.name + else: + module_dir = output_dir / relative.parent + module_dir.mkdir(parents=True, exist_ok=True) + target_stem = module_dir / relative.stem + copy_slint = True + slint_destination = target_stem.with_suffix(".slint") + resource_name = relative.name + source_descriptor = str(relative) + + write_python_module( + target_stem.with_suffix(".py"), + source_relative=source_descriptor, + resource_name=resource_name, + config=config, + artifacts=artifacts, + ) + write_stub_module(target_stem.with_suffix(".pyi"), artifacts=artifacts) + + if copy_slint and slint_destination != source_path: + shutil.copy2(source_path, slint_destination) + + +def _discover_slint_files(inputs: Iterable[Path]) -> Iterable[tuple[Path, Path]]: + for path in inputs: + if path.is_file() and path.suffix == ".slint": + resolved = path.resolve() + yield resolved, resolved.parent + elif path.is_dir(): + root = path.resolve() + for file in sorted(root.rglob("*.slint")): + resolved = file.resolve() + yield resolved, root + + +def _compile_slint( + compiler: native.Compiler, + source_path: Path, + config: GenerationConfig, +) -> native.CompilationResult | None: + result = compiler.build_from_path(source_path) + + diagnostics = result.diagnostics + diagnostic_error = getattr(native, "DiagnosticLevel", None) + error_enum = getattr(diagnostic_error, "Error", None) + + def is_error(diag: PyDiagnostic) -> bool: + if error_enum is not None: + return diag.level == error_enum + return str(diag.level).lower().startswith("error") + + errors = [diag for diag in diagnostics if is_error(diag)] + warnings = [diag for diag in diagnostics if not is_error(diag)] + + if warnings and not config.quiet: + for diag in warnings: + print(f"warning: {diag}") + + if errors: + for diag in errors: + print(f"error: {diag}") + print(f"Skipping generation for {source_path}") + return None + + return result + + +def _collect_metadata(result: native.CompilationResult) -> ModuleArtifacts: + components: list[ComponentMeta] = [] + for name in result.component_names: + comp = result.component(name) + + property_info = {info.name: info for info in comp.property_infos()} + callback_info = {info.name: info for info in comp.callback_infos()} + function_info = {info.name: info for info in comp.function_infos()} + + properties: list[PropertyMeta] = [] + for key in comp.properties: + info = property_info[key] + type_hint = info.python_type + properties.append( + PropertyMeta( + name=key, + py_name=normalize_identifier(key), + type_hint=type_hint, + ) + ) + + callbacks = [_callback_meta(cb, callback_info[cb]) for cb in comp.callbacks] + functions = [_callback_meta(fn, function_info[fn]) for fn in comp.functions] + + globals_meta: list[GlobalMeta] = [] + for global_name in comp.globals: + global_property_info = { + info.name: info for info in comp.global_property_infos(global_name) + } + global_callback_info = { + info.name: info for info in comp.global_callback_infos(global_name) + } + global_function_info = { + info.name: info for info in comp.global_function_infos(global_name) + } + properties_meta: list[PropertyMeta] = [] + + for key in comp.global_properties(global_name): + py_key = normalize_identifier(key) + info = global_property_info[key] + type_hint = info.python_type + properties_meta.append( + PropertyMeta( + name=key, + py_name=py_key, + type_hint=type_hint, + ) + ) + + callbacks_meta = [ + _callback_meta(cb, global_callback_info[cb]) + for cb in comp.global_callbacks(global_name) + ] + + functions_meta = [ + _callback_meta(fn, global_function_info[fn]) + for fn in comp.global_functions(global_name) + ] + + globals_meta.append( + GlobalMeta( + name=global_name, + py_name=normalize_identifier(global_name), + properties=properties_meta, + callbacks=callbacks_meta, + functions=functions_meta, + ) + ) + + components.append( + ComponentMeta( + name=name, + py_name=normalize_identifier(name), + properties=properties, + callbacks=callbacks, + functions=functions, + globals=globals_meta, + ) + ) + + structs_meta: list[StructMeta] = [] + enums_meta: list[EnumMeta] = [] + structs, enums = result.structs_and_enums + + for struct_name, struct_prototype in structs.items(): + fields: list[StructFieldMeta] = [] + for field_name, value in struct_prototype: + fields.append( + StructFieldMeta( + name=field_name, + py_name=normalize_identifier(field_name), + type_hint=_python_value_hint(value), + ) + ) + structs_meta.append( + StructMeta( + name=struct_name, + py_name=normalize_identifier(struct_name), + fields=fields, + ) + ) + + for enum_name, enum_cls in enums.items(): + values: list[EnumValueMeta] = [] + for member, enum_member in enum_cls.__members__.items(): # type: ignore + values.append( + EnumValueMeta( + name=member, + py_name=normalize_identifier(member), + value=enum_member.name, + ) + ) + enums_meta.append( + EnumMeta( + name=enum_name, + py_name=normalize_identifier(enum_name), + values=values, + ) + ) + + named_exports = [(orig, alias) for orig, alias in result.named_exports] + + return ModuleArtifacts( + components=components, + structs=structs_meta, + enums=enums_meta, + named_exports=named_exports, + ) + + +__all__ = ["generate_project"] + + +def _python_value_hint(value: object) -> str: + if isinstance(value, bool): + return "bool" + if isinstance(value, int): + return "int" + if isinstance(value, float): + return "float" + if isinstance(value, str): + return "str" + if isinstance(value, native.Image): + return "slint.Image" + if isinstance(value, native.Brush): + return "slint.Brush" + return "Any" + + +def _callback_meta(name: str, info: CallbackInfo | FunctionInfo) -> CallbackMeta: + return CallbackMeta( + name=name, + py_name=normalize_identifier(name), + arg_types=[param.python_type for param in info.parameters], + return_type=info.return_type, + ) diff --git a/api/python/slint/slint/codegen/models.py b/api/python/slint/slint/codegen/models.py new file mode 100644 index 00000000000..1fa48bb33d5 --- /dev/null +++ b/api/python/slint/slint/codegen/models.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import List, Dict + + +@dataclass(slots=True) +class GenerationConfig: + include_paths: List[Path] + library_paths: Dict[str, Path] + style: str | None + translation_domain: str | None + quiet: bool = False + + +@dataclass(slots=True) +class PropertyMeta: + name: str + py_name: str + type_hint: str + + +@dataclass(slots=True) +class CallbackMeta: + name: str + py_name: str + arg_types: List[str] + return_type: str + + +@dataclass(slots=True) +class ComponentMeta: + name: str + py_name: str + properties: List[PropertyMeta] + callbacks: List[CallbackMeta] + functions: List[CallbackMeta] + globals: List["GlobalMeta"] + + +@dataclass(slots=True) +class GlobalMeta: + name: str + py_name: str + properties: List[PropertyMeta] + callbacks: List[CallbackMeta] + functions: List[CallbackMeta] + + +@dataclass(slots=True) +class StructFieldMeta: + name: str + py_name: str + type_hint: str + + +@dataclass(slots=True) +class StructMeta: + name: str + py_name: str + fields: List[StructFieldMeta] + + +@dataclass(slots=True) +class EnumValueMeta: + name: str + py_name: str + value: str + + +@dataclass(slots=True) +class EnumMeta: + name: str + py_name: str + values: List[EnumValueMeta] + + +@dataclass(slots=True) +class ModuleArtifacts: + components: List[ComponentMeta] + structs: List[StructMeta] + enums: List[EnumMeta] + named_exports: List[tuple[str, str]] + + +__all__ = [ + "GenerationConfig", + "PropertyMeta", + "CallbackMeta", + "ComponentMeta", + "GlobalMeta", + "StructFieldMeta", + "StructMeta", + "EnumValueMeta", + "EnumMeta", + "ModuleArtifacts", +] diff --git a/api/python/slint/slint/codegen/utils.py b/api/python/slint/slint/codegen/utils.py new file mode 100644 index 00000000000..70270d062a5 --- /dev/null +++ b/api/python/slint/slint/codegen/utils.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from pathlib import Path + + +def normalize_identifier(identifier: str) -> str: + identifier = identifier.replace("-", "_") + if identifier[0].isdigit(): + identifier = f"_{identifier}" + return identifier + + +def path_literal(value: str | Path) -> str: + return repr(str(value)) + + +__all__ = ["normalize_identifier", "path_literal"] diff --git a/api/python/slint/slint/slint.pyi b/api/python/slint/slint/slint.pyi index c506ea4b4d1..b138b6dfe8a 100644 --- a/api/python/slint/slint/slint.pyi +++ b/api/python/slint/slint/slint.pyi @@ -203,6 +203,24 @@ class ComponentInstance: ) -> None: ... def get_global_property(self, global_name: str, property_name: str) -> Any: ... +class PropertyInfo: + name: str + python_type: str + +class CallbackParameter: + name: str | None + python_type: str + +class CallbackInfo: + name: str + parameters: list[CallbackParameter] + return_type: str + +class FunctionInfo: + name: str + parameters: list[CallbackParameter] + return_type: str + class ComponentDefinition: def create(self) -> ComponentInstance: ... name: str @@ -210,6 +228,12 @@ class ComponentDefinition: functions: list[str] callbacks: list[str] properties: dict[str, ValueType] + def property_infos(self) -> list[PropertyInfo]: ... + def callback_infos(self) -> list[CallbackInfo]: ... + def function_infos(self) -> list[FunctionInfo]: ... + def global_property_infos(self, global_name: str) -> list[PropertyInfo]: ... + def global_callback_infos(self, global_name: str) -> list[CallbackInfo]: ... + def global_function_infos(self, global_name: str) -> list[FunctionInfo]: ... def global_functions(self, global_name: str) -> list[str]: ... def global_callbacks(self, global_name: str) -> list[str]: ... def global_properties(self, global_name: str) -> typing.Dict[str, ValueType]: ... diff --git a/api/python/slint/tests/codegen/examples/__init__.py b/api/python/slint/tests/codegen/examples/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/python/slint/tests/codegen/examples/counter/.gitignore b/api/python/slint/tests/codegen/examples/counter/.gitignore new file mode 100644 index 00000000000..dc9b2375c7a --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/.gitignore @@ -0,0 +1 @@ +generated \ No newline at end of file diff --git a/api/python/slint/tests/codegen/examples/counter/README.md b/api/python/slint/tests/codegen/examples/counter/README.md new file mode 100644 index 00000000000..0f27984df37 --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/README.md @@ -0,0 +1,32 @@ +# Counter Example Using slint.codegen + +This example mirrors the callback pattern described in the +[Slint Python binding documentation](https://github.com/slint-ui/slint/blob/master/api/python/slint/README.md#readme). +Instead of relying on the runtime auto-loader, it uses the experimental +`slint.codegen` CLI to emit static Python modules (`.py`/`.pyi`) for the +`counter.slint` UI and then subclasses the generated component. + +## Steps + +1. Generate the bindings: + + ```bash + uv run python examples/counter/generate.py + ``` + + This produces `examples/counter/generated/counter.py` and + `examples/counter/generated/counter.pyi` alongside a copy of the + source `.slint` file, all ready for import. + +2. Run the app: + + ```bash + uv run python -m examples.counter.main + ``` + + Each click anywhere in the window increments the counter via the + `request_increase` callback implemented in Python. + +> **Tip:** The generated `.pyi` file makes the `CounterWindow` API visible to +> type checkers and IDEs, providing a smoother developer experience compared to +> using the dynamic import hook. diff --git a/api/python/slint/tests/codegen/examples/counter/__init__.py b/api/python/slint/tests/codegen/examples/counter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/python/slint/tests/codegen/examples/counter/counter.slint b/api/python/slint/tests/codegen/examples/counter/counter.slint new file mode 100644 index 00000000000..6679facadc8 --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/counter.slint @@ -0,0 +1,26 @@ +export component CounterWindow inherits Window { + width: 240px; + height: 120px; + + in-out property counter: 0; + callback request_increase(); + + Rectangle { + width: parent.width; + height: parent.height; + background: #2b2b2b; + border-radius: 12px; + + Text { + text: "Count: " + root.counter; + color: white; + font-size: 24px; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => root.request_increase(); + } + } +} diff --git a/api/python/slint/tests/codegen/examples/counter/generate.py b/api/python/slint/tests/codegen/examples/counter/generate.py new file mode 100644 index 00000000000..2f528b66e0a --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/generate.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from pathlib import Path + +from slint.codegen.generator import generate_project +from slint.codegen.models import GenerationConfig + + +def main() -> None: + base_dir = Path(__file__).parent + output = base_dir / "generated" + config = GenerationConfig( + include_paths=[base_dir], + library_paths={}, + style=None, + translation_domain=None, + quiet=False, + ) + + generate_project(inputs=[base_dir / "counter.slint"], output_dir=output, config=config) + print(f"Generated Python bindings into {output.relative_to(base_dir)}") + + +if __name__ == "__main__": + main() diff --git a/api/python/slint/tests/codegen/examples/counter/main.py b/api/python/slint/tests/codegen/examples/counter/main.py new file mode 100644 index 00000000000..e53c8d5e732 --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/main.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import slint + +try: + from .generated.counter import CounterWindow +except ImportError as exc: # pragma: no cover - user-facing guidance + raise SystemExit( + "Generated bindings not found. Run `python generate.py` in the " + "examples/counter directory first." + ) from exc + + +class CounterApp(CounterWindow): + @slint.callback + def request_increase(self) -> None: + self.counter += 1 + + +def main() -> None: + app = CounterApp() + app.show() + app.run() + + +if __name__ == "__main__": + main() diff --git a/api/python/slint/tests/codegen/examples/struct_test.slint b/api/python/slint/tests/codegen/examples/struct_test.slint new file mode 100644 index 00000000000..5cb22938bfc --- /dev/null +++ b/api/python/slint/tests/codegen/examples/struct_test.slint @@ -0,0 +1,7 @@ +struct Foo { + number: int, +} + +export component TestComp inherits Rectangle { + in property data; +} diff --git a/api/python/slint/tests/codegen/test_generator.py b/api/python/slint/tests/codegen/test_generator.py new file mode 100644 index 00000000000..31a32fdc88e --- /dev/null +++ b/api/python/slint/tests/codegen/test_generator.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +import importlib +import importlib.util +import subprocess +import sys +import textwrap +import shutil +from pathlib import Path + +import pytest + +from slint.codegen.cli import _parse_library_paths, main as cli_main +from slint.codegen.generator import generate_project +from slint.codegen.models import GenerationConfig + + +def _write_slint_fixture(target_dir: Path) -> Path: + source = textwrap.dedent( + """ + export struct Config { + value: int, + label: string, + } + + export enum Choice { + first, + second, + } + + export global SharedLogic { + out property total: 0; + callback notify(int); + } + + export component AppWindow inherits Window { + width: 160px; + height: 80px; + in-out property counter: SharedLogic.total; + callback activated(int); + + public function reset() -> int { + SharedLogic.notify(counter); + return counter; + } + + Text { + text: counter; + } + } + """ + ).strip() + + slint_dir = target_dir / "ui" + slint_dir.mkdir(parents=True, exist_ok=True) + slint_file = slint_dir / "app.slint" + slint_file.write_text(source + "\n", encoding="utf-8") + return slint_file + + +def _write_optional_fixture(target_dir: Path) -> Path: + import inspect + + source = inspect.cleandoc( + """ + export struct OptionalInt := { + maybe_value: int, + } + + export struct OptionalFloat := { + maybe_value: float, + } + + export struct OptionalString := { + maybe_value: string, + } + + export struct OptionalBool := { + maybe_value: bool, + } + + export component OptionalDemo { + in-out property maybe_count; + callback on_action(value: OptionalFloat) -> OptionalInt; + + public function compute(input: OptionalString) -> OptionalBool { + return { maybe_value: true }; + } + } + """ + ) + + slint_dir = target_dir / "optional" + slint_dir.mkdir(parents=True, exist_ok=True) + slint_file = slint_dir / "optional.slint" + slint_file.write_text(source + "\n", encoding="utf-8") + return slint_file + + +def _load_module(module_path: Path): + spec = importlib.util.spec_from_file_location("generated_module", module_path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules.pop("generated_module", None) + spec.loader.exec_module(module) # type: ignore[assignment] + return module + + +def test_generate_project_creates_runtime_and_stub(tmp_path: Path) -> None: + slint_file = _write_slint_fixture(tmp_path) + output_dir = tmp_path / "generated" + config = GenerationConfig( + include_paths=[slint_file.parent], + library_paths={}, + style=None, + translation_domain=None, + quiet=True, + ) + + generate_project(inputs=[slint_file], output_dir=output_dir, config=config) + + py_file = output_dir / "app.py" + pyi_file = output_dir / "app.pyi" + slint_copy = output_dir / "app.slint" + + assert py_file.exists() + assert pyi_file.exists() + assert slint_copy.exists() + + runtime_src = py_file.read_text(encoding="utf-8") + assert "Generated by slint.codegen" in runtime_src + assert "AppWindow = _module.AppWindow" in runtime_src + assert "Config = _module.Config" in runtime_src + assert "Choice = _module.Choice" in runtime_src + + stub_src = pyi_file.read_text(encoding="utf-8") + assert "class AppWindow" in stub_src + assert "class Config" in stub_src + assert "class Choice" in stub_src + assert "class SharedLogic" in stub_src + assert "counter: int" in stub_src + assert "activated: Callable[[int], None]" in stub_src + assert "reset: Callable[[], int]" in stub_src + assert "notify: Callable[[int], None]" in stub_src + assert "from typing import Any, Callable" in stub_src + + module = _load_module(py_file) + assert hasattr(module, "AppWindow") + instance = module.AppWindow() + assert hasattr(instance, "show") + assert instance.reset() == 0 + + config_struct = module.Config(value=5, label="demo") + assert config_struct.value == 5 + + +def test_cli_generate(tmp_path: Path) -> None: + slint_file = _write_slint_fixture(tmp_path) + output_dir = tmp_path / "out" + + cmd = [ + sys.executable, + "-m", + "slint.codegen", + "generate", + "--input", + str(slint_file), + "--output", + str(output_dir), + "--quiet", + ] + subprocess.run(cmd, check=True) + + assert (output_dir / "app.py").exists() + assert (output_dir / "app.pyi").exists() + + +def test_generate_optional_type_hints(tmp_path: Path) -> None: + slint_file = _write_optional_fixture(tmp_path) + output_dir = tmp_path / "optional-generated" + config = GenerationConfig( + include_paths=[slint_file.parent], + library_paths={}, + style=None, + translation_domain=None, + quiet=True, + ) + + generate_project(inputs=[slint_file], output_dir=output_dir, config=config) + + stub_src = (output_dir / "optional.pyi").read_text(encoding="utf-8") + assert "maybe_count: Optional[int]" in stub_src + assert "on_action: Callable[[Optional[float]], Optional[int]]" in stub_src + assert "compute: Callable[[Optional[str]], Optional[bool]]" in stub_src + assert "from typing import Any, Callable, Optional" in stub_src + + +def test_cli_main_without_subcommand(tmp_path: Path) -> None: + slint_file = _write_slint_fixture(tmp_path) + exit_code = cli_main([ + "--input", + str(slint_file), + "--quiet", + ]) + + assert exit_code == 0 + assert (slint_file.parent / "app.py").exists() + + +def test_counter_example_workflow(tmp_path: Path) -> None: + project_root = Path(__file__).resolve().parent + example_src = project_root / "examples" / "counter" + example_copy = tmp_path / "examples" / "counter" + shutil.copytree(example_src, example_copy) + + subprocess.run([sys.executable, "generate.py"], cwd=example_copy, check=True) + generated_py = example_copy / "generated" / "counter.py" + assert generated_py.exists() + + sys.path.insert(0, str(tmp_path)) + try: + module = importlib.import_module("examples.counter.main") + finally: + sys.path.pop(0) + + app = module.CounterApp() + assert app.counter == 0 + app.request_increase() + assert app.counter == 1 + +def test_parse_library_paths_and_error_handling() -> None: + mapping = _parse_library_paths(["std=path/to/std"]) + assert mapping == {"std": Path("path/to/std")} + + with pytest.raises(SystemExit): + _parse_library_paths(["missing_delimiter"]) + + +def test_generate_project_requires_sources(tmp_path: Path) -> None: + config = GenerationConfig( + include_paths=[], + library_paths={}, + style=None, + translation_domain=None, + quiet=True, + ) + with pytest.raises(SystemExit): + generate_project(inputs=[tmp_path], output_dir=tmp_path / "o", config=config) diff --git a/api/python/slint/tests/codegen/test_utils.py b/api/python/slint/tests/codegen/test_utils.py new file mode 100644 index 00000000000..7041c4309f5 --- /dev/null +++ b/api/python/slint/tests/codegen/test_utils.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from pathlib import Path + +from slint.codegen.utils import normalize_identifier, path_literal + + +def test_normalize_identifier() -> None: + assert normalize_identifier("foo-bar") == "foo_bar" + assert normalize_identifier("1value") == "_1value" + + +def test_path_literal() -> None: + path = Path("/tmp/demo") + assert path_literal(path) == repr(str(path)) From 4e6819661bacee293c619eba0ba4fd9fe46d6af0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:07:10 +0000 Subject: [PATCH 02/52] [autofix.ci] apply automated fixes --- api/python/slint/interpreter.rs | 12 +---- api/python/slint/pyproject.toml | 4 +- api/python/slint/slint/__init__.py | 18 +++++-- api/python/slint/slint/codegen/__init__.py | 3 ++ api/python/slint/slint/codegen/__main__.py | 3 ++ api/python/slint/slint/codegen/cli.py | 7 +-- api/python/slint/slint/codegen/emitters.py | 53 +++++++++++++++---- api/python/slint/slint/codegen/generator.py | 3 ++ api/python/slint/slint/codegen/models.py | 3 ++ api/python/slint/slint/codegen/utils.py | 3 ++ .../slint/tests/codegen/examples/__init__.py | 2 + .../tests/codegen/examples/counter/README.md | 2 + .../codegen/examples/counter/__init__.py | 2 + .../codegen/examples/counter/counter.slint | 3 ++ .../codegen/examples/counter/generate.py | 7 ++- .../tests/codegen/examples/counter/main.py | 3 ++ .../tests/codegen/examples/struct_test.slint | 3 ++ .../slint/tests/codegen/test_generator.py | 16 ++++-- api/python/slint/tests/codegen/test_utils.py | 3 ++ 19 files changed, 114 insertions(+), 36 deletions(-) diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index b9d207c1a6a..ee7dde70748 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -566,11 +566,7 @@ impl PyCallbackInfo { let arg_name = function.arg_names.get(idx).cloned(); parameters.push(PyCallbackParameter::new(arg_name, arg_ty)); } - Self { - name, - parameters, - return_type: type_to_python_hint(&function.return_type), - } + Self { name, parameters, return_type: type_to_python_hint(&function.return_type) } } } @@ -593,11 +589,7 @@ impl PyFunctionInfo { let arg_name = function.arg_names.get(idx).cloned(); parameters.push(PyCallbackParameter::new(arg_name, arg_ty)); } - Self { - name, - parameters, - return_type: type_to_python_hint(&function.return_type), - } + Self { name, parameters, return_type: type_to_python_hint(&function.return_type) } } } diff --git a/api/python/slint/pyproject.toml b/api/python/slint/pyproject.toml index c154ff2a9af..506f88b9b6e 100644 --- a/api/python/slint/pyproject.toml +++ b/api/python/slint/pyproject.toml @@ -27,9 +27,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", ] -dependencies = [ - "libcst>=1.8.5", -] +dependencies = ["libcst>=1.8.5"] [project.urls] Homepage = "https://slint.dev" diff --git a/api/python/slint/slint/__init__.py b/api/python/slint/slint/__init__.py index 9ea19cee199..f3b9b9ef3a1 100644 --- a/api/python/slint/slint/__init__.py +++ b/api/python/slint/slint/__init__.py @@ -418,20 +418,32 @@ def run_as_task(*args, **kwargs) -> None: # type: ignore return callable + _T_Callback = TypeVar("_T_Callback", bound=Callable[..., Any]) + @overload def callback(__func: _T_Callback, /) -> _T_Callback: ... + @overload -def callback(*, global_name: str | None = ..., name: str | None = ...) -> Callable[[ _T_Callback ], _T_Callback]: ... +def callback( + *, global_name: str | None = ..., name: str | None = ... +) -> Callable[[_T_Callback], _T_Callback]: ... + @overload -def callback(__func: _T_Callback, /, *, global_name: str | None = ..., name: str | None = ...) -> _T_Callback: ... +def callback( + __func: _T_Callback, /, *, global_name: str | None = ..., name: str | None = ... +) -> _T_Callback: ... def callback( - __func: _T_Callback | None = None, /, *, global_name: str | None = None, name: str | None = None + __func: _T_Callback | None = None, + /, + *, + global_name: str | None = None, + name: str | None = None, ) -> typing.Union[_T_Callback, typing.Callable[[_T_Callback], _T_Callback]]: """Use the callback decorator to mark a method as a callback that can be invoked from the Slint component. diff --git a/api/python/slint/slint/codegen/__init__.py b/api/python/slint/slint/codegen/__init__.py index a9b50fb8802..e3afd308114 100644 --- a/api/python/slint/slint/codegen/__init__.py +++ b/api/python/slint/slint/codegen/__init__.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations from .cli import main diff --git a/api/python/slint/slint/codegen/__main__.py b/api/python/slint/slint/codegen/__main__.py index 35aa8e81ab9..df59de77810 100644 --- a/api/python/slint/slint/codegen/__main__.py +++ b/api/python/slint/slint/codegen/__main__.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import sys diff --git a/api/python/slint/slint/codegen/cli.py b/api/python/slint/slint/codegen/cli.py index ad5ef2ba6cd..57a03b2d521 100644 --- a/api/python/slint/slint/codegen/cli.py +++ b/api/python/slint/slint/codegen/cli.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import argparse @@ -10,9 +13,7 @@ def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="slint.codegen", - description=( - "Generate Python source and stub files from Slint .slint inputs." - ), + description=("Generate Python source and stub files from Slint .slint inputs."), ) subparsers = parser.add_subparsers(dest="command", required=False) diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index cb78bd26e89..7812b766cac 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import os @@ -53,7 +56,9 @@ def _stmt(code: str) -> cst.BaseStatement: library_expr_code = f"{{{', '.join(library_items)}}}" if library_items else "None" style_expr = path_literal(config.style) if config.style else "None" - domain_expr = path_literal(config.translation_domain) if config.translation_domain else "None" + domain_expr = ( + path_literal(config.translation_domain) if config.translation_domain else "None" + ) export_bindings: dict[str, str] = {} for component in artifacts.components: @@ -68,7 +73,9 @@ def _stmt(code: str) -> cst.BaseStatement: ] header: list[cst.CSTNode] = [ - cst.EmptyLine(comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}")) + cst.EmptyLine( + comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}") + ) ] body: list[cst.CSTNode] = [ @@ -90,7 +97,11 @@ def _stmt(code: str) -> cst.BaseStatement: ) body.append( cst.SimpleStatementLine( - [cst.Assign(targets=[cst.AssignTarget(cst.Name("__all__"))], value=all_list)] + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], value=all_list + ) + ] ) ) body.append(cst.EmptyLine()) @@ -112,7 +123,7 @@ def _stmt(code: str) -> cst.BaseStatement: load_source = ( "def _load() -> types.SimpleNamespace:\n" - " \"\"\"Load the compiled Slint module for this package.\"\"\"\n" + ' """Load the compiled Slint module for this package."""\n' " package = __package__ or (__spec__.parent if __spec__ else None)\n" " if package:\n" " ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE))\n" @@ -180,11 +191,17 @@ def register_type(type_str: str) -> None: ] if export_names: all_list = cst.List( - elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] + elements=[ + cst.Element(cst.SimpleString(repr(name))) for name in export_names + ] ) post_body.append( cst.SimpleStatementLine( - [cst.Assign(targets=[cst.AssignTarget(cst.Name("__all__"))], value=all_list)] + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], value=all_list + ) + ] ) ) post_body.append(cst.EmptyLine()) @@ -194,7 +211,9 @@ def ann_assign(name: str, type_expr: str) -> cst.BaseStatement: [ cst.AnnAssign( target=cst.Name(name), - annotation=cst.Annotation(annotation=cst.parse_expression(type_expr)), + annotation=cst.Annotation( + annotation=cst.parse_expression(type_expr) + ), value=None, ) ] @@ -253,7 +272,13 @@ def init_stub() -> cst.FunctionDef: post_body.append( cst.ClassDef( name=cst.Name(enum_meta.py_name), - bases=[cst.Arg(value=cst.Attribute(value=cst.Name("enum"), attr=cst.Name("Enum")))], + bases=[ + cst.Arg( + value=cst.Attribute( + value=cst.Name("enum"), attr=cst.Name("Enum") + ) + ) + ], body=cst.IndentedBlock(body=enum_body), ) ) @@ -274,12 +299,16 @@ def init_stub() -> cst.FunctionDef: component_body.append(ann_assign(fn.py_name, annotation)) for global_meta in component.globals: component_body.append( - ann_assign(global_meta.py_name, f"{component.py_name}.{global_meta.py_name}") + ann_assign( + global_meta.py_name, f"{component.py_name}.{global_meta.py_name}" + ) ) for global_meta in component.globals: inner_body: list[cst.BaseStatement] = [] - if not (global_meta.properties or global_meta.callbacks or global_meta.functions): + if not ( + global_meta.properties or global_meta.callbacks or global_meta.functions + ): inner_body.append(ellipsis_line()) else: for prop in global_meta.properties: @@ -306,7 +335,9 @@ def init_stub() -> cst.FunctionDef: name=cst.Name(component.py_name), bases=[ cst.Arg( - value=cst.Attribute(value=cst.Name("slint"), attr=cst.Name("Component")) + value=cst.Attribute( + value=cst.Name("slint"), attr=cst.Name("Component") + ) ) ], body=cst.IndentedBlock(body=component_body), diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index 3c637e260f0..a63fcc5db63 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import shutil diff --git a/api/python/slint/slint/codegen/models.py b/api/python/slint/slint/codegen/models.py index 1fa48bb33d5..c92819b8c1f 100644 --- a/api/python/slint/slint/codegen/models.py +++ b/api/python/slint/slint/codegen/models.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations from dataclasses import dataclass diff --git a/api/python/slint/slint/codegen/utils.py b/api/python/slint/slint/codegen/utils.py index 70270d062a5..d81905220ee 100644 --- a/api/python/slint/slint/codegen/utils.py +++ b/api/python/slint/slint/codegen/utils.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations from pathlib import Path diff --git a/api/python/slint/tests/codegen/examples/__init__.py b/api/python/slint/tests/codegen/examples/__init__.py index e69de29bb2d..55c735ca1ff 100644 --- a/api/python/slint/tests/codegen/examples/__init__.py +++ b/api/python/slint/tests/codegen/examples/__init__.py @@ -0,0 +1,2 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 diff --git a/api/python/slint/tests/codegen/examples/counter/README.md b/api/python/slint/tests/codegen/examples/counter/README.md index 0f27984df37..7dfbfd314df 100644 --- a/api/python/slint/tests/codegen/examples/counter/README.md +++ b/api/python/slint/tests/codegen/examples/counter/README.md @@ -1,3 +1,5 @@ + + # Counter Example Using slint.codegen This example mirrors the callback pattern described in the diff --git a/api/python/slint/tests/codegen/examples/counter/__init__.py b/api/python/slint/tests/codegen/examples/counter/__init__.py index e69de29bb2d..55c735ca1ff 100644 --- a/api/python/slint/tests/codegen/examples/counter/__init__.py +++ b/api/python/slint/tests/codegen/examples/counter/__init__.py @@ -0,0 +1,2 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 diff --git a/api/python/slint/tests/codegen/examples/counter/counter.slint b/api/python/slint/tests/codegen/examples/counter/counter.slint index 6679facadc8..c28ccb2e8b0 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.slint +++ b/api/python/slint/tests/codegen/examples/counter/counter.slint @@ -1,3 +1,6 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + export component CounterWindow inherits Window { width: 240px; height: 120px; diff --git a/api/python/slint/tests/codegen/examples/counter/generate.py b/api/python/slint/tests/codegen/examples/counter/generate.py index 2f528b66e0a..b1aab4bbfe9 100644 --- a/api/python/slint/tests/codegen/examples/counter/generate.py +++ b/api/python/slint/tests/codegen/examples/counter/generate.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations from pathlib import Path @@ -17,7 +20,9 @@ def main() -> None: quiet=False, ) - generate_project(inputs=[base_dir / "counter.slint"], output_dir=output, config=config) + generate_project( + inputs=[base_dir / "counter.slint"], output_dir=output, config=config + ) print(f"Generated Python bindings into {output.relative_to(base_dir)}") diff --git a/api/python/slint/tests/codegen/examples/counter/main.py b/api/python/slint/tests/codegen/examples/counter/main.py index e53c8d5e732..b5bd0ccf60f 100644 --- a/api/python/slint/tests/codegen/examples/counter/main.py +++ b/api/python/slint/tests/codegen/examples/counter/main.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import slint diff --git a/api/python/slint/tests/codegen/examples/struct_test.slint b/api/python/slint/tests/codegen/examples/struct_test.slint index 5cb22938bfc..e66c7803951 100644 --- a/api/python/slint/tests/codegen/examples/struct_test.slint +++ b/api/python/slint/tests/codegen/examples/struct_test.slint @@ -1,3 +1,6 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + struct Foo { number: int, } diff --git a/api/python/slint/tests/codegen/test_generator.py b/api/python/slint/tests/codegen/test_generator.py index 31a32fdc88e..5c1c7fa214e 100644 --- a/api/python/slint/tests/codegen/test_generator.py +++ b/api/python/slint/tests/codegen/test_generator.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import importlib @@ -197,11 +200,13 @@ def test_generate_optional_type_hints(tmp_path: Path) -> None: def test_cli_main_without_subcommand(tmp_path: Path) -> None: slint_file = _write_slint_fixture(tmp_path) - exit_code = cli_main([ - "--input", - str(slint_file), - "--quiet", - ]) + exit_code = cli_main( + [ + "--input", + str(slint_file), + "--quiet", + ] + ) assert exit_code == 0 assert (slint_file.parent / "app.py").exists() @@ -228,6 +233,7 @@ def test_counter_example_workflow(tmp_path: Path) -> None: app.request_increase() assert app.counter == 1 + def test_parse_library_paths_and_error_handling() -> None: mapping = _parse_library_paths(["std=path/to/std"]) assert mapping == {"std": Path("path/to/std")} diff --git a/api/python/slint/tests/codegen/test_utils.py b/api/python/slint/tests/codegen/test_utils.py index 7041c4309f5..712822964f6 100644 --- a/api/python/slint/tests/codegen/test_utils.py +++ b/api/python/slint/tests/codegen/test_utils.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations from pathlib import Path From b844bf4f11fee49eba94591e89a76b572a940c07 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Fri, 24 Oct 2025 01:17:00 +0800 Subject: [PATCH 03/52] python: fix missing examples.counter.generated --- .../tests/codegen/examples/counter/.gitignore | 1 - .../tests/codegen/examples/counter/counter.py | 40 +++++++++++++++++++ .../codegen/examples/counter/counter.pyi | 16 ++++++++ .../examples/counter/generated/counter.py | 40 +++++++++++++++++++ .../examples/counter/generated/counter.pyi | 16 ++++++++ .../examples/counter/generated/counter.slint | 26 ++++++++++++ 6 files changed, 138 insertions(+), 1 deletion(-) delete mode 100644 api/python/slint/tests/codegen/examples/counter/.gitignore create mode 100644 api/python/slint/tests/codegen/examples/counter/counter.py create mode 100644 api/python/slint/tests/codegen/examples/counter/counter.pyi create mode 100644 api/python/slint/tests/codegen/examples/counter/generated/counter.py create mode 100644 api/python/slint/tests/codegen/examples/counter/generated/counter.pyi create mode 100644 api/python/slint/tests/codegen/examples/counter/generated/counter.slint diff --git a/api/python/slint/tests/codegen/examples/counter/.gitignore b/api/python/slint/tests/codegen/examples/counter/.gitignore deleted file mode 100644 index dc9b2375c7a..00000000000 --- a/api/python/slint/tests/codegen/examples/counter/.gitignore +++ /dev/null @@ -1 +0,0 @@ -generated \ No newline at end of file diff --git a/api/python/slint/tests/codegen/examples/counter/counter.py b/api/python/slint/tests/codegen/examples/counter/counter.py new file mode 100644 index 00000000000..3e66fffc650 --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/counter.py @@ -0,0 +1,40 @@ +# Generated by slint.codegen from counter.slint +from __future__ import annotations + +import importlib.resources as _resources +import os +import types +from contextlib import nullcontext as _nullcontext +from pathlib import Path +from typing import Any + +import slint + +__all__ = ['CounterWindow'] + +_MODULE_DIR = Path(__file__).parent + +_SLINT_RESOURCE = 'counter.slint' + +def _load() -> types.SimpleNamespace: + """Load the compiled Slint module for this package.""" + package = __package__ or (__spec__.parent if __spec__ else None) + if package: + ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE)) + else: + ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE)) + with ctx as slint_path: + include_paths: list[os.PathLike[Any] | Path] = None + library_paths: dict[str, os.PathLike[Any] | Path] | None = None + return slint.load_file( + path=slint_path, + quiet=True, + style=None, + include_paths=include_paths, + library_paths=library_paths, + translation_domain=None, + ) + +_module = _load() + +CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/counter.pyi b/api/python/slint/tests/codegen/examples/counter/counter.pyi new file mode 100644 index 00000000000..cd609c18c1b --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/counter.pyi @@ -0,0 +1,16 @@ +from __future__ import annotations + +import enum + +from typing import Any, Callable + +import slint + +__all__ = ['CounterWindow'] + +class CounterWindow(slint.Component): + def __init__(self, **kwargs: Any) -> None: + ... + counter: int + request_increase: Callable[[], None] + diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.py b/api/python/slint/tests/codegen/examples/counter/generated/counter.py new file mode 100644 index 00000000000..3e66fffc650 --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/generated/counter.py @@ -0,0 +1,40 @@ +# Generated by slint.codegen from counter.slint +from __future__ import annotations + +import importlib.resources as _resources +import os +import types +from contextlib import nullcontext as _nullcontext +from pathlib import Path +from typing import Any + +import slint + +__all__ = ['CounterWindow'] + +_MODULE_DIR = Path(__file__).parent + +_SLINT_RESOURCE = 'counter.slint' + +def _load() -> types.SimpleNamespace: + """Load the compiled Slint module for this package.""" + package = __package__ or (__spec__.parent if __spec__ else None) + if package: + ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE)) + else: + ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE)) + with ctx as slint_path: + include_paths: list[os.PathLike[Any] | Path] = None + library_paths: dict[str, os.PathLike[Any] | Path] | None = None + return slint.load_file( + path=slint_path, + quiet=True, + style=None, + include_paths=include_paths, + library_paths=library_paths, + translation_domain=None, + ) + +_module = _load() + +CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi b/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi new file mode 100644 index 00000000000..cd609c18c1b --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi @@ -0,0 +1,16 @@ +from __future__ import annotations + +import enum + +from typing import Any, Callable + +import slint + +__all__ = ['CounterWindow'] + +class CounterWindow(slint.Component): + def __init__(self, **kwargs: Any) -> None: + ... + counter: int + request_increase: Callable[[], None] + diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.slint b/api/python/slint/tests/codegen/examples/counter/generated/counter.slint new file mode 100644 index 00000000000..6679facadc8 --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/generated/counter.slint @@ -0,0 +1,26 @@ +export component CounterWindow inherits Window { + width: 240px; + height: 120px; + + in-out property counter: 0; + callback request_increase(); + + Rectangle { + width: parent.width; + height: parent.height; + background: #2b2b2b; + border-radius: 12px; + + Text { + text: "Count: " + root.counter; + color: white; + font-size: 24px; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => root.request_increase(); + } + } +} From e91cf74b560a8b4381a1ad6ab40807d5e07e6c84 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:18:48 +0000 Subject: [PATCH 04/52] [autofix.ci] apply automated fixes --- .../slint/tests/codegen/examples/counter/counter.py | 9 +++++++-- .../slint/tests/codegen/examples/counter/counter.pyi | 9 +++++---- .../tests/codegen/examples/counter/generated/counter.py | 9 +++++++-- .../tests/codegen/examples/counter/generated/counter.pyi | 9 +++++---- .../codegen/examples/counter/generated/counter.slint | 3 +++ 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/api/python/slint/tests/codegen/examples/counter/counter.py b/api/python/slint/tests/codegen/examples/counter/counter.py index 3e66fffc650..2740284068f 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.py +++ b/api/python/slint/tests/codegen/examples/counter/counter.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # Generated by slint.codegen from counter.slint from __future__ import annotations @@ -10,11 +13,12 @@ import slint -__all__ = ['CounterWindow'] +__all__ = ["CounterWindow"] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = 'counter.slint' +_SLINT_RESOURCE = "counter.slint" + def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -35,6 +39,7 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) + _module = _load() CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/counter.pyi b/api/python/slint/tests/codegen/examples/counter/counter.pyi index cd609c18c1b..ec9e1bad2d6 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.pyi +++ b/api/python/slint/tests/codegen/examples/counter/counter.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import enum @@ -6,11 +9,9 @@ from typing import Any, Callable import slint -__all__ = ['CounterWindow'] +__all__ = ["CounterWindow"] class CounterWindow(slint.Component): - def __init__(self, **kwargs: Any) -> None: - ... + def __init__(self, **kwargs: Any) -> None: ... counter: int request_increase: Callable[[], None] - diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.py b/api/python/slint/tests/codegen/examples/counter/generated/counter.py index 3e66fffc650..2740284068f 100644 --- a/api/python/slint/tests/codegen/examples/counter/generated/counter.py +++ b/api/python/slint/tests/codegen/examples/counter/generated/counter.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # Generated by slint.codegen from counter.slint from __future__ import annotations @@ -10,11 +13,12 @@ import slint -__all__ = ['CounterWindow'] +__all__ = ["CounterWindow"] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = 'counter.slint' +_SLINT_RESOURCE = "counter.slint" + def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -35,6 +39,7 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) + _module = _load() CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi b/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi index cd609c18c1b..ec9e1bad2d6 100644 --- a/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi +++ b/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import enum @@ -6,11 +9,9 @@ from typing import Any, Callable import slint -__all__ = ['CounterWindow'] +__all__ = ["CounterWindow"] class CounterWindow(slint.Component): - def __init__(self, **kwargs: Any) -> None: - ... + def __init__(self, **kwargs: Any) -> None: ... counter: int request_increase: Callable[[], None] - diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.slint b/api/python/slint/tests/codegen/examples/counter/generated/counter.slint index 6679facadc8..c28ccb2e8b0 100644 --- a/api/python/slint/tests/codegen/examples/counter/generated/counter.slint +++ b/api/python/slint/tests/codegen/examples/counter/generated/counter.slint @@ -1,3 +1,6 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + export component CounterWindow inherits Window { width: 240px; height: 120px; From fd249ad537f721a30c35b7839bc75edeb4908775 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Fri, 24 Oct 2025 01:43:56 +0800 Subject: [PATCH 05/52] refactor: replace normalize_identifier with _normalize_prop and remove utils.py --- api/python/slint/slint/codegen/emitters.py | 22 ++++++++++---------- api/python/slint/slint/codegen/generator.py | 20 +++++++++--------- api/python/slint/slint/codegen/utils.py | 20 ------------------ api/python/slint/tests/codegen/test_utils.py | 18 ---------------- 4 files changed, 21 insertions(+), 59 deletions(-) delete mode 100644 api/python/slint/slint/codegen/utils.py delete mode 100644 api/python/slint/tests/codegen/test_utils.py diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index 7812b766cac..457c80385c4 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -9,14 +9,14 @@ import libcst as cst from .models import GenerationConfig, ModuleArtifacts, CallbackMeta -from .utils import normalize_identifier, path_literal +from .. import _normalize_prop def module_relative_path_expr(module_dir: Path, target: Path) -> str: try: rel = os.path.relpath(target, module_dir) except ValueError: - return f"Path({path_literal(target)})" + return repr(target) if rel in (".", ""): return "_MODULE_DIR" @@ -50,14 +50,14 @@ def _stmt(code: str) -> cst.BaseStatement: include_expr_code = f"[{', '.join(include_exprs)}]" if include_exprs else "None" library_items = [ - f"{path_literal(name)}: {module_relative_path_expr(module_dir, lib_path)}" + f"{repr(Path(name))}: {module_relative_path_expr(module_dir, lib_path)}" for name, lib_path in config.library_paths.items() ] library_expr_code = f"{{{', '.join(library_items)}}}" if library_items else "None" - style_expr = path_literal(config.style) if config.style else "None" + style_expr = repr(Path(config.style)) if config.style else "None" domain_expr = ( - path_literal(config.translation_domain) if config.translation_domain else "None" + repr(Path(config.translation_domain)) if config.translation_domain else "None" ) export_bindings: dict[str, str] = {} @@ -69,7 +69,7 @@ def _stmt(code: str) -> cst.BaseStatement: export_bindings[enum.name] = enum.py_name export_items = list(export_bindings.values()) + [ - normalize_identifier(alias) for _, alias in artifacts.named_exports + _normalize_prop(alias) for _, alias in artifacts.named_exports ] header: list[cst.CSTNode] = [ @@ -152,8 +152,8 @@ def _stmt(code: str) -> cst.BaseStatement: for original, binding in export_bindings.items(): body.append(_stmt(f"{binding} = _module.{original}")) for orig, alias in artifacts.named_exports: - alias_name = normalize_identifier(alias) - target = export_bindings.get(orig, normalize_identifier(orig)) + alias_name = _normalize_prop(alias) + target = export_bindings.get(orig, _normalize_prop(orig)) body.append(_stmt(f"{alias_name} = {target}")) module = cst.Module(body=header + body) # type: ignore[arg-type] @@ -187,7 +187,7 @@ def register_type(type_str: str) -> None: export_names += [struct.py_name for struct in artifacts.structs] export_names += [enum.py_name for enum in artifacts.enums] export_names += [ - normalize_identifier(alias) for _, alias in artifacts.named_exports + _normalize_prop(alias) for _, alias in artifacts.named_exports ] if export_names: all_list = cst.List( @@ -354,8 +354,8 @@ def init_stub() -> cst.FunctionDef: bindings[enum_meta.name] = enum_meta.py_name for orig, alias in artifacts.named_exports: - alias_name = normalize_identifier(alias) - target = bindings.get(orig, normalize_identifier(orig)) + alias_name = _normalize_prop(alias) + target = bindings.get(orig, _normalize_prop(orig)) post_body.append(_stmt(f"{alias_name} = {target}")) post_body.append(cst.EmptyLine()) diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index a63fcc5db63..8e2592664dc 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -22,7 +22,7 @@ StructFieldMeta, StructMeta, ) -from .utils import normalize_identifier +from .. import _normalize_prop if TYPE_CHECKING: from slint.slint import CallbackInfo, FunctionInfo, PyDiagnostic @@ -149,7 +149,7 @@ def _collect_metadata(result: native.CompilationResult) -> ModuleArtifacts: properties.append( PropertyMeta( name=key, - py_name=normalize_identifier(key), + py_name=_normalize_prop(key), type_hint=type_hint, ) ) @@ -171,7 +171,7 @@ def _collect_metadata(result: native.CompilationResult) -> ModuleArtifacts: properties_meta: list[PropertyMeta] = [] for key in comp.global_properties(global_name): - py_key = normalize_identifier(key) + py_key = _normalize_prop(key) info = global_property_info[key] type_hint = info.python_type properties_meta.append( @@ -195,7 +195,7 @@ def _collect_metadata(result: native.CompilationResult) -> ModuleArtifacts: globals_meta.append( GlobalMeta( name=global_name, - py_name=normalize_identifier(global_name), + py_name=_normalize_prop(global_name), properties=properties_meta, callbacks=callbacks_meta, functions=functions_meta, @@ -205,7 +205,7 @@ def _collect_metadata(result: native.CompilationResult) -> ModuleArtifacts: components.append( ComponentMeta( name=name, - py_name=normalize_identifier(name), + py_name=_normalize_prop(name), properties=properties, callbacks=callbacks, functions=functions, @@ -223,14 +223,14 @@ def _collect_metadata(result: native.CompilationResult) -> ModuleArtifacts: fields.append( StructFieldMeta( name=field_name, - py_name=normalize_identifier(field_name), + py_name=_normalize_prop(field_name), type_hint=_python_value_hint(value), ) ) structs_meta.append( StructMeta( name=struct_name, - py_name=normalize_identifier(struct_name), + py_name=_normalize_prop(struct_name), fields=fields, ) ) @@ -241,14 +241,14 @@ def _collect_metadata(result: native.CompilationResult) -> ModuleArtifacts: values.append( EnumValueMeta( name=member, - py_name=normalize_identifier(member), + py_name=_normalize_prop(member), value=enum_member.name, ) ) enums_meta.append( EnumMeta( name=enum_name, - py_name=normalize_identifier(enum_name), + py_name=_normalize_prop(enum_name), values=values, ) ) @@ -285,7 +285,7 @@ def _python_value_hint(value: object) -> str: def _callback_meta(name: str, info: CallbackInfo | FunctionInfo) -> CallbackMeta: return CallbackMeta( name=name, - py_name=normalize_identifier(name), + py_name=_normalize_prop(name), arg_types=[param.python_type for param in info.parameters], return_type=info.return_type, ) diff --git a/api/python/slint/slint/codegen/utils.py b/api/python/slint/slint/codegen/utils.py deleted file mode 100644 index d81905220ee..00000000000 --- a/api/python/slint/slint/codegen/utils.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -from __future__ import annotations - -from pathlib import Path - - -def normalize_identifier(identifier: str) -> str: - identifier = identifier.replace("-", "_") - if identifier[0].isdigit(): - identifier = f"_{identifier}" - return identifier - - -def path_literal(value: str | Path) -> str: - return repr(str(value)) - - -__all__ = ["normalize_identifier", "path_literal"] diff --git a/api/python/slint/tests/codegen/test_utils.py b/api/python/slint/tests/codegen/test_utils.py deleted file mode 100644 index 712822964f6..00000000000 --- a/api/python/slint/tests/codegen/test_utils.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -from __future__ import annotations - -from pathlib import Path - -from slint.codegen.utils import normalize_identifier, path_literal - - -def test_normalize_identifier() -> None: - assert normalize_identifier("foo-bar") == "foo_bar" - assert normalize_identifier("1value") == "_1value" - - -def test_path_literal() -> None: - path = Path("/tmp/demo") - assert path_literal(path) == repr(str(path)) From 646e92e318b0ef2ef6ff453246613af967427fdd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:50:17 +0000 Subject: [PATCH 06/52] [autofix.ci] apply automated fixes --- api/python/slint/slint/codegen/emitters.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index 457c80385c4..93159f50325 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -186,9 +186,7 @@ def register_type(type_str: str) -> None: export_names = [component.py_name for component in artifacts.components] export_names += [struct.py_name for struct in artifacts.structs] export_names += [enum.py_name for enum in artifacts.enums] - export_names += [ - _normalize_prop(alias) for _, alias in artifacts.named_exports - ] + export_names += [_normalize_prop(alias) for _, alias in artifacts.named_exports] if export_names: all_list = cst.List( elements=[ From 6b0dab90b99766e5554ecc9cc457003a2a2ee6b0 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Fri, 24 Oct 2025 02:20:59 +0800 Subject: [PATCH 07/52] fix: python codegen circual import --- api/python/slint/slint/codegen/__main__.py | 1 - api/python/slint/slint/codegen/emitters.py | 2 +- api/python/slint/slint/codegen/generator.py | 6 +++--- api/python/slint/slint/codegen/models.py | 2 +- api/python/slint/tests/codegen/test_generator.py | 12 ++++++------ 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/api/python/slint/slint/codegen/__main__.py b/api/python/slint/slint/codegen/__main__.py index df59de77810..28b0ab52cdc 100644 --- a/api/python/slint/slint/codegen/__main__.py +++ b/api/python/slint/slint/codegen/__main__.py @@ -7,6 +7,5 @@ from .cli import main - if __name__ == "__main__": # pragma: no cover - CLI entry point sys.exit(main()) diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index 93159f50325..d3dee56ccc3 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -8,8 +8,8 @@ import libcst as cst -from .models import GenerationConfig, ModuleArtifacts, CallbackMeta from .. import _normalize_prop +from .models import CallbackMeta, GenerationConfig, ModuleArtifacts def module_relative_path_expr(module_dir: Path, target: Path) -> str: diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index 8e2592664dc..ae5e3c9ce7b 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -5,9 +5,10 @@ import shutil from pathlib import Path -from typing import Iterable, TYPE_CHECKING +from typing import TYPE_CHECKING, Iterable -from slint import slint as native +from .. import slint as native +from .. import _normalize_prop from .emitters import write_python_module, write_stub_module from .models import ( @@ -22,7 +23,6 @@ StructFieldMeta, StructMeta, ) -from .. import _normalize_prop if TYPE_CHECKING: from slint.slint import CallbackInfo, FunctionInfo, PyDiagnostic diff --git a/api/python/slint/slint/codegen/models.py b/api/python/slint/slint/codegen/models.py index c92819b8c1f..a4e37b7bfcf 100644 --- a/api/python/slint/slint/codegen/models.py +++ b/api/python/slint/slint/codegen/models.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from pathlib import Path -from typing import List, Dict +from typing import Dict, List @dataclass(slots=True) diff --git a/api/python/slint/tests/codegen/test_generator.py b/api/python/slint/tests/codegen/test_generator.py index 5c1c7fa214e..56a4c6646aa 100644 --- a/api/python/slint/tests/codegen/test_generator.py +++ b/api/python/slint/tests/codegen/test_generator.py @@ -5,21 +5,21 @@ import importlib import importlib.util +import inspect +import shutil import subprocess import sys -import textwrap -import shutil from pathlib import Path import pytest - -from slint.codegen.cli import _parse_library_paths, main as cli_main +from slint.codegen.cli import _parse_library_paths +from slint.codegen.cli import main as cli_main from slint.codegen.generator import generate_project from slint.codegen.models import GenerationConfig def _write_slint_fixture(target_dir: Path) -> Path: - source = textwrap.dedent( + source = inspect.cleandoc( """ export struct Config { value: int, @@ -52,7 +52,7 @@ def _write_slint_fixture(target_dir: Path) -> Path: } } """ - ).strip() + ) slint_dir = target_dir / "ui" slint_dir.mkdir(parents=True, exist_ok=True) From ed7b1baebc31de3fb0628eb6f87b41c6b8472a5e Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Fri, 24 Oct 2025 04:04:39 +0800 Subject: [PATCH 08/52] fix: ambignous `slint.slint` now `slint.core` via workable maturin configuration --- api/python/slint/Cargo.toml | 3 -- api/python/slint/lib.rs | 2 +- api/python/slint/pyproject.toml | 4 +++ api/python/slint/slint/__init__.py | 30 +++++++++---------- api/python/slint/slint/codegen/generator.py | 18 +++++------ .../slint/slint/{slint.pyi => core.pyi} | 0 api/python/slint/slint/loop.py | 18 +++++------ api/python/slint/slint/models.py | 4 +-- api/python/slint/tests/test_async.py | 6 ++-- api/python/slint/tests/test_compiler.py | 10 +++---- api/python/slint/tests/test_gc.py | 16 +++++----- api/python/slint/tests/test_instance.py | 10 +++---- .../tests/test_invoke_from_event_loop.py | 14 ++++----- api/python/slint/tests/test_loop.py | 4 +-- api/python/slint/tests/test_models.py | 10 +++---- api/python/slint/tests/test_timers.py | 14 ++++----- 16 files changed, 82 insertions(+), 81 deletions(-) rename api/python/slint/slint/{slint.pyi => core.pyi} (100%) diff --git a/api/python/slint/Cargo.toml b/api/python/slint/Cargo.toml index 5aebefa7f7f..e4545fa16bf 100644 --- a/api/python/slint/Cargo.toml +++ b/api/python/slint/Cargo.toml @@ -54,6 +54,3 @@ css-color-parser2 = { workspace = true } pyo3-stub-gen = { version = "0.9.0", default-features = false } smol = { version = "2.0.0" } smol_str = { workspace = true } - -[package.metadata.maturin] -python-source = "slint" diff --git a/api/python/slint/lib.rs b/api/python/slint/lib.rs index a16763c625b..05643469124 100644 --- a/api/python/slint/lib.rs +++ b/api/python/slint/lib.rs @@ -158,7 +158,7 @@ impl Translator for PyGettextTranslator { use pyo3::prelude::*; -#[pymodule] +#[pymodule(name = "core")] fn slint(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { i_slint_backend_selector::with_platform(|_b| { // Nothing to do, just make sure a backend was created diff --git a/api/python/slint/pyproject.toml b/api/python/slint/pyproject.toml index 506f88b9b6e..bacb7a895e1 100644 --- a/api/python/slint/pyproject.toml +++ b/api/python/slint/pyproject.toml @@ -51,6 +51,10 @@ dev = [ "aiohttp>=3.12.15", ] +[tool.maturin] +module-name = "slint.core" +python-packages = ["slint"] + [tool.uv] # Rebuild package when any rust files change cache-keys = [{ file = "pyproject.toml" }, { file = "Cargo.toml" }, { file = "**/*.rs" }] diff --git a/api/python/slint/slint/__init__.py b/api/python/slint/slint/__init__.py index f3b9b9ef3a1..86424e8f2a0 100644 --- a/api/python/slint/slint/__init__.py +++ b/api/python/slint/slint/__init__.py @@ -7,7 +7,7 @@ import os import sys -from . import slint as native +from . import core import types import logging import copy @@ -15,24 +15,24 @@ from typing import Any, Callable, TypeVar, overload import pathlib from .models import ListModel, Model -from .slint import Image, Color, Brush, Timer, TimerMode +from .core import Image, Color, Brush, Timer, TimerMode from .loop import SlintEventLoop from pathlib import Path from collections.abc import Coroutine import asyncio import gettext -Struct = native.PyStruct +Struct = core.PyStruct class CompileError(Exception): message: str """The error message that produced this compile error.""" - diagnostics: list[native.PyDiagnostic] + diagnostics: list[core.PyDiagnostic] """A list of detailed diagnostics that were produced as part of the compilation.""" - def __init__(self, message: str, diagnostics: list[native.PyDiagnostic]): + def __init__(self, message: str, diagnostics: list[core.PyDiagnostic]): """@private""" super().__init__(message) self.message = message @@ -45,7 +45,7 @@ class Component: """Component is the base class for all instances of Slint components. Use the member functions to show or hide the window, or spin the event loop.""" - __instance__: native.ComponentInstance + __instance__: core.ComponentInstance def show(self) -> None: """Shows the window on the screen.""" @@ -68,7 +68,7 @@ def _normalize_prop(name: str) -> str: return name.replace("-", "_") -def _build_global_class(compdef: native.ComponentDefinition, global_name: str) -> Any: +def _build_global_class(compdef: core.ComponentDefinition, global_name: str) -> Any: properties_and_callbacks = {} for prop_name in compdef.global_properties(global_name).keys(): @@ -139,7 +139,7 @@ def call(*args: Any) -> Any: def _build_class( - compdef: native.ComponentDefinition, + compdef: core.ComponentDefinition, ) -> typing.Callable[..., Component]: def cls_init(self: Component, **kwargs: Any) -> Any: self.__instance__ = compdef.create() @@ -252,8 +252,8 @@ def global_getter(self: Component) -> Any: return type("SlintClassWrapper", (Component,), properties_and_callbacks) -def _build_struct(name: str, struct_prototype: native.PyStruct) -> type: - def new_struct(cls: Any, *args: Any, **kwargs: Any) -> native.PyStruct: +def _build_struct(name: str, struct_prototype: core.PyStruct) -> type: + def new_struct(cls: Any, *args: Any, **kwargs: Any) -> core.PyStruct: inst = copy.copy(struct_prototype) for prop, val in kwargs.items(): @@ -291,7 +291,7 @@ def load_file( """ - compiler = native.Compiler() + compiler = core.Compiler() if style is not None: compiler.style = style @@ -308,11 +308,11 @@ def load_file( if diagnostics: if not quiet: for diag in diagnostics: - if diag.level == native.DiagnosticLevel.Warning: + if diag.level == core.DiagnosticLevel.Warning: logging.warning(diag) errors = [ - diag for diag in diagnostics if diag.level == native.DiagnosticLevel.Error + diag for diag in diagnostics if diag.level == core.DiagnosticLevel.Error ] if errors: raise CompileError(f"Could not compile {path}", diagnostics) @@ -492,7 +492,7 @@ def set_xdg_app_id(app_id: str) -> None: """Sets the application id for use on Wayland or X11 with [xdg](https://specifications.freedesktop.org/desktop-entry-spec/latest/) compliant window managers. This id must be set before the window is shown; it only applies to Wayland or X11.""" - native.set_xdg_app_id(app_id) + core.set_xdg_app_id(app_id) quit_event = asyncio.Event() @@ -573,7 +573,7 @@ def init_translations(translations: typing.Optional[gettext.GNUTranslations]) -> pass ``` """ - native.init_translations(translations) + core.init_translations(translations) __all__ = [ diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index ae5e3c9ce7b..08fdfcdf0a0 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Iterable -from .. import slint as native +from .. import core from .. import _normalize_prop from .emitters import write_python_module, write_stub_module @@ -25,7 +25,7 @@ ) if TYPE_CHECKING: - from slint.slint import CallbackInfo, FunctionInfo, PyDiagnostic + from slint.core import CallbackInfo, FunctionInfo, PyDiagnostic def generate_project( @@ -42,7 +42,7 @@ def generate_project( if output_dir is not None: output_dir.mkdir(parents=True, exist_ok=True) - compiler = native.Compiler() + compiler = core.Compiler() if config.style: compiler.style = config.style if config.include_paths: @@ -102,14 +102,14 @@ def _discover_slint_files(inputs: Iterable[Path]) -> Iterable[tuple[Path, Path]] def _compile_slint( - compiler: native.Compiler, + compiler: core.Compiler, source_path: Path, config: GenerationConfig, -) -> native.CompilationResult | None: +) -> core.CompilationResult | None: result = compiler.build_from_path(source_path) diagnostics = result.diagnostics - diagnostic_error = getattr(native, "DiagnosticLevel", None) + diagnostic_error = getattr(core, "DiagnosticLevel", None) error_enum = getattr(diagnostic_error, "Error", None) def is_error(diag: PyDiagnostic) -> bool: @@ -133,7 +133,7 @@ def is_error(diag: PyDiagnostic) -> bool: return result -def _collect_metadata(result: native.CompilationResult) -> ModuleArtifacts: +def _collect_metadata(result: core.CompilationResult) -> ModuleArtifacts: components: list[ComponentMeta] = [] for name in result.component_names: comp = result.component(name) @@ -275,9 +275,9 @@ def _python_value_hint(value: object) -> str: return "float" if isinstance(value, str): return "str" - if isinstance(value, native.Image): + if isinstance(value, core.Image): return "slint.Image" - if isinstance(value, native.Brush): + if isinstance(value, core.Brush): return "slint.Brush" return "Any" diff --git a/api/python/slint/slint/slint.pyi b/api/python/slint/slint/core.pyi similarity index 100% rename from api/python/slint/slint/slint.pyi rename to api/python/slint/slint/core.pyi diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index b6a0e5bd39c..da7442a0993 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -1,7 +1,7 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from . import slint as native +from . import core import asyncio.selector_events import asyncio import asyncio.events @@ -44,7 +44,7 @@ class _SlintSelector(selectors.BaseSelector): def __init__(self) -> None: self.fd_to_selector_key: typing.Dict[typing.Any, selectors.SelectorKey] = {} self.mapping = _SlintSelectorMapping(self) - self.adapters: typing.Dict[int, native.AsyncAdapter] = {} + self.adapters: typing.Dict[int, core.AsyncAdapter] = {} def register( self, fileobj: typing.Any, events: typing.Any, data: typing.Any = None @@ -53,7 +53,7 @@ def register( key = selectors.SelectorKey(fileobj, fd, events, data) self.fd_to_selector_key[fd] = key - adapter = native.AsyncAdapter(fd) + adapter = core.AsyncAdapter(fd) self.adapters[fd] = adapter if events & selectors.EVENT_READ: @@ -114,7 +114,7 @@ def write_notify(self, fd: int) -> None: class SlintEventLoop(asyncio.SelectorEventLoop): def __init__(self) -> None: self._is_running = False - self._timers: typing.Set[native.Timer] = set() + self._timers: typing.Set[core.Timer] = set() self.stop_run_forever_event = asyncio.Event() self._soon_tasks: typing.List[asyncio.TimerHandle] = [] super().__init__(_SlintSelector()) @@ -122,14 +122,14 @@ def __init__(self) -> None: def run_forever(self) -> None: async def loop_stopper(event: asyncio.Event) -> None: await event.wait() - native.quit_event_loop() + core.quit_event_loop() asyncio.events._set_running_loop(self) self._is_running = True try: self.stop_run_forever_event = asyncio.Event() self.create_task(loop_stopper(self.stop_run_forever_event)) - native.run_event_loop() + core.run_event_loop() finally: self._is_running = False asyncio.events._set_running_loop(None) @@ -178,7 +178,7 @@ def is_closed(self) -> bool: return False def call_later(self, delay, callback, *args, context=None) -> asyncio.TimerHandle: # type: ignore - timer = native.Timer() + timer = core.Timer() handle = asyncio.TimerHandle( when=self.time() + delay, @@ -196,7 +196,7 @@ def timer_done_cb() -> None: handle._run() timer.start( - native.TimerMode.SingleShot, + core.TimerMode.SingleShot, interval=datetime.timedelta(seconds=delay), callback=timer_done_cb, ) @@ -237,7 +237,7 @@ def run_handle_cb() -> None: if not handle._cancelled: handle._run() - native.invoke_from_event_loop(run_handle_cb) + core.invoke_from_event_loop(run_handle_cb) return handle def _write_to_self(self) -> None: diff --git a/api/python/slint/slint/models.py b/api/python/slint/slint/models.py index 5e583a86c6f..7093cb6f560 100644 --- a/api/python/slint/slint/models.py +++ b/api/python/slint/slint/models.py @@ -1,14 +1,14 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from . import slint as native +from . import core from collections.abc import Iterable from abc import abstractmethod import typing from typing import Any, cast, Iterator -class Model[T](native.PyModelBase, Iterable[T]): +class Model[T](core.PyModelBase, Iterable[T]): """Model is the base class for feeding dynamic data into Slint views. Subclass Model to implement your own models, or use `ListModel` to wrap a list. diff --git a/api/python/slint/tests/test_async.py b/api/python/slint/tests/test_async.py index 90260f826fa..116b9780927 100644 --- a/api/python/slint/tests/test_async.py +++ b/api/python/slint/tests/test_async.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 import slint -from slint import slint as native +from slint import core import asyncio import typing import aiohttp @@ -166,11 +166,11 @@ async def run_server_and_client(exception_check: typing.List[Exception]) -> None def test_loop_close_while_main_future_runs() -> None: def q() -> None: - native.quit_event_loop() + core.quit_event_loop() async def never_quit() -> None: loop = asyncio.get_running_loop() - # Call native.quit_event_loop() directly as if the user closed the last window. We should gracefully + # Call core.quit_event_loop() directly as if the user closed the last window. We should gracefully # handle that the future that this function represents isn't terminated. loop.call_later(0.1, q) while True: diff --git a/api/python/slint/tests/test_compiler.py b/api/python/slint/tests/test_compiler.py index 93114899a83..f41f869916d 100644 --- a/api/python/slint/tests/test_compiler.py +++ b/api/python/slint/tests/test_compiler.py @@ -1,13 +1,13 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from slint import slint as native -from slint.slint import ValueType +from slint import core +from slint.core import ValueType from pathlib import Path def test_basic_compiler() -> None: - compiler = native.Compiler() + compiler = core.Compiler() assert compiler.include_paths == [] compiler.include_paths = [Path("testing")] @@ -79,7 +79,7 @@ def test_basic_compiler() -> None: def test_compiler_build_from_path() -> None: - compiler = native.Compiler() + compiler = core.Compiler() result = compiler.build_from_path(Path("Nonexistent.slint")) assert len(result.component_names) == 0 @@ -87,5 +87,5 @@ def test_compiler_build_from_path() -> None: diags = result.diagnostics assert len(diags) == 1 - assert diags[0].level == native.DiagnosticLevel.Error + assert diags[0].level == core.DiagnosticLevel.Error assert diags[0].message.startswith("Could not load Nonexistent.slint:") diff --git a/api/python/slint/tests/test_gc.py b/api/python/slint/tests/test_gc.py index f7ce7451cf1..a333c527b6e 100644 --- a/api/python/slint/tests/test_gc.py +++ b/api/python/slint/tests/test_gc.py @@ -1,7 +1,7 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from slint import slint as native +from slint import core import slint import weakref import gc @@ -10,7 +10,7 @@ def test_callback_gc() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ @@ -23,11 +23,11 @@ def test_callback_gc() -> None: ).component("Test") assert compdef is not None - instance: native.ComponentInstance | None = compdef.create() + instance: core.ComponentInstance | None = compdef.create() assert instance is not None class Handler: - def __init__(self, instance: native.ComponentInstance) -> None: + def __init__(self, instance: core.ComponentInstance) -> None: self.instance = instance def python_callback(self, input: str) -> str: @@ -48,7 +48,7 @@ def python_callback(self, input: str) -> str: def test_struct_gc() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ @@ -63,7 +63,7 @@ def test_struct_gc() -> None: ).component("Test") assert compdef is not None - instance: native.ComponentInstance | None = compdef.create() + instance: core.ComponentInstance | None = compdef.create() assert instance is not None model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) @@ -79,7 +79,7 @@ def test_struct_gc() -> None: def test_properties_gc() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ @@ -91,7 +91,7 @@ def test_properties_gc() -> None: ).component("Test") assert compdef is not None - instance: native.ComponentInstance | None = compdef.create() + instance: core.ComponentInstance | None = compdef.create() assert instance is not None model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) diff --git a/api/python/slint/tests/test_instance.py b/api/python/slint/tests/test_instance.py index e1087929f24..042a34a2130 100644 --- a/api/python/slint/tests/test_instance.py +++ b/api/python/slint/tests/test_instance.py @@ -2,14 +2,14 @@ # 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 slint as native -from slint.slint import Image, Color, Brush +from slint import core +from slint.core import Image, Color, Brush import os from pathlib import Path def test_property_access() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ @@ -78,7 +78,7 @@ def test_property_access() -> None: instance.set_property("boolprop", 0) structval = instance.get_property("structprop") - assert isinstance(structval, native.PyStruct) + assert isinstance(structval, core.PyStruct) assert structval.title == "builtin" assert structval.finished assert structval.dash_prop @@ -139,7 +139,7 @@ def test_property_access() -> None: def test_callbacks() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ diff --git a/api/python/slint/tests/test_invoke_from_event_loop.py b/api/python/slint/tests/test_invoke_from_event_loop.py index 4f021c8950e..5aa716f9bd6 100644 --- a/api/python/slint/tests/test_invoke_from_event_loop.py +++ b/api/python/slint/tests/test_invoke_from_event_loop.py @@ -1,7 +1,7 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from slint import slint as native +from slint import core import threading from datetime import timedelta @@ -15,18 +15,18 @@ def test_threads() -> None: def invoked_from_event_loop() -> None: global was_here was_here = True - native.quit_event_loop() + core.quit_event_loop() def quit() -> None: - native.invoke_from_event_loop(invoked_from_event_loop) + core.invoke_from_event_loop(invoked_from_event_loop) thr = threading.Thread(target=quit) - native.Timer.single_shot(timedelta(milliseconds=10), lambda: thr.start()) - fallback_timer = native.Timer() + core.Timer.single_shot(timedelta(milliseconds=10), lambda: thr.start()) + fallback_timer = core.Timer() fallback_timer.start( - native.TimerMode.Repeated, timedelta(milliseconds=100), native.quit_event_loop + core.TimerMode.Repeated, timedelta(milliseconds=100), core.quit_event_loop ) - native.run_event_loop() + core.run_event_loop() thr.join() fallback_timer.stop() assert was_here diff --git a/api/python/slint/tests/test_loop.py b/api/python/slint/tests/test_loop.py index 603aea7a389..2f7621a0368 100644 --- a/api/python/slint/tests/test_loop.py +++ b/api/python/slint/tests/test_loop.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 import slint -from slint import slint as native +from slint import core from datetime import timedelta import pytest import sys @@ -14,7 +14,7 @@ def call_sys_exit() -> None: slint.Timer.single_shot(timedelta(milliseconds=100), call_sys_exit) with pytest.raises(SystemExit) as exc_info: - native.run_event_loop() + core.run_event_loop() assert ( "unexpected failure running python singleshot timer callback" in exc_info.value.__notes__ diff --git a/api/python/slint/tests/test_models.py b/api/python/slint/tests/test_models.py index 7b3c2e7a60f..651d0bf5c7c 100644 --- a/api/python/slint/tests/test_models.py +++ b/api/python/slint/tests/test_models.py @@ -1,14 +1,14 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from slint import slint as native +from slint import core from slint import models as models import typing from pathlib import Path def test_model_notify() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ @@ -55,7 +55,7 @@ def test_model_notify() -> None: def test_model_from_list() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ @@ -103,7 +103,7 @@ def test_generator(max: int) -> typing.Iterator[int]: def test_rust_model_sequence() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ @@ -126,7 +126,7 @@ def test_rust_model_sequence() -> None: def test_model_writeback() -> None: - compiler = native.Compiler() + compiler = core.Compiler() compdef = compiler.build_from_source( """ diff --git a/api/python/slint/tests/test_timers.py b/api/python/slint/tests/test_timers.py index 08441e59192..d0b06265b51 100644 --- a/api/python/slint/tests/test_timers.py +++ b/api/python/slint/tests/test_timers.py @@ -1,7 +1,7 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from slint import slint as native +from slint import core from datetime import timedelta counter: int @@ -15,19 +15,19 @@ def quit_after_two_invocations() -> None: global counter counter = min(counter + 1, 2) if counter == 2: - native.quit_event_loop() + core.quit_event_loop() - test_timer = native.Timer() + test_timer = core.Timer() test_timer.start( - native.TimerMode.Repeated, + core.TimerMode.Repeated, timedelta(milliseconds=100), quit_after_two_invocations, ) - native.run_event_loop() + core.run_event_loop() test_timer.stop() assert counter == 2 def test_single_shot() -> None: - native.Timer.single_shot(timedelta(milliseconds=100), native.quit_event_loop) - native.run_event_loop() + core.Timer.single_shot(timedelta(milliseconds=100), core.quit_event_loop) + core.run_event_loop() From f8ece22260d7c44ad0b0420c7999df6aca8898de Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Fri, 24 Oct 2025 04:16:33 +0800 Subject: [PATCH 09/52] fix: cleanup slint.codegen.__init__ --- api/python/slint/slint/codegen/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/api/python/slint/slint/codegen/__init__.py b/api/python/slint/slint/codegen/__init__.py index e3afd308114..e69de29bb2d 100644 --- a/api/python/slint/slint/codegen/__init__.py +++ b/api/python/slint/slint/codegen/__init__.py @@ -1,8 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -from __future__ import annotations - -from .cli import main - -__all__ = ["main"] From 6adb9de1fe16771feccf9004d5bc11ea9a5f7cc2 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Fri, 24 Oct 2025 13:29:52 +0800 Subject: [PATCH 10/52] Refactor python binding module structure - Drop nox cause we only rely on uv and maturin - Also drop unnessesary optional-dependencies group - Make ruff & mypy happy - `slint.{slint => core}`, `slint.{__init__ => api}` --- .github/workflows/ci.yaml | 5 +- api/python/slint/Cargo.toml | 1 + api/python/slint/lib.rs | 2 +- api/python/slint/noxfile.py | 11 - api/python/slint/pyproject.toml | 5 +- api/python/slint/slint/__init__.py | 590 +---------------- api/python/slint/slint/api.py | 607 ++++++++++++++++++ api/python/slint/slint/codegen/__init__.py | 2 + api/python/slint/slint/codegen/emitters.py | 4 +- api/python/slint/slint/codegen/generator.py | 37 +- api/python/slint/slint/core.pyi | 6 +- api/python/slint/slint/loop.py | 7 +- api/python/slint/slint/models.py | 9 +- .../codegen/examples/counter/counter.pyi | 2 - .../examples/counter/generated/counter.pyi | 2 - .../slint/tests/codegen/test_generator.py | 5 +- 16 files changed, 668 insertions(+), 627 deletions(-) delete mode 100644 api/python/slint/noxfile.py create mode 100644 api/python/slint/slint/api.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fa517b98f1e..0c727114323 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -203,10 +203,11 @@ jobs: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v7 - - uses: fjwillemsen/setup-nox2@v3.0.0 - name: Run python tests working-directory: api/python/slint - run: nox --default-venv-backend uv + run: uv run pytest tests + env: + MATURIN_PEP517_ARGS: --profile=dev - name: Run mypy working-directory: api/python/slint run: uv run mypy -p tests -p slint diff --git a/api/python/slint/Cargo.toml b/api/python/slint/Cargo.toml index e4545fa16bf..53d03ab8e90 100644 --- a/api/python/slint/Cargo.toml +++ b/api/python/slint/Cargo.toml @@ -14,6 +14,7 @@ publish = false rust-version.workspace = true [lib] +name = "core" path = "lib.rs" crate-type = ["cdylib", "rlib"] diff --git a/api/python/slint/lib.rs b/api/python/slint/lib.rs index 05643469124..8a662559ede 100644 --- a/api/python/slint/lib.rs +++ b/api/python/slint/lib.rs @@ -159,7 +159,7 @@ impl Translator for PyGettextTranslator { use pyo3::prelude::*; #[pymodule(name = "core")] -fn slint(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +fn slint_core(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { i_slint_backend_selector::with_platform(|_b| { // Nothing to do, just make sure a backend was created Ok(()) diff --git a/api/python/slint/noxfile.py b/api/python/slint/noxfile.py deleted file mode 100644 index 078dc8cd118..00000000000 --- a/api/python/slint/noxfile.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -import nox - - -@nox.session(python="3.12") -def python(session: nox.Session): - session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" - session.install(".[dev]") - session.run("pytest", "-s", "-v") diff --git a/api/python/slint/pyproject.toml b/api/python/slint/pyproject.toml index bacb7a895e1..b8bc6821b60 100644 --- a/api/python/slint/pyproject.toml +++ b/api/python/slint/pyproject.toml @@ -36,9 +36,6 @@ Repository = "https://github.com/slint-ui/slint" Changelog = "https://github.com/slint-ui/slint/blob/master/CHANGELOG.md" Tracker = "https://github.com/slint-ui/slint/issues" -[project.optional-dependencies] -dev = ["pytest", "numpy>=2.3.2", "pillow>=11.3.0", "aiohttp>=3.12.15"] - [dependency-groups] dev = [ "mypy>=1.15.0", @@ -49,11 +46,13 @@ dev = [ "pillow>=11.3.0", "numpy>=2.3.2", "aiohttp>=3.12.15", + "maturin>=1.9.6", ] [tool.maturin] module-name = "slint.core" python-packages = ["slint"] +features = ["pyo3/extension-module"] [tool.uv] # Rebuild package when any rust files change diff --git a/api/python/slint/slint/__init__.py b/api/python/slint/slint/__init__.py index 86424e8f2a0..881e593459b 100644 --- a/api/python/slint/slint/__init__.py +++ b/api/python/slint/slint/__init__.py @@ -1,580 +1,22 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -r""" -.. include:: ../README.md -""" - -import os -import sys -from . import core -import types -import logging -import copy -import typing -from typing import Any, Callable, TypeVar, overload -import pathlib -from .models import ListModel, Model -from .core import Image, Color, Brush, Timer, TimerMode -from .loop import SlintEventLoop -from pathlib import Path -from collections.abc import Coroutine -import asyncio -import gettext - -Struct = core.PyStruct - - -class CompileError(Exception): - message: str - """The error message that produced this compile error.""" - - diagnostics: list[core.PyDiagnostic] - """A list of detailed diagnostics that were produced as part of the compilation.""" - - def __init__(self, message: str, diagnostics: list[core.PyDiagnostic]): - """@private""" - super().__init__(message) - self.message = message - self.diagnostics = diagnostics - for diag in self.diagnostics: - self.add_note(str(diag)) - - -class Component: - """Component is the base class for all instances of Slint components. Use the member functions to show or hide the - window, or spin the event loop.""" - - __instance__: core.ComponentInstance - - def show(self) -> None: - """Shows the window on the screen.""" - - self.__instance__.show() - - def hide(self) -> None: - """Hides the window from the screen.""" - - self.__instance__.hide() - - def run(self) -> None: - """Shows the window, runs the event loop, hides it when the loop is quit, and returns.""" - self.show() - run_event_loop() - self.hide() - - -def _normalize_prop(name: str) -> str: - return name.replace("-", "_") - - -def _build_global_class(compdef: core.ComponentDefinition, global_name: str) -> Any: - properties_and_callbacks = {} - - for prop_name in compdef.global_properties(global_name).keys(): - python_prop = _normalize_prop(prop_name) - if python_prop in properties_and_callbacks: - logging.warning(f"Duplicated property {prop_name}") - continue - - def mk_setter_getter(prop_or_callback_name: str) -> property: - def getter(self: Component) -> Any: - return self.__instance__.get_global_property( - global_name, prop_or_callback_name - ) - - def setter(self: Component, value: Any) -> None: - self.__instance__.set_global_property( - global_name, prop_or_callback_name, value - ) - - return property(getter, setter) - - properties_and_callbacks[python_prop] = mk_setter_getter(prop_name) - - for callback_name in compdef.global_callbacks(global_name): - python_prop = _normalize_prop(callback_name) - if python_prop in properties_and_callbacks: - logging.warning(f"Duplicated property {prop_name}") - continue - - def mk_setter_getter(prop_or_callback_name: str) -> property: - def getter(self: Component) -> typing.Callable[..., Any]: - def call(*args: Any) -> Any: - return self.__instance__.invoke_global( - global_name, prop_or_callback_name, *args - ) - - return call - - def setter(self: Component, value: typing.Callable[..., Any]) -> None: - self.__instance__.set_global_callback( - global_name, prop_or_callback_name, value - ) - - return property(getter, setter) - - properties_and_callbacks[python_prop] = mk_setter_getter(callback_name) - - for function_name in compdef.global_functions(global_name): - python_prop = _normalize_prop(function_name) - if python_prop in properties_and_callbacks: - logging.warning(f"Duplicated function {prop_name}") - continue - - def mk_getter(function_name: str) -> property: - def getter(self: Component) -> typing.Callable[..., Any]: - def call(*args: Any) -> Any: - return self.__instance__.invoke_global( - global_name, function_name, *args - ) - - return call - - return property(getter) - - properties_and_callbacks[python_prop] = mk_getter(function_name) - - return type("SlintGlobalClassWrapper", (), properties_and_callbacks) - - -def _build_class( - compdef: core.ComponentDefinition, -) -> typing.Callable[..., Component]: - def cls_init(self: Component, **kwargs: Any) -> Any: - self.__instance__ = compdef.create() - for name, value in self.__class__.__dict__.items(): - if hasattr(value, "slint.callback"): - callback_info = getattr(value, "slint.callback") - name = callback_info["name"] - - is_async = getattr(value, "slint.async", False) - if is_async: - if "global_name" in callback_info: - global_name = callback_info["global_name"] - if not compdef.global_callback_returns_void(global_name, name): - raise RuntimeError( - f"Callback '{name}' in global '{global_name}' cannot be used with a callback decorator for an async function, as it doesn't return void" - ) - else: - if not compdef.callback_returns_void(name): - raise RuntimeError( - f"Callback '{name}' cannot be used with a callback decorator for an async function, as it doesn't return void" - ) - - def mk_callback( - self: Any, callback: typing.Callable[..., Any] - ) -> typing.Callable[..., Any]: - def invoke(*args: Any, **kwargs: Any) -> Any: - return callback(self, *args, **kwargs) - - return invoke - - if "global_name" in callback_info: - self.__instance__.set_global_callback( - callback_info["global_name"], name, mk_callback(self, value) - ) - else: - self.__instance__.set_callback(name, mk_callback(self, value)) - - for prop, val in kwargs.items(): - setattr(self, prop, val) - - properties_and_callbacks: dict[Any, Any] = {"__init__": cls_init} - - for prop_name in compdef.properties.keys(): - python_prop = _normalize_prop(prop_name) - if python_prop in properties_and_callbacks: - logging.warning(f"Duplicated property {prop_name}") - continue - - def mk_setter_getter(prop_or_callback_name: str) -> property: - def getter(self: Component) -> Any: - return self.__instance__.get_property(prop_or_callback_name) - - def setter(self: Component, value: Any) -> None: - self.__instance__.set_property(prop_or_callback_name, value) - - return property(getter, setter) - - properties_and_callbacks[python_prop] = mk_setter_getter(prop_name) - - for callback_name in compdef.callbacks: - python_prop = _normalize_prop(callback_name) - if python_prop in properties_and_callbacks: - logging.warning(f"Duplicated property {prop_name}") - continue - - def mk_setter_getter(prop_or_callback_name: str) -> property: - def getter(self: Component) -> typing.Callable[..., Any]: - def call(*args: Any) -> Any: - return self.__instance__.invoke(prop_or_callback_name, *args) - - return call - - def setter(self: Component, value: typing.Callable[..., Any]) -> None: - self.__instance__.set_callback(prop_or_callback_name, value) - - return property(getter, setter) - - properties_and_callbacks[python_prop] = mk_setter_getter(callback_name) - - for function_name in compdef.functions: - python_prop = _normalize_prop(function_name) - if python_prop in properties_and_callbacks: - logging.warning(f"Duplicated function {prop_name}") - continue - - def mk_getter(function_name: str) -> property: - def getter(self: Component) -> typing.Callable[..., Any]: - def call(*args: Any) -> Any: - return self.__instance__.invoke(function_name, *args) - - return call - - return property(getter) - - properties_and_callbacks[python_prop] = mk_getter(function_name) - - for global_name in compdef.globals: - global_class = _build_global_class(compdef, global_name) - - def mk_global(global_class: typing.Callable[..., Any]) -> property: - def global_getter(self: Component) -> Any: - wrapper = global_class() - setattr(wrapper, "__instance__", self.__instance__) - return wrapper - - return property(global_getter) - - properties_and_callbacks[global_name] = mk_global(global_class) - - return type("SlintClassWrapper", (Component,), properties_and_callbacks) - - -def _build_struct(name: str, struct_prototype: core.PyStruct) -> type: - def new_struct(cls: Any, *args: Any, **kwargs: Any) -> core.PyStruct: - 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: str | os.PathLike[Any] | pathlib.Path, - quiet: bool = False, - style: typing.Optional[str] = None, - include_paths: typing.Optional[typing.List[os.PathLike[Any] | pathlib.Path]] = None, - library_paths: typing.Optional[ - typing.Dict[str, os.PathLike[Any] | pathlib.Path] - ] = None, - translation_domain: typing.Optional[str] = None, -) -> types.SimpleNamespace: - """This function is the low-level entry point into Slint for instantiating components. It loads the `.slint` file at - the specified `path` and returns a namespace with all exported components as Python classes, as well as enums, and structs. - - * `quiet`: Set to true to prevent any warnings during compilation from being printed to stderr. - * `style`: Specify a widget style. - * `include_paths`: Additional include paths used to look up `.slint` files imported from other `.slint` files. - * `library_paths`: A dictionary that maps library names to their location in the file system. This is then used to look up - library imports, such as `import { MyButton } from "@mylibrary";`. - * `translation_domain`: The domain to use for looking up the catalogue run-time translations. This must match the - translation domain used when extracting translations with `slint-tr-extractor`. - - """ - - compiler = core.Compiler() - - if style is not None: - compiler.style = style - if include_paths is not None: - compiler.include_paths = include_paths - if library_paths is not None: - compiler.library_paths = library_paths - if translation_domain is not None: - compiler.translation_domain = translation_domain - - result = compiler.build_from_path(Path(path)) - - diagnostics = result.diagnostics - if diagnostics: - if not quiet: - for diag in diagnostics: - if diag.level == core.DiagnosticLevel.Warning: - logging.warning(diag) - - errors = [ - diag for diag in diagnostics if diag.level == core.DiagnosticLevel.Error - ] - if errors: - raise CompileError(f"Could not compile {path}", diagnostics) - - module = types.SimpleNamespace() - for comp_name in result.component_names: - wrapper_class = _build_class(result.component(comp_name)) - - setattr(module, comp_name, wrapper_class) - - structs, enums = result.structs_and_enums - - for name, struct_prototype in structs.items(): - name = _normalize_prop(name) - 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) - setattr(module, new_name, getattr(module, orig_name)) - - return module - - -class SlintAutoLoader: - def __init__(self, base_dir: Path | None = None): - self.local_dirs: typing.List[Path] | None = None - if base_dir: - self.local_dirs = [base_dir] - - def __getattr__(self, name: str) -> Any: - for path in self.local_dirs or sys.path: - dir_candidate = Path(path) / name - if os.path.isdir(dir_candidate): - loader = SlintAutoLoader(dir_candidate) - setattr(self, name, loader) - return loader - - file_candidate = dir_candidate.with_suffix(".slint") - if os.path.isfile(file_candidate): - type_namespace = load_file(file_candidate) - setattr(self, name, type_namespace) - return type_namespace - - dir_candidate = Path(path) / name.replace("_", "-") - file_candidate = dir_candidate.with_suffix(".slint") - if os.path.isfile(file_candidate): - type_namespace = load_file(file_candidate) - setattr(self, name, type_namespace) - return type_namespace - - return None - - -loader = SlintAutoLoader() -"""Use the global `loader` object to load Slint files from the file system. It exposes two stages of attributes: -1. Any lookup of an attribute in the loader tries to match a file in `sys.path` with the `.slint` extension. For example - `loader.my_component` looks for a file `my_component.slint` in the directories in `sys.path`. -2. Any lookup in the object returned by the first stage tries to match an exported component in the loaded file, or a - struct, or enum. For example `loader.my_component.MyComponent` looks for an *exported* component named `MyComponent` - in the file `my_component.slint`. - -**Note:** The first entry in the module search path `sys.path` is the directory that contains the input script. - -Example: -```python -import slint -# Look for a file `main.slint` in the current directory, -# #load & compile it, and instantiate the exported `MainWindow` component -main_window = slint.loader.main_window.MainWindow() -main_window.show() -... -``` -""" - - -def _callback_decorator( - callable: typing.Callable[..., Any], info: typing.Dict[str, Any] -) -> typing.Callable[..., Any]: - if "name" not in info: - info["name"] = callable.__name__ - setattr(callable, "slint.callback", info) - - try: - import inspect - - if inspect.iscoroutinefunction(callable): - - def run_as_task(*args, **kwargs) -> None: # type: ignore - loop = asyncio.get_event_loop() - loop.create_task(callable(*args, **kwargs)) - - setattr(run_as_task, "slint.callback", info) - setattr(run_as_task, "slint.async", True) - return run_as_task - except ImportError: - pass - - return callable - - -_T_Callback = TypeVar("_T_Callback", bound=Callable[..., Any]) - - -@overload -def callback(__func: _T_Callback, /) -> _T_Callback: ... - - -@overload -def callback( - *, global_name: str | None = ..., name: str | None = ... -) -> Callable[[_T_Callback], _T_Callback]: ... - - -@overload -def callback( - __func: _T_Callback, /, *, global_name: str | None = ..., name: str | None = ... -) -> _T_Callback: ... - - -def callback( - __func: _T_Callback | None = None, - /, - *, - global_name: str | None = None, - name: str | None = None, -) -> typing.Union[_T_Callback, typing.Callable[[_T_Callback], _T_Callback]]: - """Use the callback decorator to mark a method as a callback that can be invoked from the Slint component. - - For the decorator to work, the method must be a member of a class that is Slint component. - - Example: - ```python - import slint - - class AppMainWindow(slint.loader.main_window.MainWindow): - - # Automatically connected to a callback button_clicked() - # in main_window.slint's MainWindow. - @slint.callback() - def button_clicked(self): - print("Button clicked") - - ... - ``` - - If your Python method has a different name from the Slint component's callback, use the `name` parameter to specify - the correct name. Similarly, use the `global_name` parameter to specify the name of the correct global singleton in - the Slint component. - - **Note:** The callback decorator can also be used with async functions. They will be run as task in the asyncio event loop. - This is only supported for callbacks that don't return any value, and requires Python >= 3.13. - """ - - # If used as @callback without args: __func is the callable - if __func is not None and callable(__func): - return _callback_decorator(__func, {}) - - info: dict[str, str] = {} - if name: - info["name"] = name - if global_name: - info["global_name"] = global_name - - def _wrapper(fn: _T_Callback) -> _T_Callback: - return typing.cast(_T_Callback, _callback_decorator(fn, info)) - - return _wrapper - - -def set_xdg_app_id(app_id: str) -> None: - """Sets the application id for use on Wayland or X11 with [xdg](https://specifications.freedesktop.org/desktop-entry-spec/latest/) - compliant window managers. This id must be set before the window is shown; it only applies to Wayland or X11.""" - - core.set_xdg_app_id(app_id) - - -quit_event = asyncio.Event() - - -def run_event_loop( - main_coro: typing.Optional[Coroutine[None, None, None]] = None, -) -> None: - """Runs the main Slint event loop. If specified, the coroutine `main_coro` is run in parallel. The event loop doesn't - terminate when the coroutine finishes, it terminates when calling `quit_event_loop()`. - - Example: - ```python - import slint - - ... - image_model: slint.ListModel[slint.Image] = slint.ListModel() - ... - - async def main_receiver(image_model: slint.ListModel) -> None: - async with aiohttp.ClientSession() as session: - async with session.get("http://some.server/svg-image") as response: - svg = await response.read() - image = slint.Image.from_svg_data(svg) - image_model.append(image) - - ... - slint.run_event_loop(main_receiver(image_model)) - ``` - - """ - - async def run_inner() -> None: - global quit_event - loop = typing.cast(SlintEventLoop, asyncio.get_event_loop()) - - quit_task = asyncio.ensure_future(quit_event.wait(), loop=loop) - - tasks: typing.List[asyncio.Task[typing.Any]] = [quit_task] - - main_task = None - if main_coro: - main_task = loop.create_task(main_coro) - tasks.append(main_task) - - done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - - if main_task is not None and main_task in done: - main_task.result() # propagate exception if thrown - if quit_task in pending: - await quit_event.wait() - - global quit_event - quit_event = asyncio.Event() - asyncio.run(run_inner(), debug=False, loop_factory=SlintEventLoop) - - -def quit_event_loop() -> None: - """Quits the running event loop in the next event processing cycle. This will make an earlier call to `run_event_loop()` - return.""" - global quit_event - quit_event.set() - - -def init_translations(translations: typing.Optional[gettext.GNUTranslations]) -> None: - """Installs the specified translations object to handle translations originating from the Slint code. - - Example: - ```python - import gettext - import slint - - translations_dir = os.path.join(os.path.dirname(__file__), "lang") - try: - translations = gettext.translation("my_app", translations_dir, ["de"]) - slint.install_translations(translations) - except OSError: - pass - ``` - """ - core.init_translations(translations) - +from .api import Brush as Brush +from .api import Color as Color +from .api import CompileError as CompileError +from .api import Component as Component +from .api import Image as Image +from .api import ListModel as ListModel +from .api import Model as Model +from .api import Timer as Timer +from .api import TimerMode as TimerMode +from .api import callback as callback +from .api import init_translations as init_translations +from .api import load_file as load_file +from .api import loader as loader +from .api import quit_event_loop as quit_event_loop +from .api import run_event_loop as run_event_loop +from .api import set_xdg_app_id as set_xdg_app_id __all__ = [ "CompileError", diff --git a/api/python/slint/slint/api.py b/api/python/slint/slint/api.py new file mode 100644 index 00000000000..daba75e4c7c --- /dev/null +++ b/api/python/slint/slint/api.py @@ -0,0 +1,607 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +r""" +.. include:: ../README.md +""" + +import asyncio +import copy +import gettext +import logging +import os +import pathlib +import sys +import types +import typing +from collections.abc import Coroutine +from pathlib import Path +from typing import Any, Callable, TypeVar, overload + +from .core import ( + Brush, + Color, + Compiler, + ComponentDefinition, + ComponentInstance, + DiagnosticLevel, + Image, + PyDiagnostic, + PyStruct, + Timer, + TimerMode, +) +from .core import ( + init_translations as _slint_init_translations, +) +from .loop import SlintEventLoop +from .models import ListModel, Model + + +class CompileError(Exception): + message: str + """The error message that produced this compile error.""" + + diagnostics: list[PyDiagnostic] + """A list of detailed diagnostics that were produced as part of the compilation.""" + + def __init__(self, message: str, diagnostics: list[PyDiagnostic]): + """@private""" + super().__init__(message) + self.message = message + self.diagnostics = diagnostics + for diag in self.diagnostics: + self.add_note(str(diag)) + + +class Component: + """Component is the base class for all instances of Slint components. Use the member functions to show or hide the + window, or spin the event loop.""" + + __instance__: ComponentInstance + + def show(self) -> None: + """Shows the window on the screen.""" + + self.__instance__.show() + + def hide(self) -> None: + """Hides the window from the screen.""" + + self.__instance__.hide() + + def run(self) -> None: + """Shows the window, runs the event loop, hides it when the loop is quit, and returns.""" + self.show() + run_event_loop() + self.hide() + + +def _normalize_prop(name: str) -> str: + return name.replace("-", "_") + + +def _build_global_class(compdef: ComponentDefinition, global_name: str) -> Any: + properties_and_callbacks = {} + + for prop_name in compdef.global_properties(global_name).keys(): + python_prop = _normalize_prop(prop_name) + if python_prop in properties_and_callbacks: + logging.warning(f"Duplicated property {prop_name}") + continue + + def mk_setter_getter(prop_or_callback_name: str) -> property: + def getter(self: Component) -> Any: + return self.__instance__.get_global_property( + global_name, prop_or_callback_name + ) + + def setter(self: Component, value: Any) -> None: + self.__instance__.set_global_property( + global_name, prop_or_callback_name, value + ) + + return property(getter, setter) + + properties_and_callbacks[python_prop] = mk_setter_getter(prop_name) + + for callback_name in compdef.global_callbacks(global_name): + python_prop = _normalize_prop(callback_name) + if python_prop in properties_and_callbacks: + logging.warning(f"Duplicated property {prop_name}") + continue + + def mk_setter_getter(prop_or_callback_name: str) -> property: + def getter(self: Component) -> typing.Callable[..., Any]: + def call(*args: Any) -> Any: + return self.__instance__.invoke_global( + global_name, prop_or_callback_name, *args + ) + + return call + + def setter(self: Component, value: typing.Callable[..., Any]) -> None: + self.__instance__.set_global_callback( + global_name, prop_or_callback_name, value + ) + + return property(getter, setter) + + properties_and_callbacks[python_prop] = mk_setter_getter(callback_name) + + for function_name in compdef.global_functions(global_name): + python_prop = _normalize_prop(function_name) + if python_prop in properties_and_callbacks: + logging.warning(f"Duplicated function {prop_name}") + continue + + def mk_getter(function_name: str) -> property: + def getter(self: Component) -> typing.Callable[..., Any]: + def call(*args: Any) -> Any: + return self.__instance__.invoke_global( + global_name, function_name, *args + ) + + return call + + return property(getter) + + properties_and_callbacks[python_prop] = mk_getter(function_name) + + return type("SlintGlobalClassWrapper", (), properties_and_callbacks) + + +def _build_class( + compdef: ComponentDefinition, +) -> typing.Callable[..., Component]: + def cls_init(self: Component, **kwargs: Any) -> Any: + self.__instance__ = compdef.create() + for name, value in self.__class__.__dict__.items(): + if hasattr(value, "slint.callback"): + callback_info = getattr(value, "slint.callback") + name = callback_info["name"] + + is_async = getattr(value, "slint.async", False) + if is_async: + if "global_name" in callback_info: + global_name = callback_info["global_name"] + if not compdef.global_callback_returns_void(global_name, name): + raise RuntimeError( + f"Callback '{name}' in global '{global_name}' cannot be used with a callback decorator for an async function, as it doesn't return void" + ) + else: + if not compdef.callback_returns_void(name): + raise RuntimeError( + f"Callback '{name}' cannot be used with a callback decorator for an async function, as it doesn't return void" + ) + + def mk_callback( + self: Any, callback: typing.Callable[..., Any] + ) -> typing.Callable[..., Any]: + def invoke(*args: Any, **kwargs: Any) -> Any: + return callback(self, *args, **kwargs) + + return invoke + + if "global_name" in callback_info: + self.__instance__.set_global_callback( + callback_info["global_name"], name, mk_callback(self, value) + ) + else: + self.__instance__.set_callback(name, mk_callback(self, value)) + + for prop, val in kwargs.items(): + setattr(self, prop, val) + + properties_and_callbacks: dict[Any, Any] = {"__init__": cls_init} + + for prop_name in compdef.properties.keys(): + python_prop = _normalize_prop(prop_name) + if python_prop in properties_and_callbacks: + logging.warning(f"Duplicated property {prop_name}") + continue + + def mk_setter_getter(prop_or_callback_name: str) -> property: + def getter(self: Component) -> Any: + return self.__instance__.get_property(prop_or_callback_name) + + def setter(self: Component, value: Any) -> None: + self.__instance__.set_property(prop_or_callback_name, value) + + return property(getter, setter) + + properties_and_callbacks[python_prop] = mk_setter_getter(prop_name) + + for callback_name in compdef.callbacks: + python_prop = _normalize_prop(callback_name) + if python_prop in properties_and_callbacks: + logging.warning(f"Duplicated property {prop_name}") + continue + + def mk_setter_getter(prop_or_callback_name: str) -> property: + def getter(self: Component) -> typing.Callable[..., Any]: + def call(*args: Any) -> Any: + return self.__instance__.invoke(prop_or_callback_name, *args) + + return call + + def setter(self: Component, value: typing.Callable[..., Any]) -> None: + self.__instance__.set_callback(prop_or_callback_name, value) + + return property(getter, setter) + + properties_and_callbacks[python_prop] = mk_setter_getter(callback_name) + + for function_name in compdef.functions: + python_prop = _normalize_prop(function_name) + if python_prop in properties_and_callbacks: + logging.warning(f"Duplicated function {prop_name}") + continue + + def mk_getter(function_name: str) -> property: + def getter(self: Component) -> typing.Callable[..., Any]: + def call(*args: Any) -> Any: + return self.__instance__.invoke(function_name, *args) + + return call + + return property(getter) + + properties_and_callbacks[python_prop] = mk_getter(function_name) + + for global_name in compdef.globals: + global_class = _build_global_class(compdef, global_name) + + def mk_global(global_class: typing.Callable[..., Any]) -> property: + def global_getter(self: Component) -> Any: + wrapper = global_class() + setattr(wrapper, "__instance__", self.__instance__) + return wrapper + + return property(global_getter) + + properties_and_callbacks[global_name] = mk_global(global_class) + + return type("SlintClassWrapper", (Component,), properties_and_callbacks) + + +def _build_struct(name: str, struct_prototype: PyStruct) -> type: + def new_struct(cls: Any, *args: Any, **kwargs: Any) -> PyStruct: + 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: str | os.PathLike[Any] | pathlib.Path, + quiet: bool = False, + style: typing.Optional[str] = None, + include_paths: typing.Optional[typing.List[os.PathLike[Any] | pathlib.Path]] = None, + library_paths: typing.Optional[ + typing.Dict[str, os.PathLike[Any] | pathlib.Path] + ] = None, + translation_domain: typing.Optional[str] = None, +) -> types.SimpleNamespace: + """This function is the low-level entry point into Slint for instantiating components. It loads the `.slint` file at + the specified `path` and returns a namespace with all exported components as Python classes, as well as enums, and structs. + + * `quiet`: Set to true to prevent any warnings during compilation from being printed to stderr. + * `style`: Specify a widget style. + * `include_paths`: Additional include paths used to look up `.slint` files imported from other `.slint` files. + * `library_paths`: A dictionary that maps library names to their location in the file system. This is then used to look up + library imports, such as `import { MyButton } from "@mylibrary";`. + * `translation_domain`: The domain to use for looking up the catalogue run-time translations. This must match the + translation domain used when extracting translations with `slint-tr-extractor`. + + """ + + compiler = Compiler() + + if style is not None: + compiler.style = style + if include_paths is not None: + compiler.include_paths = include_paths + if library_paths is not None: + compiler.library_paths = library_paths + if translation_domain is not None: + compiler.translation_domain = translation_domain + + result = compiler.build_from_path(Path(path)) + + diagnostics = result.diagnostics + if diagnostics: + if not quiet: + for diag in diagnostics: + if diag.level == DiagnosticLevel.Warning: + logging.warning(diag) + + errors = [diag for diag in diagnostics if diag.level == DiagnosticLevel.Error] + if errors: + raise CompileError(f"Could not compile {path}", diagnostics) + + module = types.SimpleNamespace() + for comp_name in result.component_names: + wrapper_class = _build_class(result.component(comp_name)) + + setattr(module, comp_name, wrapper_class) + + structs, enums = result.structs_and_enums + + for name, struct_prototype in structs.items(): + name = _normalize_prop(name) + 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) + setattr(module, new_name, getattr(module, orig_name)) + + return module + + +class SlintAutoLoader: + def __init__(self, base_dir: Path | None = None): + self.local_dirs: typing.List[Path] | None = None + if base_dir: + self.local_dirs = [base_dir] + + def __getattr__(self, name: str) -> Any: + for path in self.local_dirs or sys.path: + dir_candidate = Path(path) / name + if os.path.isdir(dir_candidate): + loader = SlintAutoLoader(dir_candidate) + setattr(self, name, loader) + return loader + + file_candidate = dir_candidate.with_suffix(".slint") + if os.path.isfile(file_candidate): + type_namespace = load_file(file_candidate) + setattr(self, name, type_namespace) + return type_namespace + + dir_candidate = Path(path) / name.replace("_", "-") + file_candidate = dir_candidate.with_suffix(".slint") + if os.path.isfile(file_candidate): + type_namespace = load_file(file_candidate) + setattr(self, name, type_namespace) + return type_namespace + + return None + + +loader = SlintAutoLoader() +"""Use the global `loader` object to load Slint files from the file system. It exposes two stages of attributes: +1. Any lookup of an attribute in the loader tries to match a file in `sys.path` with the `.slint` extension. For example + `loader.my_component` looks for a file `my_component.slint` in the directories in `sys.path`. +2. Any lookup in the object returned by the first stage tries to match an exported component in the loaded file, or a + struct, or enum. For example `loader.my_component.MyComponent` looks for an *exported* component named `MyComponent` + in the file `my_component.slint`. + +**Note:** The first entry in the module search path `sys.path` is the directory that contains the input script. + +Example: +```python +import slint +# Look for a file `main.slint` in the current directory, +# #load & compile it, and instantiate the exported `MainWindow` component +main_window = slint.loader.main_window.MainWindow() +main_window.show() +... +``` +""" + + +def _callback_decorator( + callable: typing.Callable[..., Any], info: typing.Dict[str, Any] +) -> typing.Callable[..., Any]: + if "name" not in info: + info["name"] = callable.__name__ + setattr(callable, "slint.callback", info) + + try: + import inspect + + if inspect.iscoroutinefunction(callable): + + def run_as_task(*args, **kwargs) -> None: # type: ignore + loop = asyncio.get_event_loop() + loop.create_task(callable(*args, **kwargs)) + + setattr(run_as_task, "slint.callback", info) + setattr(run_as_task, "slint.async", True) + return run_as_task + except ImportError: + pass + + return callable + + +_T_Callback = TypeVar("_T_Callback", bound=Callable[..., Any]) + + +@overload +def callback(__func: _T_Callback, /) -> _T_Callback: ... + + +@overload +def callback( + *, global_name: str | None = ..., name: str | None = ... +) -> Callable[[_T_Callback], _T_Callback]: ... + + +@overload +def callback( + __func: _T_Callback, /, *, global_name: str | None = ..., name: str | None = ... +) -> _T_Callback: ... + + +def callback( + __func: _T_Callback | None = None, + /, + *, + global_name: str | None = None, + name: str | None = None, +) -> typing.Union[_T_Callback, typing.Callable[[_T_Callback], _T_Callback]]: + """Use the callback decorator to mark a method as a callback that can be invoked from the Slint component. + + For the decorator to work, the method must be a member of a class that is Slint component. + + Example: + ```python + import slint + + class AppMainWindow(slint.loader.main_window.MainWindow): + + # Automatically connected to a callback button_clicked() + # in main_window.slint's MainWindow. + @slint.callback() + def button_clicked(self): + print("Button clicked") + + ... + ``` + + If your Python method has a different name from the Slint component's callback, use the `name` parameter to specify + the correct name. Similarly, use the `global_name` parameter to specify the name of the correct global singleton in + the Slint component. + + **Note:** The callback decorator can also be used with async functions. They will be run as task in the asyncio event loop. + This is only supported for callbacks that don't return any value, and requires Python >= 3.13. + """ + + # If used as @callback without args: __func is the callable + if __func is not None and callable(__func): + return _callback_decorator(__func, {}) + + info: dict[str, str] = {} + if name: + info["name"] = name + if global_name: + info["global_name"] = global_name + + def _wrapper(fn: _T_Callback) -> _T_Callback: + return typing.cast(_T_Callback, _callback_decorator(fn, info)) + + return _wrapper + + +def set_xdg_app_id(app_id: str) -> None: + """Sets the application id for use on Wayland or X11 with [xdg](https://specifications.freedesktop.org/desktop-entry-spec/latest/) + compliant window managers. This id must be set before the window is shown; it only applies to Wayland or X11.""" + + set_xdg_app_id(app_id) + + +quit_event = asyncio.Event() + + +def run_event_loop( + main_coro: typing.Optional[Coroutine[None, None, None]] = None, +) -> None: + """Runs the main Slint event loop. If specified, the coroutine `main_coro` is run in parallel. The event loop doesn't + terminate when the coroutine finishes, it terminates when calling `quit_event_loop()`. + + Example: + ```python + import slint + + ... + image_model: slint.ListModel[slint.Image] = slint.ListModel() + ... + + async def main_receiver(image_model: slint.ListModel) -> None: + async with aiohttp.ClientSession() as session: + async with session.get("http://some.server/svg-image") as response: + svg = await response.read() + image = slint.Image.from_svg_data(svg) + image_model.append(image) + + ... + slint.run_event_loop(main_receiver(image_model)) + ``` + + """ + + async def run_inner() -> None: + global quit_event + loop = typing.cast(SlintEventLoop, asyncio.get_event_loop()) + + quit_task = asyncio.ensure_future(quit_event.wait(), loop=loop) + + tasks: typing.List[asyncio.Task[typing.Any]] = [quit_task] + + main_task = None + if main_coro: + main_task = loop.create_task(main_coro) + tasks.append(main_task) + + done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + + if main_task is not None and main_task in done: + main_task.result() # propagate exception if thrown + if quit_task in pending: + await quit_event.wait() + + global quit_event + quit_event = asyncio.Event() + asyncio.run(run_inner(), debug=False, loop_factory=SlintEventLoop) + + +def quit_event_loop() -> None: + """Quits the running event loop in the next event processing cycle. This will make an earlier call to `run_event_loop()` + return.""" + global quit_event + quit_event.set() + + +def init_translations(translations: typing.Optional[gettext.GNUTranslations]) -> None: + """Installs the specified translations object to handle translations originating from the Slint code. + + Example: + ```python + import gettext + import slint + + translations_dir = os.path.join(os.path.dirname(__file__), "lang") + try: + translations = gettext.translation("my_app", translations_dir, ["de"]) + slint.install_translations(translations) + except OSError: + pass + ``` + """ + _slint_init_translations(translations) + + +__all__ = [ + "CompileError", + "Component", + "load_file", + "loader", + "Image", + "Color", + "Brush", + "Model", + "ListModel", + "Timer", + "TimerMode", + "set_xdg_app_id", + "callback", + "run_event_loop", + "quit_event_loop", + "init_translations", +] diff --git a/api/python/slint/slint/codegen/__init__.py b/api/python/slint/slint/codegen/__init__.py index e69de29bb2d..55c735ca1ff 100644 --- a/api/python/slint/slint/codegen/__init__.py +++ b/api/python/slint/slint/codegen/__init__.py @@ -0,0 +1,2 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index d3dee56ccc3..0b29a12e384 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -8,7 +8,7 @@ import libcst as cst -from .. import _normalize_prop +from ..api import _normalize_prop from .models import CallbackMeta, GenerationConfig, ModuleArtifacts @@ -369,7 +369,7 @@ def init_stub() -> cst.FunctionDef: path.write_text(module.code, encoding="utf-8") -def format_callable_annotation(callback: "CallbackMeta") -> str: # type: ignore[name-defined] +def format_callable_annotation(callback: "CallbackMeta") -> str: args = callback.arg_types return_type = callback.return_type diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index 08fdfcdf0a0..d48f8535448 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -7,9 +7,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Iterable -from .. import core -from .. import _normalize_prop - +from ..api import _normalize_prop +from ..core import Brush, CompilationResult, Compiler, DiagnosticLevel, Image from .emitters import write_python_module, write_stub_module from .models import ( CallbackMeta, @@ -42,7 +41,8 @@ def generate_project( if output_dir is not None: output_dir.mkdir(parents=True, exist_ok=True) - compiler = core.Compiler() + compiler = Compiler() + if config.style: compiler.style = config.style if config.include_paths: @@ -102,23 +102,23 @@ def _discover_slint_files(inputs: Iterable[Path]) -> Iterable[tuple[Path, Path]] def _compile_slint( - compiler: core.Compiler, + compiler: Compiler, source_path: Path, config: GenerationConfig, -) -> core.CompilationResult | None: +) -> CompilationResult | None: result = compiler.build_from_path(source_path) - diagnostics = result.diagnostics - diagnostic_error = getattr(core, "DiagnosticLevel", None) - error_enum = getattr(diagnostic_error, "Error", None) - def is_error(diag: PyDiagnostic) -> bool: - if error_enum is not None: - return diag.level == error_enum - return str(diag.level).lower().startswith("error") + return diag.level == DiagnosticLevel.Error - errors = [diag for diag in diagnostics if is_error(diag)] - warnings = [diag for diag in diagnostics if not is_error(diag)] + errors: list[PyDiagnostic] = [] + warnings: list[PyDiagnostic] = [] + + for diag in result.diagnostics: + if is_error(diag): + errors.append(diag) + else: + warnings.append(diag) if warnings and not config.quiet: for diag in warnings: @@ -133,8 +133,9 @@ def is_error(diag: PyDiagnostic) -> bool: return result -def _collect_metadata(result: core.CompilationResult) -> ModuleArtifacts: +def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: components: list[ComponentMeta] = [] + for name in result.component_names: comp = result.component(name) @@ -275,9 +276,9 @@ def _python_value_hint(value: object) -> str: return "float" if isinstance(value, str): return "str" - if isinstance(value, core.Image): + if isinstance(value, Image): return "slint.Image" - if isinstance(value, core.Brush): + if isinstance(value, Brush): return "slint.Brush" return "Any" diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index b138b6dfe8a..95a0e0f9ae8 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -6,13 +6,13 @@ import builtins import datetime +import gettext import os import pathlib import typing -from typing import Any, List -from collections.abc import Callable, Buffer, Coroutine +from collections.abc import Buffer, Callable, Coroutine from enum import Enum, auto -import gettext +from typing import Any, List class RgbColor: red: int diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index da7442a0993..c15c2af5a04 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -1,14 +1,15 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from . import core -import asyncio.selector_events import asyncio import asyncio.events +import asyncio.selector_events +import datetime import selectors import typing from collections.abc import Mapping -import datetime + +from . import core class HasFileno(typing.Protocol): diff --git a/api/python/slint/slint/models.py b/api/python/slint/slint/models.py index 7093cb6f560..2a8ad7372a3 100644 --- a/api/python/slint/slint/models.py +++ b/api/python/slint/slint/models.py @@ -1,11 +1,12 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from . import core -from collections.abc import Iterable -from abc import abstractmethod import typing -from typing import Any, cast, Iterator +from abc import abstractmethod +from collections.abc import Iterable +from typing import Any, Iterator, cast + +from . import core class Model[T](core.PyModelBase, Iterable[T]): diff --git a/api/python/slint/tests/codegen/examples/counter/counter.pyi b/api/python/slint/tests/codegen/examples/counter/counter.pyi index ec9e1bad2d6..7208e979f57 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.pyi +++ b/api/python/slint/tests/codegen/examples/counter/counter.pyi @@ -3,8 +3,6 @@ from __future__ import annotations -import enum - from typing import Any, Callable import slint diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi b/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi index ec9e1bad2d6..7208e979f57 100644 --- a/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi +++ b/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi @@ -3,8 +3,6 @@ from __future__ import annotations -import enum - from typing import Any, Callable import slint diff --git a/api/python/slint/tests/codegen/test_generator.py b/api/python/slint/tests/codegen/test_generator.py index 56a4c6646aa..42aa046a7c0 100644 --- a/api/python/slint/tests/codegen/test_generator.py +++ b/api/python/slint/tests/codegen/test_generator.py @@ -10,6 +10,7 @@ import subprocess import sys from pathlib import Path +from typing import Any import pytest from slint.codegen.cli import _parse_library_paths @@ -100,12 +101,12 @@ def _write_optional_fixture(target_dir: Path) -> Path: return slint_file -def _load_module(module_path: Path): +def _load_module(module_path: Path) -> Any: spec = importlib.util.spec_from_file_location("generated_module", module_path) assert spec and spec.loader module = importlib.util.module_from_spec(spec) sys.modules.pop("generated_module", None) - spec.loader.exec_module(module) # type: ignore[assignment] + spec.loader.exec_module(module) return module From 11275c8616b511d978fca1336abfa8c4a72dc9e6 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Sat, 25 Oct 2025 15:08:47 +0800 Subject: [PATCH 11/52] fix: nomalize identity with python keyword --- api/python/slint/slint/api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/python/slint/slint/api.py b/api/python/slint/slint/api.py index daba75e4c7c..c4d4342eed5 100644 --- a/api/python/slint/slint/api.py +++ b/api/python/slint/slint/api.py @@ -8,6 +8,7 @@ import asyncio import copy import gettext +import keyword import logging import os import pathlib @@ -78,7 +79,12 @@ def run(self) -> None: def _normalize_prop(name: str) -> str: - return name.replace("-", "_") + ident = name.replace("-", "_") + if ident and ident[0].isdigit(): + ident = f"_{ident}" + if keyword.iskeyword(ident): + ident = f"{ident}_" + return ident def _build_global_class(compdef: ComponentDefinition, global_name: str) -> Any: From 78ff39de16a4b5d16fc1c1063a5174b04444e5f2 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Sat, 25 Oct 2025 15:09:43 +0800 Subject: [PATCH 12/52] fix: missing hint for slint.Color --- api/python/slint/slint/codegen/generator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index d48f8535448..de4ea6469ad 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Iterable from ..api import _normalize_prop -from ..core import Brush, CompilationResult, Compiler, DiagnosticLevel, Image +from ..core import Brush, Color, CompilationResult, Compiler, DiagnosticLevel, Image from .emitters import write_python_module, write_stub_module from .models import ( CallbackMeta, @@ -280,6 +280,8 @@ def _python_value_hint(value: object) -> str: return "slint.Image" if isinstance(value, Brush): return "slint.Brush" + if isinstance(value, Color): + return "slint.Color" return "Any" From a5ce4e6a0df542b6dd38506d379cca39b17569cb Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Sat, 25 Oct 2025 15:10:35 +0800 Subject: [PATCH 13/52] fix(python/cli): expect at least one input --- api/python/slint/slint/codegen/cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/python/slint/slint/codegen/cli.py b/api/python/slint/slint/codegen/cli.py index 57a03b2d521..8f1c95fc897 100644 --- a/api/python/slint/slint/codegen/cli.py +++ b/api/python/slint/slint/codegen/cli.py @@ -108,8 +108,13 @@ def main(argv: list[str] | None = None) -> int: command = getattr(args, "command", None) if command not in (None, "generate"): parser.error(f"Unknown command: {command}") + return 1 + + if not args.inputs: + print("error: At least one --input must be provided.") + return 1 - inputs: list[Path] = args.inputs or [Path.cwd()] + inputs: list[Path] = args.inputs config = GenerationConfig( include_paths=args.include_paths or [], library_paths=_parse_library_paths(args.library_paths or []), From d24f5dd46b99ab63eb0cf94acdecaa558df1d6bb Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Sat, 25 Oct 2025 15:57:31 +0800 Subject: [PATCH 14/52] fix(python): recorrect python identifier nomi in rust --- api/python/slint/interpreter.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index ee7dde70748..33f3b66bd16 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::rc::Rc; +use std::sync::OnceLock; use pyo3::IntoPyObjectExt; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyclass_enum, gen_stub_pymethods}; @@ -424,15 +425,32 @@ impl From for PyValueType { } } +fn is_python_keyword(name: &str) -> bool { + static PYTHON_KEYWORDS: OnceLock> = OnceLock::new(); + let keywords = PYTHON_KEYWORDS.get_or_init(|| { + let keywords: HashSet<&str> = HashSet::from([ + "False", "await", "else", "import", "pass", "None", "break", "except", "in", "raise", + "True", "class", "finally", "is", "return", "and", "continue", "for", "lambda", "try", + "as", "def", "from", "nonlocal", "while", "assert", "del", "global", "not", "with", + "async", "elif", "if", "or", "yield", + ]); + keywords + }); + keywords.contains(name) +} + fn python_identifier(name: &str) -> String { if name.is_empty() { return String::new(); } - let mut result = name.replace('-', "_"); - if result.chars().next().is_some_and(|c| c.is_ascii_digit()) { - result.insert(0, '_'); + let mut ident = name.replace('-', "_"); + if ident.chars().next().is_some_and(|c| c.is_ascii_digit()) { + ident.insert(0, '_'); + } + if is_python_keyword(&ident) { + ident.push('_'); } - result + ident } fn type_to_python_hint(ty: &i_slint_compiler::langtype::Type) -> String { From 449d76aa12c7114b3fee48e562b0365fe6dd758a Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Sat, 25 Oct 2025 15:58:03 +0800 Subject: [PATCH 15/52] fix(python): missing inspect import --- api/python/slint/slint/codegen/emitters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index 0b29a12e384..6c5f2939d14 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -10,6 +10,7 @@ from ..api import _normalize_prop from .models import CallbackMeta, GenerationConfig, ModuleArtifacts +import inspect def module_relative_path_expr(module_dir: Path, target: Path) -> str: From 35b6b9d9acb16f7134ea7a88edc977e5d2906dac Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Sat, 25 Oct 2025 22:20:06 +0800 Subject: [PATCH 16/52] refactor: restructure slint-compiler workflow --- .../workflows/upload_pypi_slint_compiler.yaml | 151 +++++++++++------- api/python/compiler/.gitignore | 1 - api/python/compiler/README.md | 15 -- api/python/compiler/pyproject.toml | 32 ---- .../compiler/slint_compiler/__init__.py | 67 -------- tools/compiler/.gitignore | 5 + tools/compiler/pyproject.toml | 16 ++ 7 files changed, 117 insertions(+), 170 deletions(-) delete mode 100644 api/python/compiler/.gitignore delete mode 100644 api/python/compiler/README.md delete mode 100644 api/python/compiler/pyproject.toml delete mode 100644 api/python/compiler/slint_compiler/__init__.py create mode 100644 tools/compiler/.gitignore create mode 100644 tools/compiler/pyproject.toml diff --git a/.github/workflows/upload_pypi_slint_compiler.yaml b/.github/workflows/upload_pypi_slint_compiler.yaml index 6156ef6b05b..e57df36744c 100644 --- a/.github/workflows/upload_pypi_slint_compiler.yaml +++ b/.github/workflows/upload_pypi_slint_compiler.yaml @@ -4,62 +4,103 @@ name: Upload slint-compiler to Python Package Index on: - workflow_dispatch: - inputs: - release: - type: boolean - default: false - required: false - description: "Release? If false, publish to test.pypi.org, if true, publish to pypi.org" + workflow_dispatch: + inputs: + release: + type: boolean + default: false + required: false + description: "Release? If false, publish to test.pypi.org, if true, publish to pypi.org" jobs: - publish-to-test-pypi: - if: ${{ github.event.inputs.release != 'true' }} - name: >- - Publish Python 🐍 distribution 📦 to Test PyPI - runs-on: ubuntu-latest - environment: - name: testpypi - url: https://test.pypi.org/p/slint-compiler - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - steps: - - uses: actions/checkout@v5 - - name: Install uv - uses: astral-sh/setup-uv@v7 - - name: Build - run: uv build - working-directory: api/python/compiler - - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: api/python/compiler/dist/* - - name: Publish - run: uv publish --publish-url https://test.pypi.org/legacy/ - working-directory: api/python/compiler + build-wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + [ + ubuntu-latest, + ubuntu-24.04-arm, + windows-latest, + windows-11-arm, + macos-15-intel, + macos-14, + ] + steps: + - uses: actions/checkout@v5 + - uses: ./.github/actions/setup-rust + - name: Build wheels + uses: pypa/cibuildwheel@v3.2.1 + with: + package-dir: tools/compiler + output-dir: wheelhouse + env: + CIBW_PROJECT_REQUIRES_PYTHON: ">=3.10" + CIBW_BUILD_FRONTEND: "build" + - uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl - publish-to-pypi: - if: ${{ github.event.inputs.release == 'true' }} - name: >- - Publish Python 🐍 distribution 📦 to PyPI - runs-on: ubuntu-latest - environment: - name: pypi - url: https://test.pypi.org/p/slint-compiler - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - steps: - - uses: actions/checkout@v5 - - name: Install uv - uses: astral-sh/setup-uv@v7 - - name: Build - run: uv build - working-directory: api/python/compiler - - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: api/python/compiler/dist/* - - name: Publish - run: uv publish - working-directory: api/python/compiler + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + - name: Build sdist + run: uv build --sdist -o dist tools/compiler + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + publish-to-test-pypi: + if: ${{ github.event.inputs.release != 'true' }} + needs: [build-wheels, build-sdist] + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/slint-compiler + permissions: + id-token: write + steps: + - uses: astral-sh/setup-uv@v7 + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + - name: Publish to Test PyPI + env: + UV_PUBLISH_URL: https://test.pypi.org/legacy/ + run: uv publish dist/* + + publish-to-pypi: + if: ${{ github.event.inputs.release == 'true' }} + needs: [build-wheels, build-sdist] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/slint-compiler + permissions: + id-token: write + steps: + - uses: astral-sh/setup-uv@v7 + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + - name: Publish to PyPI + run: uv publish dist/* diff --git a/api/python/compiler/.gitignore b/api/python/compiler/.gitignore deleted file mode 100644 index 11041c78340..00000000000 --- a/api/python/compiler/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.egg-info diff --git a/api/python/compiler/README.md b/api/python/compiler/README.md deleted file mode 100644 index 0e41baf771b..00000000000 --- a/api/python/compiler/README.md +++ /dev/null @@ -1,15 +0,0 @@ - - -# Python Slint Compiler - -This package is a wrapper around [Slint's](https://slint.dev) compiler for Python, to generate a typed `.py` file from a `.slint` file, for use with [Slint for Python](https://pypi.org/project/slint/). - -When run, the slint compiler binary is downloaded, cached, and run. - -By default, the Slint compiler matching the version of this package is downloaded. To select a specific version, set the `SLINT_COMPILER_VERSION` environment variable. Set it to `nightly` to select the latest nightly release. - -## Example - -```bash -uxv run slint-compiler -f python -o app_window.py app-window.slint -``` diff --git a/api/python/compiler/pyproject.toml b/api/python/compiler/pyproject.toml deleted file mode 100644 index b850979ed8f..00000000000 --- a/api/python/compiler/pyproject.toml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -[project] -name = "slint-compiler" -version = "1.14.1b1" -description = "Python wrapper around the Slint compiler for Python" -authors = [{ name = "Slint Team", email = "info@slint.dev" }] -readme = "README.md" -requires-python = ">=3.10" -dependencies = ["cached-path>=1.7.3"] - -[project.urls] -Homepage = "https://slint.dev" -Documentation = "https://slint.dev/docs" -Repository = "https://github.com/slint-ui/slint" -Changelog = "https://github.com/slint-ui/slint/blob/master/CHANGELOG.md" -Tracker = "https://github.com/slint-ui/slint/issues" - -[project.scripts] -slint-compiler = "slint_compiler:main" - -[dependency-groups] -dev = ["mypy>=1.15.0"] - -[tool.mypy] -strict = true -disallow_subclassing_any = false - -[[tool.mypy.overrides]] -module = ["cached_path.*"] -follow_untyped_imports = true diff --git a/api/python/compiler/slint_compiler/__init__.py b/api/python/compiler/slint_compiler/__init__.py deleted file mode 100644 index 23ed38ff3ce..00000000000 --- a/api/python/compiler/slint_compiler/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -import cached_path -import platform -import sys -import subprocess -import re -import os -from importlib.metadata import version, PackageNotFoundError - - -def main() -> None: - try: - package_version = version("slint-compiler") - # Strip alpha/beta from for example "1.13.0b1" - version_regex = re.search("([0-9]*)\\.([0-9]*)\\.([0-9]*).*", package_version) - assert version_regex is not None - major = version_regex.group(1) - minor = version_regex.group(2) - patch = version_regex.group(3) - github_release = f"v{major}.{minor}.{patch}" - except PackageNotFoundError: - github_release = "nightly" - - # Permit override by environment variable - github_release = os.environ.get("SLINT_COMPILER_VERSION") or github_release - - operating_system = { - "darwin": "Darwin", - "linux": "Linux", - "win32": "Windows", - "msys": "Windows", - }[sys.platform] - arch = { - "aarch64": "aarch64", - "amd64": "x86_64", - "arm64": "aarch64", - "x86_64": "x86_64", - }[platform.machine().lower()] - exe_suffix = "" - - if operating_system == "Windows": - arch = {"aarch64": "ARM64", "x86_64": "AMD64"}[arch] - exe_suffix = ".exe" - elif operating_system == "Linux": - pass - elif operating_system == "Darwin": - arch = {"aarch64": "arm64", "x86_64": "x86_64"}[arch] - else: - raise Exception(f"Unsupported operarating system: {operating_system}") - - platform_suffix = f"{operating_system}-{arch}" - prebuilt_archive_filename = f"slint-compiler-{platform_suffix}.tar.gz" - download_url = f"https://github.com/slint-ui/slint/releases/download/{github_release}/{prebuilt_archive_filename}" - url_and_path_within_archive = f"{download_url}!slint-compiler{exe_suffix}" - - local_path = cached_path.cached_path( - url_and_path_within_archive, extract_archive=True - ) - args = [str(local_path)] - args.extend(sys.argv[1:]) - subprocess.run(args) - - -if __name__ == "__main__": - main() diff --git a/tools/compiler/.gitignore b/tools/compiler/.gitignore new file mode 100644 index 00000000000..41bfc189309 --- /dev/null +++ b/tools/compiler/.gitignore @@ -0,0 +1,5 @@ +/target/ +/wheelhouse/ +/dist/ +*.egg-info/ +/build/ diff --git a/tools/compiler/pyproject.toml b/tools/compiler/pyproject.toml new file mode 100644 index 00000000000..0aa0f97ac11 --- /dev/null +++ b/tools/compiler/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["maturin>=1.9,<2.0"] +build-backend = "maturin" + +[project] +name = "slint-compiler" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", +] +dynamic = ["version"] + +[tool.maturin] +bindings = "bin" +manifest-path = "Cargo.toml" From 601faffeda3359c5f0e09215e0dc6e06d836d80a Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Sun, 26 Oct 2025 15:53:39 +0800 Subject: [PATCH 17/52] fix(python/briefcase): improve code formatting --- .../src/briefcasex_slint/__init__.py | 136 +++++++++--------- 1 file changed, 69 insertions(+), 67 deletions(-) diff --git a/api/python/briefcase/src/briefcasex_slint/__init__.py b/api/python/briefcase/src/briefcasex_slint/__init__.py index 738fc8754e2..c9532b89850 100644 --- a/api/python/briefcase/src/briefcasex_slint/__init__.py +++ b/api/python/briefcase/src/briefcasex_slint/__init__.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 from pathlib import Path +import inspect from briefcase.bootstraps.base import BaseGuiBootstrap @@ -10,91 +11,92 @@ class SlintGuiBootstrap(BaseGuiBootstrap): display_name_annotation = "does not support Android/Web deployment" def app_source(self): - return """\ -import slint + return inspect.cleandoc(""" + import slint -class {{ cookiecutter.class_name }}(slint.loader.{{ cookiecutter.module_name }}.resources.app_window.AppWindow): - @slint.callback - def request_increase_value(self): - self.counter = self.counter + 1 + class {{ cookiecutter.class_name }}(slint.loader.{{ cookiecutter.module_name }}.resources.app_window.AppWindow): + @slint.callback + def request_increase_value(self): + self.counter = self.counter + 1 -def main(): - main_window = {{ cookiecutter.class_name }}() - main_window.show() - main_window.run() -""" + def main(): + main_window = {{ cookiecutter.class_name }}() + main_window.show() + main_window.run() + """) def app_start_source(self): - return """\ -from {{ cookiecutter.module_name }}.app import main + return inspect.cleandoc(""" + from {{ cookiecutter.module_name }}.app import main -if __name__ == "__main__": - main() -""" + if __name__ == "__main__": + main() + """) def pyproject_table_briefcase_app_extra_content(self): - return """ -requires = [ -] -test_requires = [ -{% if cookiecutter.test_framework == "pytest" %} - "pytest", -{% endif %} -] -""" + return inspect.cleandoc(""" + requires = [ + ] + test_requires = [ + {% if cookiecutter.test_framework == "pytest" %} + "pytest", + {% endif %} + ] + """) def pyproject_table_macOS(self): - return """\ -universal_build = false -requires = [ - "slint", -] -""" + return inspect.cleandoc(""" + universal_build = false + requires = [ + "slint", + ] + """) def pyproject_table_linux(self): - return """\ -requires = [ - "slint", -] -""" + return inspect.cleandoc(""" + requires = [ + "slint", + ] + """) def pyproject_table_windows(self): - return """\ -requires = [ - "slint", -] -""" + return inspect.cleandoc(""" + requires = [ + "slint", + ] + """) def pyproject_table_iOS(self): - return """\ -requires = [ - "slint", -] -""" + return inspect.cleandoc(""" + requires = [ + "slint", + ] + """) def post_generate(self, base_path: Path) -> None: target_dir = base_path / self.context["source_dir"] / "resources" target_dir.mkdir(parents=True, exist_ok=True) with open(target_dir / "app-window.slint", "w") as slint_file: - slint_file.write(r""" -import { Button, VerticalBox, AboutSlint } from "std-widgets.slint"; - -export component AppWindow inherits Window { - in-out property counter: 42; - callback request-increase-value(); - VerticalBox { - alignment: center; - AboutSlint {} - Text { - text: "Counter: \{root.counter}"; - } - Button { - text: "Increase value"; - clicked => { - root.request-increase-value(); - } - } - } -} -""") + content = inspect.cleandoc(""" + import { Button, VerticalBox, AboutSlint } from "std-widgets.slint"; + + export component AppWindow inherits Window { + in-out property counter: 42; + callback request-increase-value(); + VerticalBox { + alignment: center; + AboutSlint {} + Text { + text: "Counter: {root.counter}"; + } + Button { + text: "Increase value"; + clicked => { + root.request-increase-value(); + } + } + } + } + """) + slint_file.write(content) From f6a4972bce9d18cb3a99290497abc3ae055770dd Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Mon, 27 Oct 2025 19:52:02 +0800 Subject: [PATCH 18/52] fix: override SlintToPyValue returning to Any --- api/python/slint/interpreter.rs | 4 ++++ api/python/slint/models.rs | 3 +++ api/python/slint/value.rs | 2 ++ 3 files changed, 9 insertions(+) diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index 33f3b66bd16..67bb7bdf432 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -630,6 +630,7 @@ impl ComponentInstance { } } + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn get_property(&self, name: &str) -> Result { Ok(self.type_collection.to_py_value(self.instance.get_property(name)?)) } @@ -640,6 +641,7 @@ impl ComponentInstance { Ok(self.instance.set_property(name, pv).map_err(|e| PySetPropertyError(e))?) } + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn get_global_property( &self, global_name: &str, @@ -665,6 +667,7 @@ impl ComponentInstance { } #[pyo3(signature = (callback_name, *args))] + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn invoke(&self, callback_name: &str, args: Bound<'_, PyTuple>) -> PyResult { let mut rust_args = vec![]; for arg in args.iter() { @@ -678,6 +681,7 @@ impl ComponentInstance { } #[pyo3(signature = (global_name, callback_name, *args))] + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn invoke_global( &self, global_name: &str, diff --git a/api/python/slint/models.rs b/api/python/slint/models.rs index c07225d6a54..274b45e073d 100644 --- a/api/python/slint/models.rs +++ b/api/python/slint/models.rs @@ -226,6 +226,7 @@ impl ReadOnlyRustModel { self.model.row_count() } + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn row_data(&self, row: usize) -> Option { self.model.row_data(row).map(|value| self.type_collection.to_py_value(value)) } @@ -242,6 +243,7 @@ impl ReadOnlyRustModel { } } + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn __getitem__(&self, index: usize) -> Option { self.row_data(index) } @@ -260,6 +262,7 @@ impl ReadOnlyRustModelIterator { slf } + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn __next__(&mut self) -> Option { if self.row >= self.model.row_count() { return None; diff --git a/api/python/slint/value.rs b/api/python/slint/value.rs index d152fa2ff77..7652bd361ef 100644 --- a/api/python/slint/value.rs +++ b/api/python/slint/value.rs @@ -115,6 +115,7 @@ pub struct PyStruct { #[pymethods] impl PyStruct { + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn __getattr__(&self, key: &str) -> PyResult { self.data.get_field(key).map_or_else( || { @@ -173,6 +174,7 @@ impl PyStructFieldIterator { slf } + #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(String, SlintToPyValue)> { slf.inner.next().map(|(name, val)| (name, slf.type_collection.to_py_value(val))) } From cab5a148af80b61b731d7a9aec3c8be22d779a8e Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Mon, 27 Oct 2025 20:48:03 +0800 Subject: [PATCH 19/52] fix: update pyo3_stub_gen usage and add gen_stub attributes --- api/python/slint/brush.rs | 8 +++++--- api/python/slint/interpreter.rs | 13 +++++++------ api/python/slint/models.rs | 9 +++++++++ api/python/slint/stub-gen/main.rs | 6 +----- api/python/slint/value.rs | 4 +++- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/api/python/slint/brush.rs b/api/python/slint/brush.rs index a427af3665e..12d8eb58844 100644 --- a/api/python/slint/brush.rs +++ b/api/python/slint/brush.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use pyo3::prelude::*; -use pyo3_stub_gen::{derive::gen_stub_pyclass, derive::gen_stub_pymethods, impl_stub_type}; +use pyo3_stub_gen::{ + derive::gen_stub_pyclass, derive::gen_stub_pyclass_complex_enum, derive::gen_stub_pymethods, + impl_stub_type, +}; use crate::errors::PyColorParseError; @@ -33,6 +36,7 @@ struct RgbColor { } #[derive(FromPyObject)] +#[gen_stub_pyclass_complex_enum] #[pyclass] enum PyColorInput { ColorStr(String), @@ -57,8 +61,6 @@ enum PyColorInput { }, } -impl_stub_type!(PyColorInput = String | RgbaColor | RgbColor); - /// A Color object represents a color in the RGB color space with an alpha. Each color channel and the alpha is represented /// as an 8-bit integer. The alpha channel is 0 for fully transparent and 255 for fully opaque. /// diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index 67bb7bdf432..93fd7cfb5d2 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use std::sync::OnceLock; use pyo3::IntoPyObjectExt; -use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyclass_enum, gen_stub_pymethods}; +use pyo3_stub_gen::derive::*; use slint_interpreter::{ComponentHandle, Value}; use i_slint_compiler::langtype::Type; @@ -70,7 +70,6 @@ impl Compiler { self.compiler.set_library_paths(libraries) } - #[setter] fn set_translation_domain(&mut self, domain: String) { self.compiler.set_translation_domain(domain) } @@ -227,6 +226,7 @@ pub struct ComponentDefinition { type_collection: TypeCollection, } +#[gen_stub_pymethods] #[pymethods] impl ComponentDefinition { #[getter] @@ -533,7 +533,7 @@ fn function_to_python_hint(function: &Rc) } #[gen_stub_pyclass] -#[pyclass(module = "slint")] +#[pyclass(module = "slint.core", name = "PropertyInfo")] #[derive(Clone)] pub struct PyPropertyInfo { #[pyo3(get)] @@ -549,7 +549,7 @@ impl PyPropertyInfo { } #[gen_stub_pyclass] -#[pyclass(module = "slint")] +#[pyclass(module = "slint.core", name = "CallbackParameter")] #[derive(Clone)] pub struct PyCallbackParameter { #[pyo3(get)] @@ -566,7 +566,7 @@ impl PyCallbackParameter { } #[gen_stub_pyclass] -#[pyclass(module = "slint")] +#[pyclass(module = "slint.core", name = "CallbackInfo")] #[derive(Clone)] pub struct PyCallbackInfo { #[pyo3(get)] @@ -589,7 +589,7 @@ impl PyCallbackInfo { } #[gen_stub_pyclass] -#[pyclass(module = "slint")] +#[pyclass(module = "slint.core", name = "FunctionInfo")] #[derive(Clone)] pub struct PyFunctionInfo { #[pyo3(get)] @@ -620,6 +620,7 @@ pub struct ComponentInstance { type_collection: TypeCollection, } +#[gen_stub_pymethods] #[pymethods] impl ComponentInstance { #[getter] diff --git a/api/python/slint/models.rs b/api/python/slint/models.rs index 274b45e073d..5bac5da5bee 100644 --- a/api/python/slint/models.rs +++ b/api/python/slint/models.rs @@ -10,6 +10,7 @@ use pyo3::exceptions::PyIndexError; use pyo3::gc::PyVisit; use pyo3::prelude::*; use pyo3::PyTraverseError; +use pyo3_stub_gen::derive::*; use crate::value::{SlintToPyValue, TypeCollection}; @@ -43,6 +44,7 @@ impl PyModelShared { } #[derive(Clone)] +#[gen_stub_pyclass] #[pyclass(unsendable, weakref, subclass)] pub struct PyModelBase { inner: Rc, @@ -54,8 +56,10 @@ impl PyModelBase { } } +#[gen_stub_pymethods] #[pymethods] impl PyModelBase { + #[gen_stub(skip)] #[new] fn new() -> Self { Self { @@ -83,6 +87,7 @@ impl PyModelBase { self.inner.notify.row_removed(index, count) } + #[gen_stub(skip)] fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { self.inner.__traverse__(&visit) } @@ -214,12 +219,14 @@ impl PyModelShared { } } +#[gen_stub_pyclass] #[pyclass(unsendable)] pub struct ReadOnlyRustModel { pub model: ModelRc, pub type_collection: TypeCollection, } +#[gen_stub_pymethods] #[pymethods] impl ReadOnlyRustModel { fn row_count(&self) -> usize { @@ -249,6 +256,7 @@ impl ReadOnlyRustModel { } } +#[gen_stub_pyclass] #[pyclass(unsendable)] struct ReadOnlyRustModelIterator { model: ModelRc, @@ -256,6 +264,7 @@ struct ReadOnlyRustModelIterator { type_collection: TypeCollection, } +#[gen_stub_pymethods] #[pymethods] impl ReadOnlyRustModelIterator { fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { diff --git a/api/python/slint/stub-gen/main.rs b/api/python/slint/stub-gen/main.rs index 7423cced56e..b00ce132e4f 100644 --- a/api/python/slint/stub-gen/main.rs +++ b/api/python/slint/stub-gen/main.rs @@ -1,11 +1,7 @@ -// Copyright © SixtyFPS GmbH -// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - use pyo3_stub_gen::Result; fn main() -> Result<()> { - // `stub_info` is a function defined by `define_stub_info_gatherer!` macro. - let stub = slint_python::stub_info()?; + let stub = core::stub_info()?; stub.generate()?; Ok(()) } diff --git a/api/python/slint/value.rs b/api/python/slint/value.rs index 7652bd361ef..ec49956e168 100644 --- a/api/python/slint/value.rs +++ b/api/python/slint/value.rs @@ -113,6 +113,7 @@ pub struct PyStruct { pub type_collection: TypeCollection, } +#[gen_stub_pymethods] #[pymethods] impl PyStruct { #[gen_stub(override_return_type(type_repr = "typing.Any", imports = ("typing",)))] @@ -149,6 +150,7 @@ impl PyStruct { self.clone() } + #[gen_stub(skip)] fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { traverse_struct(&self.data, &visit) } @@ -204,7 +206,7 @@ pub struct TypeCollection { impl TypeCollection { pub fn new(result: &slint_interpreter::CompilationResult, py: Python<'_>) -> Self { - let mut enum_classes = HashMap::new(); + let mut enum_classes = crate::enums::built_in_enum_classes(py); let enum_ctor = crate::value::enum_class(py); From bb0723609578854ed7bf52c0f223a4c832619e4b Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Mon, 27 Oct 2025 20:52:40 +0800 Subject: [PATCH 20/52] feat: export enums & structs --- api/python/slint/Cargo.toml | 2 + api/python/slint/enums.rs | 88 +++++++++++ api/python/slint/lib.rs | 5 + api/python/slint/slint/api.py | 13 +- api/python/slint/slint/models.py | 2 +- api/python/slint/structs.rs | 247 +++++++++++++++++++++++++++++++ api/python/slint/value.rs | 5 + 7 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 api/python/slint/enums.rs create mode 100644 api/python/slint/structs.rs diff --git a/api/python/slint/Cargo.toml b/api/python/slint/Cargo.toml index 53d03ab8e90..fed08c207a6 100644 --- a/api/python/slint/Cargo.toml +++ b/api/python/slint/Cargo.toml @@ -40,11 +40,13 @@ renderer-skia-opengl = ["slint-interpreter/renderer-skia-opengl"] renderer-skia-vulkan = ["slint-interpreter/renderer-skia-vulkan"] renderer-software = ["slint-interpreter/renderer-software"] accessibility = ["slint-interpreter/accessibility"] +stubgen = [] [dependencies] i-slint-backend-selector = { workspace = true } i-slint-core = { workspace = true, features = ["tr"] } +i-slint-common = { workspace = true } slint-interpreter = { workspace = true, features = ["default", "display-diagnostics", "internal"] } i-slint-compiler = { workspace = true } pyo3 = { version = "0.26", features = ["extension-module", "indexmap", "chrono", "abi3-py311"] } diff --git a/api/python/slint/enums.rs b/api/python/slint/enums.rs new file mode 100644 index 00000000000..af2bf685897 --- /dev/null +++ b/api/python/slint/enums.rs @@ -0,0 +1,88 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +use std::collections::HashMap; + +use pyo3::prelude::*; +use pyo3::sync::PyOnceLock; +#[cfg(feature = "stubgen")] +use pyo3_stub_gen::derive::gen_stub_pyclass_enum; + +static BUILTIN_ENUM_CLASSES: PyOnceLock>> = PyOnceLock::new(); + +macro_rules! generate_enum_support { + ($( + $(#[$enum_attr:meta])* + enum $Name:ident { + $( + $(#[$value_attr:meta])* + $Value:ident, + )* + } + )*) => { + #[cfg(feature = "stubgen")] + pub(super) mod stub_enums { + use super::*; + + $( + #[gen_stub_pyclass_enum] + #[pyclass(module = "slint.core", rename_all = "lowercase")] + #[allow(non_camel_case_types)] + $(#[$enum_attr])* + pub enum $Name { + $( + $(#[$value_attr])* + $Value, + )* + } + )* + } + + fn register_built_in_enums( + py: Python<'_>, + module: &Bound<'_, PyModule>, + enum_base: &Bound<'_, PyAny>, + enum_classes: &mut HashMap>, + ) -> PyResult<()> { + $( + { + let name = stringify!($Name); + let variants = vec![ + $( + { + let value = i_slint_core::items::$Name::$Value.to_string(); + (stringify!($Value).to_ascii_lowercase(), value) + }, + )* + ]; + + let cls = enum_base.call((name, variants), None)?; + let cls_owned = cls.unbind(); + module.add(name, cls_owned.bind(py))?; + enum_classes.insert(name.to_string(), cls_owned); + } + )* + Ok(()) + } + }; +} + +i_slint_common::for_each_enums!(generate_enum_support); + +pub fn register_enums(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { + let enum_base = crate::value::enum_class(py).into_bound(py); + let mut enum_classes: HashMap> = HashMap::new(); + + register_built_in_enums(py, module, &enum_base, &mut enum_classes)?; + + let _ = BUILTIN_ENUM_CLASSES.set(py, enum_classes); + + Ok(()) +} + +pub fn built_in_enum_classes(py: Python<'_>) -> HashMap> { + BUILTIN_ENUM_CLASSES + .get(py) + .map(|map| map.iter().map(|(name, class)| (name.clone(), class.clone_ref(py))).collect()) + .unwrap_or_default() +} diff --git a/api/python/slint/lib.rs b/api/python/slint/lib.rs index 8a662559ede..ffcba98d741 100644 --- a/api/python/slint/lib.rs +++ b/api/python/slint/lib.rs @@ -13,8 +13,10 @@ use interpreter::{ }; mod async_adapter; mod brush; +mod enums; mod errors; mod models; +mod structs; mod timer; mod value; use i_slint_core::translations::Translator; @@ -191,6 +193,9 @@ fn slint_core(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(invoke_from_event_loop, m)?)?; m.add_function(wrap_pyfunction!(init_translations, m)?)?; + enums::register_enums(_py, m)?; + structs::register_structs(_py, m)?; + Ok(()) } diff --git a/api/python/slint/slint/api.py b/api/python/slint/slint/api.py index c4d4342eed5..6ff01e21e12 100644 --- a/api/python/slint/slint/api.py +++ b/api/python/slint/slint/api.py @@ -315,11 +315,11 @@ def load_file( if style is not None: compiler.style = style if include_paths is not None: - compiler.include_paths = include_paths + compiler.include_paths = include_paths # type: ignore[assignment] if library_paths is not None: - compiler.library_paths = library_paths + compiler.library_paths = library_paths # type: ignore[assignment] if translation_domain is not None: - compiler.translation_domain = translation_domain + compiler.set_translation_domain(translation_domain) result = compiler.build_from_path(Path(path)) @@ -336,7 +336,12 @@ def load_file( module = types.SimpleNamespace() for comp_name in result.component_names: - wrapper_class = _build_class(result.component(comp_name)) + comp = result.component(comp_name) + + if comp is None: + continue + + wrapper_class = _build_class(comp) setattr(module, comp_name, wrapper_class) diff --git a/api/python/slint/slint/models.py b/api/python/slint/slint/models.py index 2a8ad7372a3..7f9ce8d14a8 100644 --- a/api/python/slint/slint/models.py +++ b/api/python/slint/slint/models.py @@ -16,7 +16,7 @@ class Model[T](core.PyModelBase, Iterable[T]): Models are iterable and can be used in for loops.""" - def __new__(cls, *args: Any) -> "Model[T]": + def __new__(cls, *args: Any): return super().__new__(cls) def __init__(self) -> None: diff --git a/api/python/slint/structs.rs b/api/python/slint/structs.rs new file mode 100644 index 00000000000..c966c506702 --- /dev/null +++ b/api/python/slint/structs.rs @@ -0,0 +1,247 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyString, PyTuple}; + +use crate::value::TypeCollection; + +#[pyclass(unsendable, module = "slint.core")] +pub struct StructFactory { + name: &'static str, + exported_fields: Vec<&'static str>, + default_data: slint_interpreter::Struct, + type_collection: TypeCollection, +} + +impl StructFactory { + fn new( + name: &'static str, + exported_fields: Vec<&'static str>, + default_data: slint_interpreter::Struct, + type_collection: TypeCollection, + ) -> Self { + Self { name, exported_fields, default_data, type_collection } + } +} + +#[pymethods] +impl StructFactory { + #[pyo3(signature = (*args, **kwargs))] + fn __call__( + &self, + py: Python<'_>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { + if args.len() != 0 { + return Err(PyTypeError::new_err(format!( + "{}() accepts keyword arguments only", + self.name + ))); + } + + let pystruct = Py::new( + py, + crate::value::PyStruct { + data: self.default_data.clone(), + type_collection: self.type_collection.clone(), + }, + )?; + + if let Some(kwargs) = kwargs { + let instance = pystruct.bind(py); + for (key_obj, value) in kwargs.iter() { + let key = key_obj.downcast::().map_err(|_| { + PyTypeError::new_err(format!( + "{}() keyword arguments must be strings", + self.name + )) + })?; + let key_str = key.to_str()?; + if !self.exported_fields.iter().any(|field| *field == key_str) { + return Err(PyTypeError::new_err(format!( + "{}() got an unexpected keyword argument '{}'", + self.name, key_str + ))); + } + instance.setattr(key_str, value)?; + } + } + + Ok(pystruct) + } + + #[getter] + fn __name__(&self) -> &str { + self.name + } +} + +macro_rules! generate_struct_support { + ($( + $(#[$struct_attr:meta])* + struct $Name:ident { + @name = $inner_name:literal + export { + $( $(#[$pub_attr:meta])* $pub_field:ident : $pub_type:ty, )* + } + private { + $( $(#[$pri_attr:meta])* $pri_field:ident : $pri_type:ty, )* + } + } + )*) => { + #[cfg(feature = "stubgen")] + pub(super) mod stub_structs { + use pyo3_stub_gen::{ + inventory, + type_info::{ + MemberInfo, MethodInfo, MethodType, ParameterDefault, ParameterInfo, + ParameterKind, PyClassInfo, PyMethodsInfo, DeprecatedInfo, + }, + TypeInfo, + }; + + const EMPTY_DOC: &str = ""; + const NO_DEFAULT: Option String> = None; + const NO_DEPRECATED: Option = None; + + macro_rules! field_type_info { + (bool) => { || ::type_output() }; + (f32) => { || ::type_output() }; + (f64) => { || ::type_output() }; + (i16) => { || ::type_output() }; + (i32) => { || ::type_output() }; + (u32) => { || ::type_output() }; + (SharedString) => { || ::type_output() }; + (String) => { || ::type_output() }; + (Coord) => { || TypeInfo::builtin("float") }; + (PointerEventButton) => { || TypeInfo::unqualified("PointerEventButton") }; + (PointerEventKind) => { || TypeInfo::unqualified("PointerEventKind") }; + (KeyboardModifiers) => { || TypeInfo::unqualified("KeyboardModifiers") }; + (SortOrder) => { || TypeInfo::unqualified("SortOrder") }; + (MenuEntry) => { || TypeInfo::unqualified("MenuEntry") }; + (LogicalPosition) => { + || TypeInfo::with_module("typing.Tuple[float, float]", "typing".into()) + }; + (Image) => { + || TypeInfo::with_module("slint.Image", "slint".into()) + }; + ($other:ty) => { || TypeInfo::with_module("typing.Any", "typing".into()) }; + } + fn ellipsis_default() -> String { + "...".to_string() + } + + mod markers { + $( + pub struct $Name; + )* + } + + $( + inventory::submit! { + PyClassInfo { + pyclass_name: stringify!($Name), + struct_id: || ::std::any::TypeId::of::(), + module: Some("slint.core"), + doc: EMPTY_DOC, + getters: &[ + $( + MemberInfo { + name: stringify!($pub_field), + r#type: field_type_info!($pub_type), + doc: EMPTY_DOC, + default: NO_DEFAULT, + deprecated: NO_DEPRECATED, + item: false, + }, + )* + ], + setters: &[ + $( + MemberInfo { + name: stringify!($pub_field), + r#type: field_type_info!($pub_type), + doc: EMPTY_DOC, + default: NO_DEFAULT, + deprecated: NO_DEPRECATED, + item: false, + }, + )* + ], + bases: &[], + has_eq: false, + has_ord: false, + has_hash: false, + has_str: false, + subclass: false, + } + } + + inventory::submit! { + PyMethodsInfo { + struct_id: || ::std::any::TypeId::of::(), + attrs: &[], + getters: &[], + setters: &[], + methods: &[MethodInfo { + name: "__init__", + parameters: &[ $( + ParameterInfo { + name: stringify!($pub_field), + kind: ParameterKind::KeywordOnly, + type_info: field_type_info!($pub_type), + default: ParameterDefault::Expr(ellipsis_default), + }, + )* ], + r#return: ::pyo3_stub_gen::type_info::no_return_type_output, + doc: EMPTY_DOC, + r#type: MethodType::Instance, + is_async: false, + deprecated: NO_DEPRECATED, + type_ignored: None, + }], + } + } + )* + } + + fn register_built_in_structs( + py: Python<'_>, + module: &Bound<'_, PyModule>, + type_collection: &TypeCollection, + ) -> PyResult<()> { + $( + { + let name = stringify!($Name); + let default_value: slint_interpreter::Value = + i_slint_core::items::$Name::default().into(); + let data = match default_value { + slint_interpreter::Value::Struct(s) => s, + _ => unreachable!(), + }; + + let factory = StructFactory::new( + name, + vec![ $( stringify!($pub_field), )* ], + data, + type_collection.clone(), + ); + + let factory_py = Py::new(py, factory)?; + module.add(name, factory_py.clone_ref(py))?; + } + )* + Ok(()) + } + }; +} + +i_slint_common::for_each_builtin_structs!(generate_struct_support); + +pub fn register_structs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { + let type_collection = TypeCollection::with_builtin(py); + register_built_in_structs(py, module, &type_collection) +} diff --git a/api/python/slint/value.rs b/api/python/slint/value.rs index ec49956e168..61055b52147 100644 --- a/api/python/slint/value.rs +++ b/api/python/slint/value.rs @@ -240,6 +240,11 @@ impl TypeCollection { Self { enum_classes } } + pub fn with_builtin(py: Python<'_>) -> Self { + let enum_classes = Rc::new(crate::enums::built_in_enum_classes(py)); + Self { enum_classes } + } + pub fn to_py_value(&self, value: slint_interpreter::Value) -> SlintToPyValue { SlintToPyValue { slint_value: value, type_collection: self.clone() } } From 8bf3aba0d79cc9f88ea0b0e13b3adedb46d23762 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Mon, 27 Oct 2025 21:54:39 +0800 Subject: [PATCH 21/52] feat: add tests for enums, structs, and load file functionality --- api/python/slint/slint/codegen/emitters.py | 52 ++-- api/python/slint/slint/loop.py | 2 +- ...file.slint => test-load-file-source.slint} | 1 + api/python/slint/tests/test_enums.py | 102 ++++++- api/python/slint/tests/test_load_file.py | 30 +- .../slint/tests/test_load_file_module.py | 84 ++++++ .../slint/tests/test_load_file_source.py | 76 +++++ .../slint/tests/test_load_file_source.pyi | 279 ++++++++++++++++++ api/python/slint/tests/test_structs.py | 28 ++ 9 files changed, 601 insertions(+), 53 deletions(-) rename api/python/slint/tests/{test-load-file.slint => test-load-file-source.slint} (95%) create mode 100644 api/python/slint/tests/test_load_file_module.py create mode 100644 api/python/slint/tests/test_load_file_source.py create mode 100644 api/python/slint/tests/test_load_file_source.pyi create mode 100644 api/python/slint/tests/test_structs.py diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index 6c5f2939d14..9ff8efa7e8d 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -51,7 +51,7 @@ def _stmt(code: str) -> cst.BaseStatement: include_expr_code = f"[{', '.join(include_exprs)}]" if include_exprs else "None" library_items = [ - f"{repr(Path(name))}: {module_relative_path_expr(module_dir, lib_path)}" + f"{repr(name)}: {module_relative_path_expr(module_dir, lib_path)}" for name, lib_path in config.library_paths.items() ] library_expr_code = f"{{{', '.join(library_items)}}}" if library_items else "None" @@ -122,25 +122,27 @@ def _stmt(code: str) -> cst.BaseStatement: ) body.append(cst.EmptyLine()) - load_source = ( - "def _load() -> types.SimpleNamespace:\n" - ' """Load the compiled Slint module for this package."""\n' - " package = __package__ or (__spec__.parent if __spec__ else None)\n" - " if package:\n" - " ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE))\n" - " else:\n" - " ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE))\n" - " with ctx as slint_path:\n" - f" include_paths: list[os.PathLike[Any] | Path] = {include_expr_code}\n" - f" library_paths: dict[str, os.PathLike[Any] | Path] | None = {library_expr_code}\n" - " return slint.load_file(\n" - " path=slint_path,\n" - " quiet=True,\n" - f" style={style_expr},\n" - " include_paths=include_paths,\n" - " library_paths=library_paths,\n" - f" translation_domain={domain_expr},\n" - " )\n" + load_source = inspect.cleandoc( + f''' + def _load() -> types.SimpleNamespace: + """Load the compiled Slint module for this package.""" + package = __package__ or (__spec__.parent if __spec__ else None) + if package: + ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE)) + else: + ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE)) + with ctx as slint_path: + include_paths: list[os.PathLike[Any] | Path] | None = {include_expr_code} + library_paths: dict[str, os.PathLike[Any] | Path] | None = {library_expr_code} + return slint.load_file( + path=slint_path, + quiet=True, + style={style_expr}, + include_paths=include_paths, + library_paths=library_paths, + translation_domain={domain_expr}, + ) + ''' ) load_func = cst.parse_module(load_source).body[0] @@ -151,7 +153,8 @@ def _stmt(code: str) -> cst.BaseStatement: body.append(cst.EmptyLine()) for original, binding in export_bindings.items(): - body.append(_stmt(f"{binding} = _module.{original}")) + module_attr = _normalize_prop(original) + body.append(_stmt(f"{binding} = _module.{module_attr}")) for orig, alias in artifacts.named_exports: alias_name = _normalize_prop(alias) target = export_bindings.get(orig, _normalize_prop(orig)) @@ -296,13 +299,6 @@ def init_stub() -> cst.FunctionDef: annotation = format_callable_annotation(fn) register_type(annotation) component_body.append(ann_assign(fn.py_name, annotation)) - for global_meta in component.globals: - component_body.append( - ann_assign( - global_meta.py_name, f"{component.py_name}.{global_meta.py_name}" - ) - ) - for global_meta in component.globals: inner_body: list[cst.BaseStatement] = [] if not ( diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index c15c2af5a04..a9ebbff176b 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -216,7 +216,7 @@ def call_soon(self, callback, *args, context=None) -> asyncio.TimerHandle: # ty when=self.time(), callback=callback, args=args, loop=self, context=context ) self._soon_tasks.append(handle) - self.call_later(0, self._flush_soon_tasks) + self.call_later(0, self._flush_soon_tasks) # type: ignore return handle def _flush_soon_tasks(self) -> None: diff --git a/api/python/slint/tests/test-load-file.slint b/api/python/slint/tests/test-load-file-source.slint similarity index 95% rename from api/python/slint/tests/test-load-file.slint rename to api/python/slint/tests/test-load-file-source.slint index 7f9c37be962..3a442937c19 100644 --- a/api/python/slint/tests/test-load-file.slint +++ b/api/python/slint/tests/test-load-file-source.slint @@ -57,6 +57,7 @@ export component App inherits Window { in-out property enum-property: TestEnum.Variant2; in-out property <[TestEnum]> model-with-enums: [TestEnum.Variant2, TestEnum.Variant1]; + in-out property builtin-enum: TextHorizontalAlignment.left; Rectangle { color: red; diff --git a/api/python/slint/tests/test_enums.py b/api/python/slint/tests/test_enums.py index 904107f58d3..d3e57320616 100644 --- a/api/python/slint/tests/test_enums.py +++ b/api/python/slint/tests/test_enums.py @@ -1,21 +1,52 @@ # Copyright © SixtyFPS GmbH # 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 __future__ import annotations + +import importlib.util +import sys from pathlib import Path +from typing import Any + +import pytest +from slint import ListModel, core +from slint.codegen.generator import generate_project +from slint.codegen.models import GenerationConfig +from slint.core import TextHorizontalAlignment, TextVerticalAlignment + +def _slint_source() -> Path: + return Path(__file__).with_name("test-load-file-source.slint") -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 +@pytest.fixture +def generated_module(tmp_path: Path) -> Any: + slint_file = _slint_source() + output_dir = tmp_path / "generated" + config = GenerationConfig( + include_paths=[slint_file.parent], + library_paths={}, + style=None, + translation_domain=None, + quiet=True, + ) + generate_project(inputs=[slint_file], output_dir=output_dir, config=config) -def test_enums() -> None: - module = load_file(base_dir() / "test-load-file.slint", quiet=False) + module_path = output_dir / "test_load_file_source.py" + assert module_path.exists() + + spec = importlib.util.spec_from_file_location("generated_test_load_file", module_path) + assert spec and spec.loader + + sys.modules.pop(spec.name, None) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) # type: ignore[arg-type] + return module + + +def test_enums(generated_module: Any) -> None: + module = generated_module TestEnum = module.TestEnum @@ -40,10 +71,59 @@ def test_enums() -> None: 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 + model_with_enums = None # allow GC to drop reference 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 + del instance + + +def test_builtin_enums_exposed() -> None: + assert TextHorizontalAlignment.left.name == "left" + assert TextVerticalAlignment.top.name == "top" + assert TextHorizontalAlignment.left != TextHorizontalAlignment.right + + +def test_builtin_enum_property_roundtrip() -> None: + compiler = core.Compiler() + comp = compiler.build_from_source( + """ + export component Test { + in-out property horizontal: TextHorizontalAlignment.left; + in-out property vertical: TextVerticalAlignment.top; + Text { + horizontal-alignment: root.horizontal; + vertical-alignment: root.vertical; + } + } + """, + Path(""), + ).component("Test") + + assert comp is not None + instance = comp.create() + assert instance is not None + + assert instance.get_property("horizontal") == TextHorizontalAlignment.left + assert instance.get_property("vertical") == TextVerticalAlignment.top + + instance.set_property("horizontal", TextHorizontalAlignment.right) + instance.set_property("vertical", TextVerticalAlignment.bottom) + + assert instance.get_property("horizontal") == TextHorizontalAlignment.right + assert instance.get_property("vertical") == TextVerticalAlignment.bottom + + +def test_builtin_enum_keyword_variants_have_safe_names() -> None: + keyword_enums = ( + core.AccessibleRole, + core.DialogButtonRole, + ) + + for enum_cls in keyword_enums: + members = enum_cls.__members__ + assert "none" in members + assert members["none"].value == "none" diff --git a/api/python/slint/tests/test_load_file.py b/api/python/slint/tests/test_load_file.py index 26e603a73b0..453203fd3e0 100644 --- a/api/python/slint/tests/test_load_file.py +++ b/api/python/slint/tests/test_load_file.py @@ -1,10 +1,11 @@ # Copyright © SixtyFPS GmbH # 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, CompileError from pathlib import Path +import pytest +from slint import CompileError, load_file + def base_dir() -> Path: origin = __spec__.origin @@ -15,21 +16,24 @@ def base_dir() -> Path: def test_load_file(caplog: pytest.LogCaptureFixture) -> None: - module = load_file(base_dir() / "test-load-file.slint", quiet=False) + module = load_file(base_dir() / "test-load-file-source.slint", quiet=False) assert ( "The property 'color' has been deprecated. Please use 'background' instead" in caplog.text ) - 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__ + module_keys = set(module.__dict__.keys()) + expected_keys = { + "App", + "Diag", + "MyDiag", + "MyData", + "Secret_Struct", + "Public_Struct", + "TestEnum", + } + assert expected_keys.issubset(module_keys) instance = module.App() del instance instance = module.MyDiag() @@ -71,7 +75,7 @@ def test_compile_error() -> None: def test_load_file_wrapper() -> None: - module = load_file(base_dir() / "test-load-file.slint", quiet=False) + module = load_file(base_dir() / "test-load-file-source.slint", quiet=False) instance = module.App() @@ -94,7 +98,7 @@ def test_load_file_wrapper() -> None: def test_constructor_kwargs() -> None: - module = load_file(base_dir() / "test-load-file.slint", quiet=False) + module = load_file(base_dir() / "test-load-file-source.slint", quiet=False) def early_say_hello(arg: str) -> str: return "early:" + arg diff --git a/api/python/slint/tests/test_load_file_module.py b/api/python/slint/tests/test_load_file_module.py new file mode 100644 index 00000000000..744b145db39 --- /dev/null +++ b/api/python/slint/tests/test_load_file_module.py @@ -0,0 +1,84 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +from __future__ import annotations + +import importlib +from types import ModuleType +from typing import TYPE_CHECKING + +import test_load_file_source as generated_module + + +def _module(): + if TYPE_CHECKING: + return generated_module + + # Reload to ensure a fresh module for each call + return importlib.reload(generated_module) + + +def test_codegen_module_exports() -> None: + module = _module() + + expected_exports = { + "App", + "Diag", + "MyDiag", + "MyData", + "Secret_Struct", + "Public_Struct", + "TestEnum", + } + assert expected_exports.issubset(set(module.__all__)) + + assert module.MyDiag is module.Diag + assert module.Public_Struct is module.Secret_Struct + + test_enum = module.TestEnum + assert test_enum.Variant1.name == "Variant1" + + instance = module.App() + 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_generated_module_wrapper() -> None: + module = _module() + + instance = module.App() + + assert instance.hello == "World" + instance.hello = "Ok" + assert instance.hello == "Ok" + + instance.say_hello = lambda x: "from here: " + x + assert instance.say_hello("wohoo") == "from here: wohoo" + + assert instance.plus_one(42) == 43 + + assert instance.MyGlobal.global_prop == "This is global" + assert instance.MyGlobal.minus_one(100) == 99 + assert instance.SecondGlobal.second == "second" + + del instance + + +def test_constructor_kwargs() -> None: + module = _module() + + def early_say_hello(arg: str) -> str: + return "early:" + arg + + instance = module.App(hello="Set early", say_hello=early_say_hello) + + assert instance.hello == "Set early" + assert instance.invoke_say_hello("test") == "early:test" + + del instance diff --git a/api/python/slint/tests/test_load_file_source.py b/api/python/slint/tests/test_load_file_source.py new file mode 100644 index 00000000000..70fe826f493 --- /dev/null +++ b/api/python/slint/tests/test_load_file_source.py @@ -0,0 +1,76 @@ +# Generated by slint.codegen from test-load-file-source.slint +from __future__ import annotations + +import importlib.resources as _resources +import os +import types +from contextlib import nullcontext as _nullcontext +from pathlib import Path +from typing import Any + +import slint + +__all__ = ['Diag', 'App', 'Secret_Struct', 'MyData', 'ImageFit', 'TextHorizontalAlignment', 'SortOrder', 'PointerEventKind', 'ImageRendering', 'ImageTiling', 'OperatingSystemType', 'InputType', 'Orientation', 'TextWrap', 'ScrollBarPolicy', 'ImageVerticalAlignment', 'PointerEventButton', 'TextOverflow', 'LayoutAlignment', 'StandardButtonKind', 'AccessibleRole', 'EventResult', 'MouseCursor', 'AnimationDirection', 'TextVerticalAlignment', 'TextStrokeStyle', 'LineCap', 'ImageHorizontalAlignment', 'FocusReason', 'FillRule', 'ColorScheme', 'PathEvent', 'TestEnum', 'DialogButtonRole', 'PopupClosePolicy', 'MyDiag', 'Public_Struct'] + +_MODULE_DIR = Path(__file__).parent + +_SLINT_RESOURCE = 'test-load-file-source.slint' + +def _load() -> types.SimpleNamespace: + """Load the compiled Slint module for this package.""" + package = __package__ or (__spec__.parent if __spec__ else None) + if package: + ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE)) + else: + ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE)) + with ctx as slint_path: + include_paths: list[os.PathLike[Any] | Path] | None = None + library_paths: dict[str, os.PathLike[Any] | Path] | None = None + return slint.load_file( + path=slint_path, + quiet=True, + style=None, + include_paths=include_paths, + library_paths=library_paths, + translation_domain=None, + ) + +_module = _load() + +Diag = _module.Diag +App = _module.App +Secret_Struct = _module.Secret_Struct +MyData = _module.MyData +ImageFit = _module.ImageFit +TextHorizontalAlignment = _module.TextHorizontalAlignment +SortOrder = _module.SortOrder +PointerEventKind = _module.PointerEventKind +ImageRendering = _module.ImageRendering +ImageTiling = _module.ImageTiling +OperatingSystemType = _module.OperatingSystemType +InputType = _module.InputType +Orientation = _module.Orientation +TextWrap = _module.TextWrap +ScrollBarPolicy = _module.ScrollBarPolicy +ImageVerticalAlignment = _module.ImageVerticalAlignment +PointerEventButton = _module.PointerEventButton +TextOverflow = _module.TextOverflow +LayoutAlignment = _module.LayoutAlignment +StandardButtonKind = _module.StandardButtonKind +AccessibleRole = _module.AccessibleRole +EventResult = _module.EventResult +MouseCursor = _module.MouseCursor +AnimationDirection = _module.AnimationDirection +TextVerticalAlignment = _module.TextVerticalAlignment +TextStrokeStyle = _module.TextStrokeStyle +LineCap = _module.LineCap +ImageHorizontalAlignment = _module.ImageHorizontalAlignment +FocusReason = _module.FocusReason +FillRule = _module.FillRule +ColorScheme = _module.ColorScheme +PathEvent = _module.PathEvent +TestEnum = _module.TestEnum +DialogButtonRole = _module.DialogButtonRole +PopupClosePolicy = _module.PopupClosePolicy +MyDiag = Diag +Public_Struct = Secret_Struct diff --git a/api/python/slint/tests/test_load_file_source.pyi b/api/python/slint/tests/test_load_file_source.pyi new file mode 100644 index 00000000000..0381742bdbf --- /dev/null +++ b/api/python/slint/tests/test_load_file_source.pyi @@ -0,0 +1,279 @@ +from __future__ import annotations + +import enum + +from typing import Any, Callable + +import slint + +__all__ = ['Diag', 'App', 'Secret_Struct', 'MyData', 'ImageFit', 'TextHorizontalAlignment', 'SortOrder', 'PointerEventKind', 'ImageRendering', 'ImageTiling', 'OperatingSystemType', 'InputType', 'Orientation', 'TextWrap', 'ScrollBarPolicy', 'ImageVerticalAlignment', 'PointerEventButton', 'TextOverflow', 'LayoutAlignment', 'StandardButtonKind', 'AccessibleRole', 'EventResult', 'MouseCursor', 'AnimationDirection', 'TextVerticalAlignment', 'TextStrokeStyle', 'LineCap', 'ImageHorizontalAlignment', 'FocusReason', 'FillRule', 'ColorScheme', 'PathEvent', 'TestEnum', 'DialogButtonRole', 'PopupClosePolicy', 'MyDiag', 'Public_Struct'] + +class Secret_Struct: + def __init__(self, **kwargs: Any) -> None: + ... + balance: float + +class MyData: + def __init__(self, **kwargs: Any) -> None: + ... + age: float + name: str + +class ImageFit(enum.Enum): + fill = 'fill' + contain = 'contain' + cover = 'cover' + preserve = 'preserve' + +class TextHorizontalAlignment(enum.Enum): + left = 'left' + center = 'center' + right = 'right' + +class SortOrder(enum.Enum): + unsorted = 'unsorted' + ascending = 'ascending' + descending = 'descending' + +class PointerEventKind(enum.Enum): + cancel = 'cancel' + down = 'down' + up = 'up' + move = 'move' + +class ImageRendering(enum.Enum): + smooth = 'smooth' + pixelated = 'pixelated' + +class ImageTiling(enum.Enum): + none = 'none' + repeat = 'repeat' + round = 'round' + +class OperatingSystemType(enum.Enum): + android = 'android' + ios = 'ios' + macos = 'macos' + linux = 'linux' + windows = 'windows' + other = 'other' + +class InputType(enum.Enum): + text = 'text' + password = 'password' + number = 'number' + decimal = 'decimal' + +class Orientation(enum.Enum): + horizontal = 'horizontal' + vertical = 'vertical' + +class TextWrap(enum.Enum): + nowrap = 'nowrap' + wordwrap = 'wordwrap' + charwrap = 'charwrap' + +class ScrollBarPolicy(enum.Enum): + asneeded = 'asneeded' + alwaysoff = 'alwaysoff' + alwayson = 'alwayson' + +class ImageVerticalAlignment(enum.Enum): + center = 'center' + top = 'top' + bottom = 'bottom' + +class PointerEventButton(enum.Enum): + other = 'other' + left = 'left' + right = 'right' + middle = 'middle' + back = 'back' + forward = 'forward' + +class TextOverflow(enum.Enum): + clip = 'clip' + elide = 'elide' + +class LayoutAlignment(enum.Enum): + stretch = 'stretch' + center = 'center' + start = 'start' + end = 'end' + spacebetween = 'spacebetween' + spacearound = 'spacearound' + spaceevenly = 'spaceevenly' + +class StandardButtonKind(enum.Enum): + ok = 'ok' + cancel = 'cancel' + apply = 'apply' + close = 'close' + reset = 'reset' + help = 'help' + yes = 'yes' + no = 'no' + abort = 'abort' + retry = 'retry' + ignore = 'ignore' + +class AccessibleRole(enum.Enum): + none = 'none' + button = 'button' + checkbox = 'checkbox' + combobox = 'combobox' + groupbox = 'groupbox' + image = 'image' + list = 'list' + slider = 'slider' + spinbox = 'spinbox' + tab = 'tab' + tablist = 'tablist' + tabpanel = 'tabpanel' + text = 'text' + table = 'table' + tree = 'tree' + progressindicator = 'progressindicator' + textinput = 'textinput' + switch = 'switch' + listitem = 'listitem' + +class EventResult(enum.Enum): + reject = 'reject' + accept = 'accept' + +class MouseCursor(enum.Enum): + default = 'default' + none = 'none' + help = 'help' + pointer = 'pointer' + progress = 'progress' + wait = 'wait' + crosshair = 'crosshair' + text = 'text' + alias = 'alias' + copy = 'copy' + move = 'move' + nodrop = 'nodrop' + notallowed = 'notallowed' + grab = 'grab' + grabbing = 'grabbing' + colresize = 'colresize' + rowresize = 'rowresize' + nresize = 'nresize' + eresize = 'eresize' + sresize = 'sresize' + wresize = 'wresize' + neresize = 'neresize' + nwresize = 'nwresize' + seresize = 'seresize' + swresize = 'swresize' + ewresize = 'ewresize' + nsresize = 'nsresize' + neswresize = 'neswresize' + nwseresize = 'nwseresize' + +class AnimationDirection(enum.Enum): + normal = 'normal' + reverse = 'reverse' + alternate = 'alternate' + alternatereverse = 'alternatereverse' + +class TextVerticalAlignment(enum.Enum): + top = 'top' + center = 'center' + bottom = 'bottom' + +class TextStrokeStyle(enum.Enum): + outside = 'outside' + center = 'center' + +class LineCap(enum.Enum): + butt = 'butt' + round = 'round' + square = 'square' + +class ImageHorizontalAlignment(enum.Enum): + center = 'center' + left = 'left' + right = 'right' + +class FocusReason(enum.Enum): + programmatic = 'programmatic' + tabnavigation = 'tabnavigation' + pointerclick = 'pointerclick' + popupactivation = 'popupactivation' + windowactivation = 'windowactivation' + +class FillRule(enum.Enum): + nonzero = 'nonzero' + evenodd = 'evenodd' + +class ColorScheme(enum.Enum): + unknown = 'unknown' + dark = 'dark' + light = 'light' + +class PathEvent(enum.Enum): + begin = 'begin' + line = 'line' + quadratic = 'quadratic' + cubic = 'cubic' + endopen = 'endopen' + endclosed = 'endclosed' + +class TestEnum(enum.Enum): + Variant1 = 'Variant1' + Variant2 = 'Variant2' + +class DialogButtonRole(enum.Enum): + none = 'none' + accept = 'accept' + reject = 'reject' + apply = 'apply' + reset = 'reset' + help = 'help' + action = 'action' + +class PopupClosePolicy(enum.Enum): + closeonclick = 'closeonclick' + closeonclickoutside = 'closeonclickoutside' + noautoclose = 'noautoclose' + +class Diag(slint.Component): + def __init__(self, **kwargs: Any) -> None: + ... + class MyGlobal: + global_prop: str + global_callback: Callable[[str], str] + minus_one: Callable[[int], None] + class SecondGlobal: + second: str + +class App(slint.Component): + def __init__(self, **kwargs: Any) -> None: + ... + builtin_enum: Any + enum_property: Any + hello: str + model_with_enums: slint.ListModel[Any] + translated: str + call_void: Callable[[], None] + invoke_call_void: Callable[[], None] + invoke_global_callback: Callable[[str], str] + invoke_say_hello: Callable[[str], str] + invoke_say_hello_again: Callable[[str], str] + say_hello: Callable[[str], str] + say_hello_again: Callable[[str], str] + plus_one: Callable[[int], None] + class MyGlobal: + global_prop: str + global_callback: Callable[[str], str] + minus_one: Callable[[int], None] + class SecondGlobal: + second: str + +MyDiag = Diag + +Public_Struct = Secret_Struct + diff --git a/api/python/slint/tests/test_structs.py b/api/python/slint/tests/test_structs.py new file mode 100644 index 00000000000..b25b961a736 --- /dev/null +++ b/api/python/slint/tests/test_structs.py @@ -0,0 +1,28 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +from slint.core import ( + KeyboardModifiers, + PointerEvent, + PointerEventButton, + PointerEventKind, +) + + +def test_keyboard_modifiers_ctor() -> None: + mods = KeyboardModifiers(control=True) + assert mods.control is True + assert mods.alt is False + + +def test_pointer_event_ctor_returns_struct() -> None: + mods = KeyboardModifiers(alt=True) + event = PointerEvent( + button=PointerEventButton.left, + kind=PointerEventKind.down, + modifiers=mods, + ) + + assert event.button == PointerEventButton.left + assert event.kind == PointerEventKind.down + assert event.modifiers.alt is True From a10ff5fac6a7d64ce20480d72ebd805224bf5561 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Mon, 27 Oct 2025 21:57:32 +0800 Subject: [PATCH 22/52] fix(python): assignment breaks typeddict --- api/python/slint/tests/test_brush.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/api/python/slint/tests/test_brush.py b/api/python/slint/tests/test_brush.py index bf0e668d971..caa4356a0b9 100644 --- a/api/python/slint/tests/test_brush.py +++ b/api/python/slint/tests/test_brush.py @@ -22,8 +22,7 @@ def test_col_from_str() -> None: def test_col_from_rgb_dict() -> None: - coldict = {"red": 0x12, "green": 0x34, "blue": 0x56} - col = Color(coldict) + col = Color({"red": 0x12, "green": 0x34, "blue": 0x56}) assert col.red == 0x12 assert col.green == 0x34 assert col.blue == 0x56 @@ -31,8 +30,7 @@ def test_col_from_rgb_dict() -> None: def test_col_from_rgba_dict() -> None: - coldict = {"red": 0x12, "green": 0x34, "blue": 0x56, "alpha": 128} - col = Color(coldict) + col = Color({"red": 0x12, "green": 0x34, "blue": 0x56, "alpha": 128}) assert col.red == 0x12 assert col.green == 0x34 assert col.blue == 0x56 From 145e74fd4791917a6d8cc1bcdba7233cfc81f0ae Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Mon, 27 Oct 2025 21:59:41 +0800 Subject: [PATCH 23/52] feat(python): codegen dependency tracking --- api/python/slint/interpreter.rs | 21 ++- api/python/slint/slint/codegen/emitters.py | 181 ++++++++++++++++++++ api/python/slint/slint/codegen/generator.py | 123 ++++++++++--- api/python/slint/slint/codegen/models.py | 1 + internal/interpreter/api.rs | 8 + internal/interpreter/dynamic_item_tree.rs | 39 ++++- 6 files changed, 344 insertions(+), 29 deletions(-) diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index 93fd7cfb5d2..2670cdb099e 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -7,6 +7,9 @@ use std::path::PathBuf; use std::rc::Rc; use std::sync::OnceLock; +use i_slint_core::api::CloseRequestResponse; +use i_slint_core::platform::WindowEvent; + use pyo3::IntoPyObjectExt; use pyo3_stub_gen::derive::*; use slint_interpreter::{ComponentHandle, Value}; @@ -217,6 +220,11 @@ impl CompilationResult { fn named_exports(&self) -> Vec<(String, String)> { self.result.named_exports(i_slint_core::InternalToken {}).cloned().collect::>() } + + #[getter] + fn resource_paths(&self) -> Vec { + self.result.dependencies().cloned().collect() + } } #[gen_stub_pyclass] @@ -290,20 +298,20 @@ impl ComponentDefinition { .collect() } - fn global_properties(&self, name: &str) -> Option> { + fn global_properties(&self, name: &str) -> IndexMap { self.definition.global_properties_and_callbacks(name).map(|propiter| { propiter .filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (name, ty.into()))) .collect() - }) + }).unwrap_or_default() } - fn global_callbacks(&self, name: &str) -> Option> { - self.definition.global_callbacks(name).map(|callbackiter| callbackiter.collect()) + fn global_callbacks(&self, name: &str) -> Vec { + self.definition.global_callbacks(name).map(|callbackiter| callbackiter.collect()).unwrap_or_default() } - fn global_functions(&self, name: &str) -> Option> { - self.definition.global_functions(name).map(|functioniter| functioniter.collect()) + fn global_functions(&self, name: &str) -> Vec { + self.definition.global_functions(name).map(|functioniter| functioniter.collect()).unwrap_or_default() } fn global_property_infos(&self, global_name: &str) -> Option> { @@ -732,6 +740,7 @@ impl ComponentInstance { Ok(self.instance.hide()?) } + #[gen_stub(skip)] fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { self.callbacks.__traverse__(&visit)?; for global_callbacks in self.global_callbacks.values() { diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index 9ff8efa7e8d..162ce59645b 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -31,6 +31,172 @@ def module_relative_path_expr(module_dir: Path, target: Path) -> str: return expr +def _write_struct_python_module( + path: Path, + *, + source_relative: str, + resource_name: str, + export_items: list[str], + include_expr_code: str, + library_expr_code: str, + style_expr: str, + domain_expr: str, + artifacts: ModuleArtifacts, + export_bindings: dict[str, str], +) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + header: list[cst.CSTNode] = [ + cst.EmptyLine( + comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}") + ) + ] + + body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + cst.EmptyLine(), + _stmt("import importlib.resources as _resources"), + _stmt("import os"), + _stmt("import types"), + _stmt("from contextlib import nullcontext as _nullcontext"), + _stmt("from pathlib import Path"), + _stmt("from typing import Any"), + cst.EmptyLine(), + _stmt("import slint"), + _stmt("from slint import core as _core"), + _stmt("from slint.api import _build_struct"), + cst.EmptyLine(), + ] + + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(item))) for item in export_items] + ) + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], value=all_list + ) + ] + ) + ) + body.append(cst.EmptyLine()) + + body.append(_stmt("_MODULE_DIR = Path(__file__).parent")) + body.append(cst.EmptyLine()) + + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("_SLINT_RESOURCE"))], + value=cst.SimpleString(repr(resource_name)), + ) + ] + ) + ) + body.append(cst.EmptyLine()) + + struct_lines: list[str] = [] + for struct in artifacts.structs: + struct_lines.append( + f" if {struct.name!r} in structs:\n" + f" module.{struct.py_name} = _build_struct({struct.py_name!r}, structs[{struct.name!r}])" + ) + + enum_lines: list[str] = [] + for enum in artifacts.enums: + enum_lines.append( + f" if {enum.name!r} in enums:\n" + f" module.{enum.py_name} = enums[{enum.name!r}]" + ) + + alias_lines: list[str] = [] + for orig, alias in artifacts.named_exports: + alias_name = _normalize_prop(alias) + target_name = export_bindings.get(orig, _normalize_prop(orig)) + alias_lines.append( + f" if hasattr(module, {target_name!r}):\n" + f" module.{alias_name} = getattr(module, {target_name!r})" + ) + + with_lines = [ + "with ctx as slint_path:", + f" include_paths: list[os.PathLike[Any] | Path] | None = {include_expr_code}", + f" library_paths: dict[str, os.PathLike[Any] | Path] | None = {library_expr_code}", + f" style = {style_expr}", + f" translation_domain = {domain_expr}", + " compiler = _core.Compiler()", + " if include_paths is not None:", + " compiler.include_paths = include_paths # type: ignore[assignment]", + " if library_paths is not None:", + " compiler.library_paths = library_paths # type: ignore[assignment]", + " if style is not None:", + " compiler.style = style", + " if translation_domain is not None:", + " compiler.set_translation_domain(translation_domain)", + " result = compiler.build_from_path(slint_path)", + " diagnostics = result.diagnostics", + " errors = [", + " diag", + " for diag in diagnostics", + " if diag.level == _core.DiagnosticLevel.Error", + " and diag.message != 'No component found'", + " ]", + " if errors:", + f" raise slint.CompileError({source_relative!r}, diagnostics)", + " module = types.SimpleNamespace()", + " structs, enums = result.structs_and_enums", + *struct_lines, + *enum_lines, + *alias_lines, + " return module", + ] + + load_statements: list[cst.BaseStatement] = [ + _stmt('"""Load struct and enum definitions for this package."""'), + _stmt("package = __package__ or (__spec__.parent if __spec__ else None)"), + _stmt( + "if package:\n" + " ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE))\n" + "else:\n" + " ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE))" + ), + _stmt("\n".join(with_lines)), + ] + + load_func = cst.FunctionDef( + name=cst.Name("_load"), + params=cst.Parameters(), + returns=cst.Annotation( + annotation=cst.Attribute( + value=cst.Name("types"), + attr=cst.Name("SimpleNamespace"), + ) + ), + body=cst.IndentedBlock(body=load_statements), + ) + + body.append(load_func) + body.append(cst.EmptyLine()) + body.append(_stmt("_module = _load()")) + body.append(cst.EmptyLine()) + + for original, binding in export_bindings.items(): + module_attr = _normalize_prop(original) + body.append(_stmt(f"{binding} = _module.{module_attr}")) + + for orig, alias in artifacts.named_exports: + alias_name = _normalize_prop(alias) + target = export_bindings.get(orig, _normalize_prop(orig)) + body.append(_stmt(f"{alias_name} = {target}")) + + module = cst.Module(body=header + body) # type: ignore[arg-type] + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(module.code, encoding="utf-8") + + def write_python_module( path: Path, *, @@ -73,6 +239,21 @@ def _stmt(code: str) -> cst.BaseStatement: _normalize_prop(alias) for _, alias in artifacts.named_exports ] + if not artifacts.components: + _write_struct_python_module( + path, + source_relative=source_relative, + resource_name=resource_name, + export_items=export_items, + include_expr_code=include_expr_code, + library_expr_code=library_expr_code, + style_expr=style_expr, + domain_expr=domain_expr, + artifacts=artifacts, + export_bindings=export_bindings, + ) + return + header: list[cst.CSTNode] = [ cst.EmptyLine( comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}") diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index de4ea6469ad..ce594035267 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -38,6 +38,19 @@ def generate_project( if not files: raise SystemExit("No .slint files found in the supplied inputs") + copied_slint: set[Path] = set() + generated_modules = 0 + struct_only_modules = 0 + failed_files: list[Path] = [] + + def copy_slint_file(source: Path, destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + resolved = destination.resolve() + if resolved in copied_slint: + return + shutil.copy2(source, destination) + copied_slint.add(resolved) + if output_dir is not None: output_dir.mkdir(parents=True, exist_ok=True) @@ -50,19 +63,23 @@ def generate_project( if config.library_paths: compiler.library_paths = config.library_paths.copy() # type: ignore[assignment] if config.translation_domain: - compiler.translation_domain = config.translation_domain + compiler.set_translation_domain(config.translation_domain) for source_path, root in files: - compilation = _compile_slint(compiler, source_path, config) + source_resolved = source_path.resolve() + relative = source_path.relative_to(root) + compilation = _compile_slint(compiler, root, source_path, config) if compilation is None: + failed_files.append(relative) continue artifacts = _collect_metadata(compilation) - relative = source_path.relative_to(root) + + sanitized_stem = _normalize_prop(source_path.stem) if output_dir is None: module_dir = source_path.parent - target_stem = module_dir / source_path.stem + target_stem = module_dir / sanitized_stem copy_slint = False slint_destination = source_path resource_name = source_path.name @@ -70,9 +87,9 @@ def generate_project( else: module_dir = output_dir / relative.parent module_dir.mkdir(parents=True, exist_ok=True) - target_stem = module_dir / relative.stem + target_stem = module_dir / sanitized_stem copy_slint = True - slint_destination = target_stem.with_suffix(".slint") + slint_destination = module_dir / relative.name resource_name = relative.name source_descriptor = str(relative) @@ -86,7 +103,51 @@ def generate_project( write_stub_module(target_stem.with_suffix(".pyi"), artifacts=artifacts) if copy_slint and slint_destination != source_path: - shutil.copy2(source_path, slint_destination) + copy_slint_file(source_path, slint_destination) + + if output_dir is not None: + for dependency in artifacts.resource_paths: + dep_path = Path(dependency) + if not dep_path.exists(): + continue + dep_resolved = dep_path.resolve() + if dep_resolved == source_resolved: + continue + relative_dep: Path | None = None + for source_root in source_roots: + try: + relative_dep = dep_resolved.relative_to(source_root) + break + except ValueError: + continue + if relative_dep is None: + continue + destination = output_dir / relative_dep + copy_slint_file(dep_resolved, destination) + + generated_modules += 1 + if not artifacts.components: + struct_only_modules += 1 + + summary_lines: list[str] = [] + struct_note = f" ({struct_only_modules} struct-only)" if struct_only_modules else "" + summary_lines.append(f"info: Generated {generated_modules} Python module(s){struct_note}") + + if output_dir is not None: + summary_lines.append( + f"info: Copied {len(copied_slint)} .slint file(s) into {output_dir}" + ) + + if failed_files: + sample = ", ".join(str(path) for path in failed_files[:3]) + if len(failed_files) > 3: + sample += ", ..." + summary_lines.append( + f"info: Skipped {len(failed_files)} file(s) due to errors ({sample})" + ) + + for line in summary_lines: + print(line) def _discover_slint_files(inputs: Iterable[Path]) -> Iterable[tuple[Path, Path]]: @@ -103,6 +164,7 @@ def _discover_slint_files(inputs: Iterable[Path]) -> Iterable[tuple[Path, Path]] def _compile_slint( compiler: Compiler, + root: Path, source_path: Path, config: GenerationConfig, ) -> CompilationResult | None: @@ -120,15 +182,31 @@ def is_error(diag: PyDiagnostic) -> bool: else: warnings.append(diag) + non_fatal_errors: list[PyDiagnostic] = [] + fatal_errors: list[PyDiagnostic] = [] + + for err in errors: + # Files that only export structs/globals yield this diagnostic. We can still collect + # metadata for them, so treat it as a warning for generation purposes. + if err.message == "No component found": + non_fatal_errors.append(err) + else: + fatal_errors.append(err) + + warnings.extend(non_fatal_errors) + + source_relative = str(source_path.relative_to(root)) + if warnings and not config.quiet: - for diag in warnings: - print(f"warning: {diag}") + print(f"info: Compilation of {source_relative} completed with warnings:") + for warn in warnings: + print(f" warning: {warn}") - if errors: - for diag in errors: - print(f"error: {diag}") - print(f"Skipping generation for {source_path}") - return None + if fatal_errors: + print(f"error: Compilation of {source_relative} failed & skiped with errors:") + for fatal in fatal_errors: + print(f" error: {fatal}") + return return result @@ -139,6 +217,9 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: for name in result.component_names: comp = result.component(name) + if comp is None: + continue + property_info = {info.name: info for info in comp.property_infos()} callback_info = {info.name: info for info in comp.callback_infos()} function_info = {info.name: info for info in comp.function_infos()} @@ -161,17 +242,17 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: globals_meta: list[GlobalMeta] = [] for global_name in comp.globals: global_property_info = { - info.name: info for info in comp.global_property_infos(global_name) + info.name: info for info in comp.global_property_infos(global_name) or [] } global_callback_info = { - info.name: info for info in comp.global_callback_infos(global_name) + info.name: info for info in comp.global_callback_infos(global_name) or [] } global_function_info = { - info.name: info for info in comp.global_function_infos(global_name) + info.name: info for info in comp.global_function_infos(global_name) or [] } properties_meta: list[PropertyMeta] = [] - for key in comp.global_properties(global_name): + for key in comp.global_properties(global_name) or []: py_key = _normalize_prop(key) info = global_property_info[key] type_hint = info.python_type @@ -185,12 +266,12 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: callbacks_meta = [ _callback_meta(cb, global_callback_info[cb]) - for cb in comp.global_callbacks(global_name) + for cb in comp.global_callbacks(global_name) or [] ] functions_meta = [ _callback_meta(fn, global_function_info[fn]) - for fn in comp.global_functions(global_name) + for fn in comp.global_functions(global_name) or [] ] globals_meta.append( @@ -255,12 +336,14 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: ) named_exports = [(orig, alias) for orig, alias in result.named_exports] + resource_paths = [Path(path) for path in result.resource_paths] return ModuleArtifacts( components=components, structs=structs_meta, enums=enums_meta, named_exports=named_exports, + resource_paths=resource_paths, ) diff --git a/api/python/slint/slint/codegen/models.py b/api/python/slint/slint/codegen/models.py index a4e37b7bfcf..65b9ff0a9ab 100644 --- a/api/python/slint/slint/codegen/models.py +++ b/api/python/slint/slint/codegen/models.py @@ -85,6 +85,7 @@ class ModuleArtifacts: structs: List[StructMeta] enums: List[EnumMeta] named_exports: List[tuple[str, str]] + resource_paths: List[Path] __all__ = [ diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index 1b1de962a85..7344c6f0bae 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -826,6 +826,7 @@ impl Compiler { return CompilationResult { components: HashMap::new(), diagnostics: diagnostics.into_iter().collect(), + dependencies: Vec::new(), #[cfg(feature = "internal")] structs_and_enums: Vec::new(), #[cfg(feature = "internal")] @@ -864,6 +865,7 @@ impl Compiler { pub struct CompilationResult { pub(crate) components: HashMap, pub(crate) diagnostics: Vec, + pub(crate) dependencies: Vec, #[cfg(feature = "internal")] pub(crate) structs_and_enums: Vec, /// For `export { Foo as Bar }` this vec contains tuples of (`Foo`, `Bar`) @@ -876,6 +878,7 @@ impl core::fmt::Debug for CompilationResult { f.debug_struct("CompilationResult") .field("components", &self.components.keys()) .field("diagnostics", &self.diagnostics) + .field("dependencies", &self.dependencies) .finish() } } @@ -920,6 +923,11 @@ impl CompilationResult { self.components.get(name).cloned() } + /// Returns an iterator over the file dependencies that were used during compilation. + pub fn dependencies(&self) -> impl Iterator { + self.dependencies.iter() + } + /// This is an internal function without API stability guarantees. #[doc(hidden)] #[cfg(feature = "internal")] diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index e60284415a3..7b3dc36319f 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -39,9 +39,9 @@ use i_slint_core::{Brush, Color, Property, SharedString, SharedVector}; use itertools::Either; use once_cell::unsync::{Lazy, OnceCell}; use smol_str::{SmolStr, ToSmolStr}; -use std::collections::BTreeMap; -use std::collections::HashMap; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::num::NonZeroU32; +use std::path::{Path, PathBuf}; use std::rc::Weak; use std::{pin::Pin, rc::Rc}; @@ -884,6 +884,7 @@ pub async fn load( return CompilationResult { components: HashMap::new(), diagnostics: diag.into_iter().collect(), + dependencies: Vec::new(), #[cfg(feature = "internal")] structs_and_enums: Vec::new(), #[cfg(feature = "internal")] @@ -896,7 +897,12 @@ pub async fn load( #[cfg(feature = "internal-highlight")] let raw_type_loader = raw_type_loader.map(Rc::new); - let doc = loader.get_document(&path).unwrap(); + #[cfg(feature = "internal-highlight")] + let loader_ref: &i_slint_compiler::typeloader::TypeLoader = loader.as_ref(); + #[cfg(not(feature = "internal-highlight"))] + let loader_ref: &i_slint_compiler::typeloader::TypeLoader = &loader; + + let doc = loader_ref.get_document(&path).unwrap(); let compiled_globals = Rc::new(CompiledGlobalCollection::compile(doc)); let mut components = HashMap::new(); @@ -965,6 +971,7 @@ pub async fn load( CompilationResult { diagnostics: diag.into_iter().collect(), components, + dependencies: gather_dependency_paths(loader_ref, &path, doc), #[cfg(feature = "internal")] structs_and_enums, #[cfg(feature = "internal")] @@ -972,6 +979,32 @@ pub async fn load( } } +fn gather_dependency_paths( + loader: &i_slint_compiler::typeloader::TypeLoader, + root_path: &PathBuf, + root_document: &object_tree::Document, +) -> Vec { + let mut collected = BTreeSet::new(); + collected.insert(root_path.clone()); + collect_document_dependencies(loader, root_document, &mut collected); + collected.into_iter().collect() +} + +fn collect_document_dependencies( + loader: &i_slint_compiler::typeloader::TypeLoader, + document: &object_tree::Document, + visited: &mut BTreeSet, +) { + for import in &document.imports { + let path = i_slint_compiler::pathutils::clean_path(Path::new(&import.file)); + if visited.insert(path.clone()) { + if let Some(dep_doc) = loader.get_document(&path) { + collect_document_dependencies(loader, dep_doc, visited); + } + } + } +} + fn generate_rtti() -> HashMap<&'static str, Rc> { let mut rtti = HashMap::new(); use i_slint_core::items::*; From a1d822f54dd8cace2a98d33235a0906b9c8048f4 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Tue, 28 Oct 2025 02:21:27 +0800 Subject: [PATCH 24/52] fix(python): enable development mode for rust code build --- api/python/slint/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/python/slint/pyproject.toml b/api/python/slint/pyproject.toml index b8bc6821b60..5abf90d1617 100644 --- a/api/python/slint/pyproject.toml +++ b/api/python/slint/pyproject.toml @@ -57,8 +57,8 @@ features = ["pyo3/extension-module"] [tool.uv] # Rebuild package when any rust files change cache-keys = [{ file = "pyproject.toml" }, { file = "Cargo.toml" }, { file = "**/*.rs" }] -# Uncomment to build rust code in development mode -# config-settings = { build-args = '--profile=dev' } +# Build rust code in development mode +config-settings = { build-args = '--profile=dev' } [tool.mypy] strict = true From 9e87cca0210e1b41b1a0dc5b19c635a8d9aa97a2 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Tue, 28 Oct 2025 02:23:49 +0800 Subject: [PATCH 25/52] feat(python): use elaina's pyo3-stub-gen (temporary) and enhance model structure with abstract methods derive macros --- api/python/slint/Cargo.toml | 2 +- api/python/slint/models.rs | 34 +++++++++++++++++++++++++++++++- api/python/slint/slint/models.py | 32 +++--------------------------- api/python/slint/structs.rs | 4 ++++ 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/api/python/slint/Cargo.toml b/api/python/slint/Cargo.toml index fed08c207a6..b18c22b8e5f 100644 --- a/api/python/slint/Cargo.toml +++ b/api/python/slint/Cargo.toml @@ -54,6 +54,6 @@ indexmap = { version = "2.1.0" } chrono = "0.4" spin_on = { workspace = true } css-color-parser2 = { workspace = true } -pyo3-stub-gen = { version = "0.9.0", default-features = false } +pyo3-stub-gen = { git = "https://github.com/GreyElaina/pyo3-stub-gen", branch = "main", default-features = false, features = ["infer_signature"] } smol = { version = "2.0.0" } smol_str = { workspace = true } diff --git a/api/python/slint/models.rs b/api/python/slint/models.rs index 5bac5da5bee..b6941a026c4 100644 --- a/api/python/slint/models.rs +++ b/api/python/slint/models.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use i_slint_core::model::{Model, ModelNotify, ModelRc}; -use pyo3::exceptions::PyIndexError; +use pyo3::exceptions::{PyIndexError, PyNotImplementedError}; use pyo3::gc::PyVisit; use pyo3::prelude::*; use pyo3::PyTraverseError; @@ -45,6 +45,7 @@ impl PyModelShared { #[derive(Clone)] #[gen_stub_pyclass] +#[gen_stub(abstract_class)] #[pyclass(unsendable, weakref, subclass)] pub struct PyModelBase { inner: Rc, @@ -75,18 +76,49 @@ impl PyModelBase { *self.inner.self_ref.borrow_mut() = Some(self_ref); } + /// Call this method from a sub-class to notify the views that + /// `count` rows have been added starting at `index`. fn notify_row_added(&self, index: usize, count: usize) { self.inner.notify.row_added(index, count) } + /// Call this method from a sub-class to notify the views that a row has changed. fn notify_row_changed(&self, index: usize) { self.inner.notify.row_changed(index) } + /// Call this method from a sub-class to notify the views that + /// `count` rows have been removed starting at `index`. fn notify_row_removed(&self, index: usize, count: usize) { self.inner.notify.row_removed(index, count) } + /// Returns the number of rows available in the model. + #[gen_stub(abstractmethod)] + fn row_count(&self) -> PyResult { + Err(PyNotImplementedError::new_err( + "Model subclasses must override row_count()", + )) + } + + /// Returns the data for the given row in the model. + #[gen_stub(abstractmethod)] + fn row_data(&self, _row: usize) -> PyResult>> { + Err(PyNotImplementedError::new_err( + "Model subclasses must override row_data()", + )) + } + + /// Call this method on mutable models to change the data for the given row. + /// The UI will also call this method when modifying a model's data. + /// Re-implement this method in a sub-class to handle the change. + #[gen_stub(abstractmethod)] + fn set_row_data(&self, _row: usize, _value: Py) -> PyResult<()> { + Err(PyNotImplementedError::new_err( + "Model subclasses must override set_row_data() when mutation is required", + )) + } + #[gen_stub(skip)] fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { self.inner.__traverse__(&visit) diff --git a/api/python/slint/slint/models.py b/api/python/slint/slint/models.py index 7f9ce8d14a8..ff18bb90057 100644 --- a/api/python/slint/slint/models.py +++ b/api/python/slint/slint/models.py @@ -2,14 +2,14 @@ # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 import typing -from abc import abstractmethod +from abc import ABC from collections.abc import Iterable -from typing import Any, Iterator, cast +from typing import Any, Iterator from . import core -class Model[T](core.PyModelBase, Iterable[T]): +class Model[T](core.PyModelBase, ABC, Iterable[T]): """Model is the base class for feeding dynamic data into Slint views. Subclass Model to implement your own models, or use `ListModel` to wrap a list. @@ -34,32 +34,6 @@ def __setitem__(self, index: int, value: T) -> None: def __iter__(self) -> Iterator[T]: return ModelIterator(self) - def set_row_data(self, row: int, value: T) -> None: - """Call this method on mutable models to change the data for the given row. - The UI will also call this method when modifying a model's data. - Re-implement this method in a sub-class to handle the change.""" - super().set_row_data(row, value) - - @abstractmethod - def row_data(self, row: int) -> typing.Optional[T]: - """Returns the data for the given row. - Re-implement this method in a sub-class to provide the data.""" - return cast(T, super().row_data(row)) - - def notify_row_changed(self, row: int) -> None: - """Call this method from a sub-class to notify the views that a row has changed.""" - super().notify_row_changed(row) - - def notify_row_removed(self, row: int, count: int) -> None: - """Call this method from a sub-class to notify the views that - `count` rows have been removed starting at `row`.""" - super().notify_row_removed(row, count) - - def notify_row_added(self, row: int, count: int) -> None: - """Call this method from a sub-class to notify the views that - `count` rows have been added starting at `row`.""" - super().notify_row_added(row, count) - class ListModel[T](Model[T]): """ListModel is a `Model` that stores its data in a Python list. diff --git a/api/python/slint/structs.rs b/api/python/slint/structs.rs index c966c506702..3cafbd32b6e 100644 --- a/api/python/slint/structs.rs +++ b/api/python/slint/structs.rs @@ -156,6 +156,7 @@ macro_rules! generate_struct_support { default: NO_DEFAULT, deprecated: NO_DEPRECATED, item: false, + is_abstract: false, }, )* ], @@ -168,6 +169,7 @@ macro_rules! generate_struct_support { default: NO_DEFAULT, deprecated: NO_DEPRECATED, item: false, + is_abstract: false, }, )* ], @@ -177,6 +179,7 @@ macro_rules! generate_struct_support { has_hash: false, has_str: false, subclass: false, + is_abstract: false, } } @@ -202,6 +205,7 @@ macro_rules! generate_struct_support { is_async: false, deprecated: NO_DEPRECATED, type_ignored: None, + is_abstract: false, }], } } From 6ef211eba0b74e9baef0c971269debd06a379712 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Tue, 28 Oct 2025 02:26:24 +0800 Subject: [PATCH 26/52] fix(python): unexpected builtin enum regenerate; ensure package structure --- api/python/slint/slint/codegen/emitters.py | 10 ++++- api/python/slint/slint/codegen/generator.py | 20 +++++++++ api/python/slint/slint/codegen/models.py | 1 + .../tests/codegen/examples/counter/README.md | 4 +- .../tests/codegen/examples/counter/counter.py | 11 ++--- .../codegen/examples/counter/counter.pyi | 11 ++--- .../codegen/examples/counter/generate.py | 5 +-- .../examples/counter/generated/counter.py | 45 ------------------- .../examples/counter/generated/counter.pyi | 15 ------- .../examples/counter/generated/counter.slint | 29 ------------ .../tests/codegen/examples/counter/main.py | 2 +- .../slint/tests/codegen/test_generator.py | 2 +- 12 files changed, 45 insertions(+), 110 deletions(-) delete mode 100644 api/python/slint/tests/codegen/examples/counter/generated/counter.py delete mode 100644 api/python/slint/tests/codegen/examples/counter/generated/counter.pyi delete mode 100644 api/python/slint/tests/codegen/examples/counter/generated/counter.slint diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index 162ce59645b..ae1b869ac68 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -107,6 +107,8 @@ def _stmt(code: str) -> cst.BaseStatement: enum_lines: list[str] = [] for enum in artifacts.enums: + if enum.is_builtin: + continue enum_lines.append( f" if {enum.name!r} in enums:\n" f" module.{enum.py_name} = enums[{enum.name!r}]" @@ -233,6 +235,8 @@ def _stmt(code: str) -> cst.BaseStatement: for struct in artifacts.structs: export_bindings[struct.name] = struct.py_name for enum in artifacts.enums: + if enum.is_builtin: + continue export_bindings[enum.name] = enum.py_name export_items = list(export_bindings.values()) + [ @@ -370,7 +374,7 @@ def register_type(type_str: str) -> None: export_names = [component.py_name for component in artifacts.components] export_names += [struct.py_name for struct in artifacts.structs] - export_names += [enum.py_name for enum in artifacts.enums] + export_names += [enum.py_name for enum in artifacts.enums if not enum.is_builtin] export_names += [_normalize_prop(alias) for _, alias in artifacts.named_exports] if export_names: all_list = cst.List( @@ -437,6 +441,8 @@ def init_stub() -> cst.FunctionDef: post_body.append(cst.EmptyLine()) for enum_meta in artifacts.enums: + if enum_meta.is_builtin: + continue enum_body: list[cst.BaseStatement] = [] if enum_meta.values: for value in enum_meta.values: @@ -527,6 +533,8 @@ def init_stub() -> cst.FunctionDef: for struct in artifacts.structs: bindings[struct.name] = struct.py_name for enum_meta in artifacts.enums: + if enum_meta.is_builtin: + continue bindings[enum_meta.name] = enum_meta.py_name for orig, alias in artifacts.named_exports: diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index ce594035267..f94220aa265 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Iterable +from .. import core as _core from ..api import _normalize_prop from ..core import Brush, Color, CompilationResult, Compiler, DiagnosticLevel, Image from .emitters import write_python_module, write_stub_module @@ -87,6 +88,7 @@ def copy_slint_file(source: Path, destination: Path) -> None: else: module_dir = output_dir / relative.parent module_dir.mkdir(parents=True, exist_ok=True) + _ensure_package_marker(module_dir) target_stem = module_dir / sanitized_stem copy_slint = True slint_destination = module_dir / relative.name @@ -327,11 +329,14 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: value=enum_member.name, ) ) + core_enum = getattr(_core, enum_name, None) + is_builtin = core_enum is enum_cls enums_meta.append( EnumMeta( name=enum_name, py_name=_normalize_prop(enum_name), values=values, + is_builtin=is_builtin, ) ) @@ -347,6 +352,21 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: ) +def _ensure_package_marker(module_dir: Path) -> None: + """Ensure the generated directory is recognised as a regular Python package.""" + + try: + module_dir.mkdir(parents=True, exist_ok=True) + init_file = module_dir / "__init__.py" + if not init_file.exists(): + init_file.touch() + except PermissionError: + # If we cannot create the file, leave the namespace untouched. The + # generated module can still be imported in environments that support + # namespace packages. + pass + + __all__ = ["generate_project"] diff --git a/api/python/slint/slint/codegen/models.py b/api/python/slint/slint/codegen/models.py index 65b9ff0a9ab..da4cfa37da3 100644 --- a/api/python/slint/slint/codegen/models.py +++ b/api/python/slint/slint/codegen/models.py @@ -77,6 +77,7 @@ class EnumMeta: name: str py_name: str values: List[EnumValueMeta] + is_builtin: bool @dataclass(slots=True) diff --git a/api/python/slint/tests/codegen/examples/counter/README.md b/api/python/slint/tests/codegen/examples/counter/README.md index 7dfbfd314df..960316724cb 100644 --- a/api/python/slint/tests/codegen/examples/counter/README.md +++ b/api/python/slint/tests/codegen/examples/counter/README.md @@ -16,8 +16,8 @@ Instead of relying on the runtime auto-loader, it uses the experimental uv run python examples/counter/generate.py ``` - This produces `examples/counter/generated/counter.py` and - `examples/counter/generated/counter.pyi` alongside a copy of the + This produces `examples/counter/counter.py` and + `examples/counter/counter.pyi` next to the source `.slint` file, all ready for import. 2. Run the app: diff --git a/api/python/slint/tests/codegen/examples/counter/counter.py b/api/python/slint/tests/codegen/examples/counter/counter.py index 2740284068f..0614ac6d045 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.py +++ b/api/python/slint/tests/codegen/examples/counter/counter.py @@ -1,6 +1,3 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - # Generated by slint.codegen from counter.slint from __future__ import annotations @@ -13,12 +10,11 @@ import slint -__all__ = ["CounterWindow"] +__all__ = ['CounterWindow'] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = "counter.slint" - +_SLINT_RESOURCE = 'counter.slint' def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -28,7 +24,7 @@ def _load() -> types.SimpleNamespace: else: ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE)) with ctx as slint_path: - include_paths: list[os.PathLike[Any] | Path] = None + include_paths: list[os.PathLike[Any] | Path] | None = [_MODULE_DIR] library_paths: dict[str, os.PathLike[Any] | Path] | None = None return slint.load_file( path=slint_path, @@ -39,7 +35,6 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) - _module = _load() CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/counter.pyi b/api/python/slint/tests/codegen/examples/counter/counter.pyi index 7208e979f57..cd609c18c1b 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.pyi +++ b/api/python/slint/tests/codegen/examples/counter/counter.pyi @@ -1,15 +1,16 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - from __future__ import annotations +import enum + from typing import Any, Callable import slint -__all__ = ["CounterWindow"] +__all__ = ['CounterWindow'] class CounterWindow(slint.Component): - def __init__(self, **kwargs: Any) -> None: ... + def __init__(self, **kwargs: Any) -> None: + ... counter: int request_increase: Callable[[], None] + diff --git a/api/python/slint/tests/codegen/examples/counter/generate.py b/api/python/slint/tests/codegen/examples/counter/generate.py index b1aab4bbfe9..3eccee76dfb 100644 --- a/api/python/slint/tests/codegen/examples/counter/generate.py +++ b/api/python/slint/tests/codegen/examples/counter/generate.py @@ -11,7 +11,6 @@ def main() -> None: base_dir = Path(__file__).parent - output = base_dir / "generated" config = GenerationConfig( include_paths=[base_dir], library_paths={}, @@ -21,9 +20,9 @@ def main() -> None: ) generate_project( - inputs=[base_dir / "counter.slint"], output_dir=output, config=config + inputs=[base_dir / "counter.slint"], output_dir=None, config=config ) - print(f"Generated Python bindings into {output.relative_to(base_dir)}") + print("Generated Python bindings next to counter.slint") if __name__ == "__main__": diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.py b/api/python/slint/tests/codegen/examples/counter/generated/counter.py deleted file mode 100644 index 2740284068f..00000000000 --- a/api/python/slint/tests/codegen/examples/counter/generated/counter.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -# Generated by slint.codegen from counter.slint -from __future__ import annotations - -import importlib.resources as _resources -import os -import types -from contextlib import nullcontext as _nullcontext -from pathlib import Path -from typing import Any - -import slint - -__all__ = ["CounterWindow"] - -_MODULE_DIR = Path(__file__).parent - -_SLINT_RESOURCE = "counter.slint" - - -def _load() -> types.SimpleNamespace: - """Load the compiled Slint module for this package.""" - package = __package__ or (__spec__.parent if __spec__ else None) - if package: - ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE)) - else: - ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE)) - with ctx as slint_path: - include_paths: list[os.PathLike[Any] | Path] = None - library_paths: dict[str, os.PathLike[Any] | Path] | None = None - return slint.load_file( - path=slint_path, - quiet=True, - style=None, - include_paths=include_paths, - library_paths=library_paths, - translation_domain=None, - ) - - -_module = _load() - -CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi b/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi deleted file mode 100644 index 7208e979f57..00000000000 --- a/api/python/slint/tests/codegen/examples/counter/generated/counter.pyi +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -from __future__ import annotations - -from typing import Any, Callable - -import slint - -__all__ = ["CounterWindow"] - -class CounterWindow(slint.Component): - def __init__(self, **kwargs: Any) -> None: ... - counter: int - request_increase: Callable[[], None] diff --git a/api/python/slint/tests/codegen/examples/counter/generated/counter.slint b/api/python/slint/tests/codegen/examples/counter/generated/counter.slint deleted file mode 100644 index c28ccb2e8b0..00000000000 --- a/api/python/slint/tests/codegen/examples/counter/generated/counter.slint +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright © SixtyFPS GmbH -// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -export component CounterWindow inherits Window { - width: 240px; - height: 120px; - - in-out property counter: 0; - callback request_increase(); - - Rectangle { - width: parent.width; - height: parent.height; - background: #2b2b2b; - border-radius: 12px; - - Text { - text: "Count: " + root.counter; - color: white; - font-size: 24px; - horizontal-alignment: center; - vertical-alignment: center; - } - - TouchArea { - clicked => root.request_increase(); - } - } -} diff --git a/api/python/slint/tests/codegen/examples/counter/main.py b/api/python/slint/tests/codegen/examples/counter/main.py index b5bd0ccf60f..7aee25a8a14 100644 --- a/api/python/slint/tests/codegen/examples/counter/main.py +++ b/api/python/slint/tests/codegen/examples/counter/main.py @@ -6,7 +6,7 @@ import slint try: - from .generated.counter import CounterWindow + from .counter import CounterWindow except ImportError as exc: # pragma: no cover - user-facing guidance raise SystemExit( "Generated bindings not found. Run `python generate.py` in the " diff --git a/api/python/slint/tests/codegen/test_generator.py b/api/python/slint/tests/codegen/test_generator.py index 42aa046a7c0..a4b86d798b4 100644 --- a/api/python/slint/tests/codegen/test_generator.py +++ b/api/python/slint/tests/codegen/test_generator.py @@ -220,7 +220,7 @@ def test_counter_example_workflow(tmp_path: Path) -> None: shutil.copytree(example_src, example_copy) subprocess.run([sys.executable, "generate.py"], cwd=example_copy, check=True) - generated_py = example_copy / "generated" / "counter.py" + generated_py = example_copy / "counter.py" assert generated_py.exists() sys.path.insert(0, str(tmp_path)) From c5affe81e5ae5d362322765d7c08a8827307c4bf Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Tue, 28 Oct 2025 02:27:07 +0800 Subject: [PATCH 27/52] refactor(python): regenerate type stubs --- api/python/slint/slint/core.pyi | 1715 +++++++++++++++++++++++++++---- 1 file changed, 1517 insertions(+), 198 deletions(-) diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index 95a0e0f9ae8..475e5b7c82f 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -1,270 +1,1589 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - # This file is automatically generated by pyo3_stub_gen # ruff: noqa: E501, F401 +import abc import builtins import datetime -import gettext +import enum import os import pathlib import typing -from collections.abc import Buffer, Callable, Coroutine -from enum import Enum, auto -from typing import Any, List -class RgbColor: - red: int - green: int - blue: int +@typing.final +class AsyncAdapter: + def __new__(cls, fd: builtins.int) -> AsyncAdapter: ... + def wait_for_readable(self, callback: typing.Any) -> None: ... + def wait_for_writable(self, callback: typing.Any) -> None: ... -class RgbaColor: - red: int - green: int - blue: int - alpha: int +@typing.final +class Brush: + r""" + A brush is a data structure that is used to describe how a shape, such as a rectangle, path or even text, + shall be filled. A brush can also be applied to the outline of a shape, that means the fill of the outline itself. + + Brushes can only be constructed from solid colors. + + **Note:** In future, we plan to reduce this constraint and allow for declaring graidient brushes programmatically. + """ + @property + def color(self) -> Color: + r""" + The brush's color. + """ + def __new__(cls, maybe_value: typing.Optional[Color] = None) -> Brush: ... + def is_transparent(self) -> builtins.bool: + r""" + Returns true if this brush contains a fully transparent color (alpha value is zero). + """ + def is_opaque(self) -> builtins.bool: + r""" + Returns true if this brush is fully opaque. + """ + def brighter(self, factor: builtins.float) -> Brush: + r""" + Returns a new version of this brush that has the brightness increased + by the specified factor. This is done by calling `Color.brighter` on + all the colors of this brush. + """ + def darker(self, factor: builtins.float) -> Brush: + r""" + Returns a new version of this brush that has the brightness decreased + by the specified factor. This is done by calling `Color.darker` on + all the color of this brush. + """ + def transparentize(self, amount: builtins.float) -> Brush: + r""" + Returns a new version of this brush with the opacity decreased by `factor`. + + The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. + + See also `Color.transparentize`. + """ + def with_alpha(self, alpha: builtins.float) -> Brush: + r""" + Returns a new version of this brush with the related color's opacities + set to `alpha`. + """ + def __eq__(self, other: Brush) -> builtins.bool: ... + +@typing.final +class CallbackInfo: + @property + def name(self) -> builtins.str: ... + @property + def parameters(self) -> builtins.list[CallbackParameter]: ... + @property + def return_type(self) -> builtins.str: ... + +@typing.final +class CallbackParameter: + @property + def name(self) -> typing.Optional[builtins.str]: ... + @property + def python_type(self) -> builtins.str: ... +@typing.final class Color: - red: int - green: int - blue: int - alpha: int - def __new__( - cls, - maybe_value: typing.Optional[ - str | RgbaColor | RgbColor | typing.Dict[str, int] - ] = None, - ) -> "Color": ... - def brighter(self, factor: float) -> "Color": ... - def darker(self, factor: float) -> "Color": ... - def transparentize(self, factor: float) -> "Color": ... - def mix(self, other: "Image", factor: float) -> "Color": ... - def with_alpha(self, alpha: float) -> "Color": ... - def __str__(self) -> str: ... - def __eq__(self, other: object) -> bool: ... + r""" + A Color object represents a color in the RGB color space with an alpha. Each color channel and the alpha is represented + as an 8-bit integer. The alpha channel is 0 for fully transparent and 255 for fully opaque. + + Construct colors from a CSS color string, or by specifying the red, green, blue, and (optional) alpha channels in a dict. + """ + @property + def red(self) -> builtins.int: + r""" + The red channel. + """ + @property + def green(self) -> builtins.int: + r""" + The green channel. + """ + @property + def blue(self) -> builtins.int: + r""" + The blue channel. + """ + @property + def alpha(self) -> builtins.int: + r""" + The alpha channel. + """ + def __new__(cls, maybe_value: typing.Optional[builtins.str | PyColorInput.RgbaColor | PyColorInput.RgbColor] = None) -> Color: ... + def brighter(self, factor: builtins.float) -> Color: + r""" + Returns a new color that is brighter than this color by the given factor. + """ + def darker(self, factor: builtins.float) -> Color: + r""" + Returns a new color that is darker than this color by the given factor. + """ + def transparentize(self, factor: builtins.float) -> Color: + r""" + Returns a new version of this color with the opacity decreased by `factor`. + + The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. + """ + def mix(self, other: Color, factor: builtins.float) -> Color: + r""" + Returns a new color that is a mix of this color and `other`. The specified factor is + clamped to be between `0.0` and `1.0` and then applied to this color, while `1.0 - factor` + is applied to `other`. + """ + def with_alpha(self, alpha: builtins.float) -> Color: + r""" + Returns a new version of this color with the opacity set to `alpha`. + """ + def __str__(self) -> builtins.str: ... + def __eq__(self, other: Color) -> builtins.bool: ... -class Brush: - color: Color - def __new__(cls, maybe_value: typing.Optional[Color]) -> "Brush": ... - def is_transparent(self) -> bool: ... - def is_opaque(self) -> bool: ... - def brighter(self, factor: float) -> "Brush": ... - def darker(self, factor: float) -> "Brush": ... - def transparentize(self, amount: float) -> "Brush": ... - def with_alpha(self, alpha: float) -> "Brush": ... - def __eq__(self, other: object) -> bool: ... +@typing.final +class CompilationResult: + @property + def component_names(self) -> builtins.list[builtins.str]: ... + @property + def diagnostics(self) -> builtins.list[PyDiagnostic]: ... + @property + def structs_and_enums(self) -> tuple[builtins.dict[builtins.str, typing.Any], builtins.dict[builtins.str, typing.Any]]: ... + @property + def named_exports(self) -> builtins.list[tuple[builtins.str, builtins.str]]: ... + @property + def resource_paths(self) -> builtins.list[pathlib.Path]: ... + def component(self, name: builtins.str) -> typing.Optional[ComponentDefinition]: ... + +@typing.final +class Compiler: + @property + def include_paths(self) -> builtins.list[pathlib.Path]: ... + @include_paths.setter + def include_paths(self, value: builtins.list[pathlib.Path]) -> None: ... + @property + def style(self) -> typing.Optional[builtins.str]: ... + @style.setter + def style(self, value: builtins.str) -> None: ... + @property + def library_paths(self) -> builtins.dict[builtins.str, pathlib.Path]: ... + @library_paths.setter + def library_paths(self, value: builtins.dict[builtins.str, pathlib.Path]) -> None: ... + def __new__(cls) -> Compiler: ... + def set_translation_domain(self, domain: builtins.str) -> None: ... + def build_from_path(self, path: builtins.str | os.PathLike | pathlib.Path) -> CompilationResult: ... + def build_from_source(self, source_code: builtins.str, path: builtins.str | os.PathLike | pathlib.Path) -> CompilationResult: ... + +@typing.final +class ComponentDefinition: + @property + def name(self) -> builtins.str: ... + @property + def properties(self) -> builtins.dict[builtins.str, ValueType]: ... + @property + def callbacks(self) -> builtins.list[builtins.str]: ... + @property + def functions(self) -> builtins.list[builtins.str]: ... + @property + def globals(self) -> builtins.list[builtins.str]: ... + def property_infos(self) -> builtins.list[PropertyInfo]: ... + def callback_infos(self) -> builtins.list[CallbackInfo]: ... + def function_infos(self) -> builtins.list[FunctionInfo]: ... + def global_properties(self, name: builtins.str) -> builtins.dict[builtins.str, ValueType]: ... + def global_callbacks(self, name: builtins.str) -> builtins.list[builtins.str]: ... + def global_functions(self, name: builtins.str) -> builtins.list[builtins.str]: ... + def global_property_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[PropertyInfo]]: ... + def global_callback_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[CallbackInfo]]: ... + def global_function_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[FunctionInfo]]: ... + def callback_returns_void(self, callback_name: builtins.str) -> builtins.bool: ... + def global_callback_returns_void(self, global_name: builtins.str, callback_name: builtins.str) -> builtins.bool: ... + def create(self) -> ComponentInstance: ... + +@typing.final +class ComponentInstance: + @property + def definition(self) -> ComponentDefinition: ... + def get_property(self, name: builtins.str) -> typing.Any: ... + def set_property(self, name: builtins.str, value: typing.Any) -> None: ... + def get_global_property(self, global_name: builtins.str, prop_name: builtins.str) -> typing.Any: ... + def set_global_property(self, global_name: builtins.str, prop_name: builtins.str, value: typing.Any) -> None: ... + def invoke(self, callback_name: builtins.str, *args: typing.Any) -> typing.Any: ... + def invoke_global(self, global_name: builtins.str, callback_name: builtins.str, *args: typing.Any) -> typing.Any: ... + def set_callback(self, name: builtins.str, callable: typing.Any) -> None: ... + def set_global_callback(self, global_name: builtins.str, callback_name: builtins.str, callable: typing.Any) -> None: ... + def show(self) -> None: ... + def hide(self) -> None: ... + def on_close_requested(self, callable: typing.Any) -> None: ... + def dispatch_close_requested_event(self) -> None: ... + def __clear__(self) -> None: ... +@typing.final +class DropEvent: + @property + def mime_type(self) -> typing.Any: ... + @mime_type.setter + def mime_type(self, value: typing.Any) -> None: ... + @property + def data(self) -> typing.Any: ... + @data.setter + def data(self, value: typing.Any) -> None: ... + @property + def position(self) -> typing.Any: ... + @position.setter + def position(self, value: typing.Any) -> None: ... + def __init__(self, *, mime_type: typing.Any = ..., data: typing.Any = ..., position: typing.Any = ...) -> None: ... + +@typing.final +class FontMetrics: + @property + def ascent(self) -> typing.Any: ... + @ascent.setter + def ascent(self, value: typing.Any) -> None: ... + @property + def descent(self) -> typing.Any: ... + @descent.setter + def descent(self, value: typing.Any) -> None: ... + @property + def x_height(self) -> typing.Any: ... + @x_height.setter + def x_height(self, value: typing.Any) -> None: ... + @property + def cap_height(self) -> typing.Any: ... + @cap_height.setter + def cap_height(self, value: typing.Any) -> None: ... + def __init__(self, *, ascent: typing.Any = ..., descent: typing.Any = ..., x_height: typing.Any = ..., cap_height: typing.Any = ...) -> None: ... + +@typing.final +class FunctionInfo: + @property + def name(self) -> builtins.str: ... + @property + def parameters(self) -> builtins.list[CallbackParameter]: ... + @property + def return_type(self) -> builtins.str: ... + +@typing.final class Image: r""" - Image objects can be set on Slint Image elements for display. Construct Image objects from a path to an - image file on disk, using `Image.load_from_path`. + Image objects can be set on Slint Image elements for display. Use `Image.load_from_path` to construct Image + objects from a path to an image file on disk. """ - - size: tuple[int, int] - width: int - height: int - path: typing.Optional[pathlib.Path] - def __new__( - cls, - ) -> "Image": ... + @property + def size(self) -> tuple[builtins.int, builtins.int]: + r""" + The size of the image as tuple of `width` and `height`. + """ + @property + def width(self) -> builtins.int: + r""" + The width of the image in pixels. + """ + @property + def height(self) -> builtins.int: + r""" + The height of the image in pixels. + """ + @property + def path(self) -> typing.Optional[pathlib.Path]: + r""" + The path of the image if it was loaded from disk, or None. + """ + def __new__(cls) -> Image: ... @staticmethod - def load_from_path(path: str | os.PathLike[Any] | pathlib.Path) -> "Image": + def load_from_path(path: builtins.str | os.PathLike | pathlib.Path) -> Image: r""" Loads the image from the specified path. Returns None if the image can't be loaded. """ - ... - @staticmethod - def load_from_svg_data(data: typing.Sequence[int]) -> "Image": + def load_from_svg_data(data: typing.Sequence[builtins.int]) -> Image: r""" Creates a new image from a string that describes the image in SVG format. """ - ... - @staticmethod - def load_from_array(array: Buffer) -> Image: + def load_from_array(array: typing.Any) -> Image: r""" Creates a new image from an array-like object that implements the [Buffer Protocol](https://docs.python.org/3/c-api/buffer.html). Use this function to import images created by third-party modules such as matplotlib or Pillow. - + The array must satisfy certain contraints to represent an image: - + - The buffer's format needs to be `B` (unsigned char) - The shape must be a tuple of (height, width, bytes-per-pixel) - If a stride is defined, the row stride must be equal to width * bytes-per-pixel, and the column stride must equal the bytes-per-pixel. - A value of 3 for bytes-per-pixel is interpreted as RGB image, a value of 4 means RGBA. - + + The image is created by performing a deep copy of the array's data. Subsequent changes to the buffer are not automatically + reflected in a previously created Image. + Example of importing a matplot figure into an image: ```python import slint import matplotlib - + from matplotlib.backends.backend_agg import FigureCanvasAgg from matplotlib.figure import Figure - + fig = Figure(figsize=(5, 4), dpi=100) canvas = FigureCanvasAgg(fig) ax = fig.add_subplot() ax.plot([1, 2, 3]) canvas.draw() - + buffer = canvas.buffer_rgba() img = slint.Image.load_from_array(buffer) ``` - + Example of loading an image with Pillow: ```python import slint from PIL import Image import numpy as np - + pil_img = Image.open("hello.jpeg") array = np.array(pil_img) img = slint.Image.load_from_array(array) ``` """ -class TimerMode(Enum): - SingleShot = auto() - Repeated = auto() +@typing.final +class KeyEvent: + @property + def text(self) -> typing.Any: ... + @text.setter + def text(self, value: typing.Any) -> None: ... + @property + def modifiers(self) -> typing.Any: ... + @modifiers.setter + def modifiers(self, value: typing.Any) -> None: ... + @property + def repeat(self) -> typing.Any: ... + @repeat.setter + def repeat(self, value: typing.Any) -> None: ... + def __init__(self, *, text: typing.Any = ..., modifiers: typing.Any = ..., repeat: typing.Any = ...) -> None: ... -class Timer: - running: bool - interval: datetime.timedelta - def __new__( - cls, - ) -> "Timer": ... - def start( - self, mode: TimerMode, interval: datetime.timedelta, callback: typing.Any - ) -> None: ... - @staticmethod - def single_shot(duration: datetime.timedelta, callback: typing.Any) -> None: ... - def stop(self) -> None: ... - def restart(self) -> None: ... +@typing.final +class KeyboardModifiers: + @property + def alt(self) -> typing.Any: ... + @alt.setter + def alt(self, value: typing.Any) -> None: ... + @property + def control(self) -> typing.Any: ... + @control.setter + def control(self, value: typing.Any) -> None: ... + @property + def shift(self) -> typing.Any: ... + @shift.setter + def shift(self, value: typing.Any) -> None: ... + @property + def meta(self) -> typing.Any: ... + @meta.setter + def meta(self, value: typing.Any) -> None: ... + def __init__(self, *, alt: typing.Any = ..., control: typing.Any = ..., shift: typing.Any = ..., meta: typing.Any = ...) -> None: ... -def set_xdg_app_id(app_id: str) -> None: ... -def invoke_from_event_loop(callable: typing.Callable[[], None]) -> None: ... -def run_event_loop() -> None: ... -def quit_event_loop() -> None: ... -def init_translations( - translations: typing.Optional[gettext.GNUTranslations], -) -> None: ... - -class PyModelBase: - def init_self(self, *args: Any) -> None: ... - def row_count(self) -> int: ... - def row_data(self, row: int) -> typing.Optional[Any]: ... - def set_row_data(self, row: int, value: Any) -> None: ... - def notify_row_changed(self, row: int) -> None: ... - def notify_row_removed(self, row: int, count: int) -> None: ... - def notify_row_added(self, row: int, count: int) -> None: ... - -class PyStruct(Any): ... - -class ValueType(Enum): - Void = auto() - Number = auto() - String = auto() - Bool = auto() - Model = auto() - Struct = auto() - Brush = auto() - Image = auto() - -class DiagnosticLevel(Enum): - Error = auto() - Warning = auto() +@typing.final +class MenuEntry: + @property + def title(self) -> typing.Any: ... + @title.setter + def title(self, value: typing.Any) -> None: ... + @property + def icon(self) -> typing.Any: ... + @icon.setter + def icon(self, value: typing.Any) -> None: ... + @property + def id(self) -> typing.Any: ... + @id.setter + def id(self, value: typing.Any) -> None: ... + @property + def enabled(self) -> typing.Any: ... + @enabled.setter + def enabled(self, value: typing.Any) -> None: ... + @property + def checkable(self) -> typing.Any: ... + @checkable.setter + def checkable(self, value: typing.Any) -> None: ... + @property + def checked(self) -> typing.Any: ... + @checked.setter + def checked(self, value: typing.Any) -> None: ... + @property + def has_sub_menu(self) -> typing.Any: ... + @has_sub_menu.setter + def has_sub_menu(self, value: typing.Any) -> None: ... + @property + def is_separator(self) -> typing.Any: ... + @is_separator.setter + def is_separator(self, value: typing.Any) -> None: ... + def __init__(self, *, title: typing.Any = ..., icon: typing.Any = ..., id: typing.Any = ..., enabled: typing.Any = ..., checkable: typing.Any = ..., checked: typing.Any = ..., has_sub_menu: typing.Any = ..., is_separator: typing.Any = ...) -> None: ... -class PyDiagnostic: - level: DiagnosticLevel - message: str - line_number: int - column_number: int - source_file: typing.Optional[str] +@typing.final +class PointerEvent: + @property + def button(self) -> typing.Any: ... + @button.setter + def button(self, value: typing.Any) -> None: ... + @property + def kind(self) -> typing.Any: ... + @kind.setter + def kind(self, value: typing.Any) -> None: ... + @property + def modifiers(self) -> typing.Any: ... + @modifiers.setter + def modifiers(self, value: typing.Any) -> None: ... + def __init__(self, *, button: typing.Any = ..., kind: typing.Any = ..., modifiers: typing.Any = ...) -> None: ... -class ComponentInstance: - def show(self) -> None: ... - def hide(self) -> None: ... - def invoke(self, callback_name: str, *args: Any) -> Any: ... - def invoke_global( - self, global_name: str, callback_name: str, *args: Any - ) -> Any: ... - def set_property(self, property_name: str, value: Any) -> None: ... - def get_property(self, property_name: str) -> Any: ... - def set_callback( - self, callback_name: str, callback: Callable[..., Any] - ) -> None: ... - def set_global_callback( - self, global_name: str, callback_name: str, callback: Callable[..., Any] - ) -> None: ... - def set_global_property( - self, global_name: str, property_name: str, value: Any - ) -> None: ... - def get_global_property(self, global_name: str, property_name: str) -> Any: ... +@typing.final +class PointerScrollEvent: + @property + def delta_x(self) -> typing.Any: ... + @delta_x.setter + def delta_x(self, value: typing.Any) -> None: ... + @property + def delta_y(self) -> typing.Any: ... + @delta_y.setter + def delta_y(self, value: typing.Any) -> None: ... + @property + def modifiers(self) -> typing.Any: ... + @modifiers.setter + def modifiers(self, value: typing.Any) -> None: ... + def __init__(self, *, delta_x: typing.Any = ..., delta_y: typing.Any = ..., modifiers: typing.Any = ...) -> None: ... +@typing.final class PropertyInfo: - name: str - python_type: str + @property + def name(self) -> builtins.str: ... + @property + def python_type(self) -> builtins.str: ... -class CallbackParameter: - name: str | None - python_type: str +class PyColorInput: + @typing.final + class ColorStr(PyColorInput): + __match_args__ = ("_0",) + @property + def _0(self) -> builtins.str: ... + def __new__(cls, _0: builtins.str) -> PyColorInput.ColorStr: ... + def __len__(self) -> builtins.int: ... + def __getitem__(self, key: builtins.int) -> typing.Any: ... + + class RgbaColor(typing.TypedDict): + red: builtins.int + green: builtins.int + blue: builtins.int + alpha: builtins.int + + class RgbColor(typing.TypedDict): + red: builtins.int + green: builtins.int + blue: builtins.int + + ... -class CallbackInfo: - name: str - parameters: list[CallbackParameter] - return_type: str +@typing.final +class PyDiagnostic: + @property + def level(self) -> DiagnosticLevel: ... + @property + def message(self) -> builtins.str: ... + @property + def column_number(self) -> builtins.int: ... + @property + def line_number(self) -> builtins.int: ... + @property + def source_file(self) -> typing.Optional[pathlib.Path]: ... + def __str__(self) -> builtins.str: ... -class FunctionInfo: - name: str - parameters: list[CallbackParameter] - return_type: str +class PyModelBase(abc.ABC): + def init_self(self, self_ref: typing.Any) -> None: ... + def notify_row_added(self, index: builtins.int, count: builtins.int) -> None: + r""" + Call this method from a sub-class to notify the views that + `count` rows have been added starting at `index`. + """ + def notify_row_changed(self, index: builtins.int) -> None: + r""" + Call this method from a sub-class to notify the views that a row has changed. + """ + def notify_row_removed(self, index: builtins.int, count: builtins.int) -> None: + r""" + Call this method from a sub-class to notify the views that + `count` rows have been removed starting at `index`. + """ + @abc.abstractmethod + def row_count(self) -> builtins.int: + r""" + Returns the number of rows available in the model. + """ + @abc.abstractmethod + def row_data(self, _row: builtins.int) -> typing.Optional[typing.Any]: + r""" + Returns the data for the given row in the model. + """ + @abc.abstractmethod + def set_row_data(self, _row: builtins.int, _value: typing.Any) -> None: + r""" + Call this method on mutable models to change the data for the given row. + The UI will also call this method when modifying a model's data. + Re-implement this method in a sub-class to handle the change. + """ + def __clear__(self) -> None: ... -class ComponentDefinition: - def create(self) -> ComponentInstance: ... - name: str - globals: list[str] - functions: list[str] - callbacks: list[str] - properties: dict[str, ValueType] - def property_infos(self) -> list[PropertyInfo]: ... - def callback_infos(self) -> list[CallbackInfo]: ... - def function_infos(self) -> list[FunctionInfo]: ... - def global_property_infos(self, global_name: str) -> list[PropertyInfo]: ... - def global_callback_infos(self, global_name: str) -> list[CallbackInfo]: ... - def global_function_infos(self, global_name: str) -> list[FunctionInfo]: ... - def global_functions(self, global_name: str) -> list[str]: ... - def global_callbacks(self, global_name: str) -> list[str]: ... - def global_properties(self, global_name: str) -> typing.Dict[str, ValueType]: ... - def callback_returns_void(self, callback_name: str) -> bool: ... - def global_callback_returns_void( - self, global_name: str, callback_name: str - ) -> bool: ... +class PyStruct: + def __getattr__(self, key: builtins.str) -> typing.Any: ... + def __setattr__(self, key: builtins.str, value: typing.Any) -> None: ... + def __iter__(self) -> PyStructFieldIterator: ... + def __copy__(self) -> PyStruct: ... + def __clear__(self) -> None: ... -class CompilationResult: - component_names: list[str] - diagnostics: list[PyDiagnostic] - named_exports: list[typing.Tuple[str, str]] - structs_and_enums: typing.Tuple[typing.Dict[str, PyStruct], typing.Dict[str, Enum]] - def component(self, name: str) -> ComponentDefinition: ... +@typing.final +class PyStructFieldIterator: + def __iter__(self) -> PyStructFieldIterator: ... + def __next__(self) -> typing.Any: ... -class Compiler: - include_paths: list[os.PathLike[Any] | pathlib.Path] - library_paths: dict[str, os.PathLike[Any] | pathlib.Path] - translation_domain: str - style: str - def build_from_path( - self, path: os.PathLike[Any] | pathlib.Path - ) -> CompilationResult: ... - def build_from_source( - self, source: str, path: os.PathLike[Any] | pathlib.Path - ) -> CompilationResult: ... +@typing.final +class ReadOnlyRustModel: + def row_count(self) -> builtins.int: ... + def row_data(self, row: builtins.int) -> typing.Any: ... + def __len__(self) -> builtins.int: ... + def __iter__(self) -> ReadOnlyRustModelIterator: ... + def __getitem__(self, index: builtins.int) -> typing.Any: ... + +@typing.final +class ReadOnlyRustModelIterator: + def __iter__(self) -> ReadOnlyRustModelIterator: ... + def __next__(self) -> typing.Any: ... + +@typing.final +class RgbColor: + @property + def red(self) -> builtins.int: ... + @red.setter + def red(self, value: builtins.int) -> None: ... + @property + def green(self) -> builtins.int: ... + @green.setter + def green(self, value: builtins.int) -> None: ... + @property + def blue(self) -> builtins.int: ... + @blue.setter + def blue(self, value: builtins.int) -> None: ... + +@typing.final +class RgbaColor: + @property + def red(self) -> builtins.int: ... + @red.setter + def red(self, value: builtins.int) -> None: ... + @property + def green(self) -> builtins.int: ... + @green.setter + def green(self, value: builtins.int) -> None: ... + @property + def blue(self) -> builtins.int: ... + @blue.setter + def blue(self, value: builtins.int) -> None: ... + @property + def alpha(self) -> builtins.int: ... + @alpha.setter + def alpha(self, value: builtins.int) -> None: ... + +@typing.final +class SlintToPyValue: + ... + +@typing.final +class StandardListViewItem: + @property + def text(self) -> typing.Any: ... + @text.setter + def text(self, value: typing.Any) -> None: ... + def __init__(self, *, text: typing.Any = ...) -> None: ... + +@typing.final +class StateInfo: + @property + def current_state(self) -> typing.Any: ... + @current_state.setter + def current_state(self, value: typing.Any) -> None: ... + @property + def previous_state(self) -> typing.Any: ... + @previous_state.setter + def previous_state(self, value: typing.Any) -> None: ... + def __init__(self, *, current_state: typing.Any = ..., previous_state: typing.Any = ...) -> None: ... + +@typing.final +class TableColumn: + @property + def title(self) -> typing.Any: ... + @title.setter + def title(self, value: typing.Any) -> None: ... + @property + def min_width(self) -> typing.Any: ... + @min_width.setter + def min_width(self, value: typing.Any) -> None: ... + @property + def horizontal_stretch(self) -> typing.Any: ... + @horizontal_stretch.setter + def horizontal_stretch(self, value: typing.Any) -> None: ... + @property + def sort_order(self) -> typing.Any: ... + @sort_order.setter + def sort_order(self, value: typing.Any) -> None: ... + @property + def width(self) -> typing.Any: ... + @width.setter + def width(self, value: typing.Any) -> None: ... + def __init__(self, *, title: typing.Any = ..., min_width: typing.Any = ..., horizontal_stretch: typing.Any = ..., sort_order: typing.Any = ..., width: typing.Any = ...) -> None: ... + +@typing.final +class Timer: + r""" + Timer is a handle to the timer system that triggers a callback after a specified + period of time. + + Use `Timer.start()` to create a timer that that repeatedly triggers a callback, or + `Timer.single_shot()` to trigger a callback only once. + + The timer will automatically stop when garbage collected. You must keep the Timer object + around for as long as you want the timer to keep firing. + + ```python + class AppWindow(...) + def __init__(self): + super().__init__() + self.my_timer = None + + @slint.callback + def button_clicked(self): + self.my_timer = slint.Timer() + self.my_timer.start(timedelta(seconds=1), self.do_something) + + def do_something(self): + pass + ``` + + Timers can only be used in the thread that runs the Slint event loop. They don't + fire if used in another thread. + """ + @property + def running(self) -> builtins.bool: + r""" + Set to true if the timer is running; false otherwise. + """ + @property + def interval(self) -> datetime.timedelta: ... + @interval.setter + def interval(self, value: datetime.timedelta) -> None: + r""" + The duration of timer. + + When setting this property and the timer is running (see `Timer.running`), + then the time when the callback will be next invoked is re-calculated to be in the + specified duration relative to when this property is set. + """ + def __new__(cls) -> Timer: ... + def start(self, mode: TimerMode, interval: datetime.timedelta, callback: typing.Any) -> None: + r""" + Starts the timer with the given mode and interval, in order for the callback to called when the + timer fires. If the timer has been started previously and not fired yet, then it will be restarted. + + Arguments: + * `mode`: The timer mode to apply, i.e. whether to repeatedly fire the timer or just once. + * `interval`: The duration from now until when the timer should firethe first time, and subsequently + for `TimerMode.Repeated` timers. + * `callback`: The function to call when the time has been reached or exceeded. + """ + @staticmethod + def single_shot(duration: datetime.timedelta, callback: typing.Any) -> None: + r""" + Starts the timer with the duration and the callback to called when the + timer fires. It is fired only once and then deleted. + + Arguments: + * `duration`: The duration from now until when the timer should fire. + * `callback`: The function to call when the time has been reached or exceeded. + """ + def stop(self) -> None: + r""" + Stops the previously started timer. Does nothing if the timer has never been started. + """ + def restart(self) -> None: + r""" + Restarts the timer. If the timer was previously started by calling `Timer.start()` + with a duration and callback, then the time when the callback will be next invoked + is re-calculated to be in the specified duration relative to when this function is called. + + Does nothing if the timer was never started. + """ + +@typing.final +class AccessibleRole(enum.Enum): + r""" + This enum represents the different values for the `accessible-role` property, used to describe the + role of an element in the context of assistive technology such as screen readers. + """ + none = ... + r""" + The element isn't accessible. + """ + button = ... + r""" + The element is a `Button` or behaves like one. + """ + checkbox = ... + r""" + The element is a `CheckBox` or behaves like one. + """ + combobox = ... + r""" + The element is a `ComboBox` or behaves like one. + """ + groupbox = ... + r""" + The element is a `GroupBox` or behaves like one. + """ + image = ... + r""" + The element is an `Image` or behaves like one. This is automatically applied to `Image` elements. + """ + list = ... + r""" + The element is a `ListView` or behaves like one. + """ + slider = ... + r""" + The element is a `Slider` or behaves like one. + """ + spinbox = ... + r""" + The element is a `SpinBox` or behaves like one. + """ + tab = ... + r""" + The element is a `Tab` or behaves like one. + """ + tablist = ... + r""" + The element is similar to the tab bar in a `TabWidget`. + """ + tabpanel = ... + r""" + The element is a container for tab content. + """ + text = ... + r""" + The role for a `Text` element. This is automatically applied to `Text` elements. + """ + table = ... + r""" + The role for a `TableView` or behaves like one. + """ + tree = ... + r""" + The role for a TreeView or behaves like one. (Not provided yet) + """ + progressindicator = ... + r""" + The element is a `ProgressIndicator` or behaves like one. + """ + textinput = ... + r""" + The role for widget with editable text such as a `LineEdit` or a `TextEdit`. + This is automatically applied to `TextInput` elements. + """ + switch = ... + r""" + The element is a `Switch` or behaves like one. + """ + listitem = ... + r""" + The element is an item in a `ListView`. + """ + +@typing.final +class AnimationDirection(enum.Enum): + r""" + This enum describes the direction of an animation. + """ + normal = ... + r""" + The ["normal" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#normal). + """ + reverse = ... + r""" + The ["reverse" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#reverse). + """ + alternate = ... + r""" + The ["alternate" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#alternate). + """ + alternatereverse = ... + r""" + The ["alternate reverse" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#alternate-reverse). + """ + +@typing.final +class ColorScheme(enum.Enum): + r""" + This enum indicates the color scheme used by the widget style. Use this to explicitly switch + between dark and light schemes, or choose Unknown to fall back to the system default. + """ + unknown = ... + r""" + The scheme is not known and a system wide setting configures this. This could mean that + the widgets are shown in a dark or light scheme, but it could also be a custom color scheme. + """ + dark = ... + r""" + The style chooses light colors for the background and dark for the foreground. + """ + light = ... + r""" + The style chooses dark colors for the background and light for the foreground. + """ + +@typing.final +class DiagnosticLevel(enum.Enum): + Error = ... + Warning = ... + +@typing.final +class DialogButtonRole(enum.Enum): + r""" + This enum represents the value of the `dialog-button-role` property which can be added to + any element within a `Dialog` to put that item in the button row, and its exact position + depends on the role and the platform. + """ + none = ... + r""" + This isn't a button meant to go into the bottom row + """ + accept = ... + r""" + This is the role of the main button to click to accept the dialog. e.g. "Ok" or "Yes" + """ + reject = ... + r""" + This is the role of the main button to click to reject the dialog. e.g. "Cancel" or "No" + """ + apply = ... + r""" + This is the role of the "Apply" button + """ + reset = ... + r""" + This is the role of the "Reset" button + """ + help = ... + r""" + This is the role of the "Help" button + """ + action = ... + r""" + This is the role of any other button that performs another action. + """ + +@typing.final +class EventResult(enum.Enum): + r""" + This enum describes whether an event was rejected or accepted by an event handler. + """ + reject = ... + r""" + The event is rejected by this event handler and may then be handled by the parent item + """ + accept = ... + r""" + The event is accepted and won't be processed further + """ + +@typing.final +class FillRule(enum.Enum): + r""" + This enum describes the different ways of deciding what the inside of a shape described by a path shall be. + """ + nonzero = ... + r""" + The ["nonzero" fill rule as defined in SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#nonzero). + """ + evenodd = ... + r""" + The ["evenodd" fill rule as defined in SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#evenodd) + """ + +@typing.final +class FocusReason(enum.Enum): + r""" + This enum describes the different reasons for a FocusEvent + """ + programmatic = ... + r""" + A built-in function invocation caused the event (`.focus()`, `.clear-focus()`) + """ + tabnavigation = ... + r""" + Keyboard navigation caused the event (tabbing) + """ + pointerclick = ... + r""" + A mouse click caused the event + """ + popupactivation = ... + r""" + A popup caused the event + """ + windowactivation = ... + r""" + The window manager changed the active window and caused the event + """ + +@typing.final +class ImageFit(enum.Enum): + r""" + This enum defines how the source image shall fit into an `Image` element. + """ + fill = ... + r""" + Scales and stretches the source image to fit the width and height of the `Image` element. + """ + contain = ... + r""" + The source image is scaled to fit into the `Image` element's dimension while preserving the aspect ratio. + """ + cover = ... + r""" + The source image is scaled to cover into the `Image` element's dimension while preserving the aspect ratio. + If the aspect ratio of the source image doesn't match the element's one, then the image will be clipped to fit. + """ + preserve = ... + r""" + Preserves the size of the source image in logical pixels. + The source image will still be scaled by the scale factor that applies to all elements in the window. + Any extra space will be left blank. + """ + +@typing.final +class ImageHorizontalAlignment(enum.Enum): + r""" + This enum specifies the horizontal alignment of the source image. + """ + center = ... + r""" + Aligns the source image at the center of the `Image` element. + """ + left = ... + r""" + Aligns the source image at the left of the `Image` element. + """ + right = ... + r""" + Aligns the source image at the right of the `Image` element. + """ + +@typing.final +class ImageRendering(enum.Enum): + r""" + This enum specifies how the source image will be scaled. + """ + smooth = ... + r""" + The image is scaled with a linear interpolation algorithm. + """ + pixelated = ... + r""" + The image is scaled with the nearest neighbor algorithm. + """ + +@typing.final +class ImageTiling(enum.Enum): + r""" + This enum specifies how the source image will be tiled. + """ + none = ... + r""" + The source image will not be tiled. + """ + repeat = ... + r""" + The source image will be repeated to fill the `Image` element. + """ + round = ... + r""" + The source image will be repeated and scaled to fill the `Image` element, ensuring an integer number of repetitions. + """ + +@typing.final +class ImageVerticalAlignment(enum.Enum): + r""" + This enum specifies the vertical alignment of the source image. + """ + center = ... + r""" + Aligns the source image at the center of the `Image` element. + """ + top = ... + r""" + Aligns the source image at the top of the `Image` element. + """ + bottom = ... + r""" + Aligns the source image at the bottom of the `Image` element. + """ + +@typing.final +class InputType(enum.Enum): + r""" + This enum is used to define the type of the input field. + """ + text = ... + r""" + The default value. This will render all characters normally + """ + password = ... + r""" + This will render all characters with a character that defaults to "*" + """ + number = ... + r""" + This will only accept and render number characters (0-9) + """ + decimal = ... + r""" + This will accept and render characters if it's valid part of a decimal + """ + +@typing.final +class LayoutAlignment(enum.Enum): + r""" + Enum representing the `alignment` property of a + `HorizontalBox`, a `VerticalBox`, + a `HorizontalLayout`, or `VerticalLayout`. + """ + stretch = ... + r""" + Use the minimum size of all elements in a layout, distribute remaining space + based on `*-stretch` among all elements. + """ + center = ... + r""" + Use the preferred size for all elements, distribute remaining space evenly before the + first and after the last element. + """ + start = ... + r""" + Use the preferred size for all elements, put remaining space after the last element. + """ + end = ... + r""" + Use the preferred size for all elements, put remaining space before the first + element. + """ + spacebetween = ... + r""" + Use the preferred size for all elements, distribute remaining space evenly between + elements. + """ + spacearound = ... + r""" + Use the preferred size for all elements, distribute remaining space evenly + between the elements, and use half spaces at the start and end. + """ + spaceevenly = ... + r""" + Use the preferred size for all elements, distribute remaining space evenly before the + first element, after the last element and between elements. + """ + +@typing.final +class LineCap(enum.Enum): + r""" + This enum describes the appearance of the ends of stroked paths. + """ + butt = ... + r""" + The stroke ends with a flat edge that is perpendicular to the path. + """ + round = ... + r""" + The stroke ends with a rounded edge. + """ + square = ... + r""" + The stroke ends with a square projection beyond the path. + """ + +@typing.final +class MouseCursor(enum.Enum): + r""" + This enum represents different types of mouse cursors. It's a subset of the mouse cursors available in CSS. + For details and pictograms see the [MDN Documentation for cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values). + Depending on the backend and used OS unidirectional resize cursors may be replaced with bidirectional ones. + """ + default = ... + r""" + The systems default cursor. + """ + none = ... + r""" + No cursor is displayed. + """ + help = ... + r""" + A cursor indicating help information. + """ + pointer = ... + r""" + A pointing hand indicating a link. + """ + progress = ... + r""" + The program is busy but can still be interacted with. + """ + wait = ... + r""" + The program is busy. + """ + crosshair = ... + r""" + A crosshair. + """ + text = ... + r""" + A cursor indicating selectable text. + """ + alias = ... + r""" + An alias or shortcut is being created. + """ + copy = ... + r""" + A copy is being created. + """ + move = ... + r""" + Something is to be moved. + """ + nodrop = ... + r""" + Something can't be dropped here. + """ + notallowed = ... + r""" + An action isn't allowed + """ + grab = ... + r""" + Something is grabbable. + """ + grabbing = ... + r""" + Something is being grabbed. + """ + colresize = ... + r""" + Indicating that a column is resizable horizontally. + """ + rowresize = ... + r""" + Indicating that a row is resizable vertically. + """ + nresize = ... + r""" + Unidirectional resize north. + """ + eresize = ... + r""" + Unidirectional resize east. + """ + sresize = ... + r""" + Unidirectional resize south. + """ + wresize = ... + r""" + Unidirectional resize west. + """ + neresize = ... + r""" + Unidirectional resize north-east. + """ + nwresize = ... + r""" + Unidirectional resize north-west. + """ + seresize = ... + r""" + Unidirectional resize south-east. + """ + swresize = ... + r""" + Unidirectional resize south-west. + """ + ewresize = ... + r""" + Bidirectional resize east-west. + """ + nsresize = ... + r""" + Bidirectional resize north-south. + """ + neswresize = ... + r""" + Bidirectional resize north-east-south-west. + """ + nwseresize = ... + r""" + Bidirectional resize north-west-south-east. + """ + +@typing.final +class OperatingSystemType(enum.Enum): + r""" + This enum describes the detected operating system types. + """ + android = ... + r""" + This variant includes any version of Android running mobile phones, tablets, as well as embedded Android devices. + """ + ios = ... + r""" + This variant covers iOS running on iPhones and iPads. + """ + macos = ... + r""" + This variant covers macOS running on Apple's Mac computers. + """ + linux = ... + r""" + This variant covers any version of Linux, except Android. + """ + windows = ... + r""" + This variant covers Microsoft Windows. + """ + other = ... + r""" + This variant is reported when the operating system is none of the above. + """ + +@typing.final +class Orientation(enum.Enum): + r""" + Represents the orientation of an element or widget such as the `Slider`. + """ + horizontal = ... + r""" + Element is oriented horizontally. + """ + vertical = ... + r""" + Element is oriented vertically. + """ + +@typing.final +class PathEvent(enum.Enum): + r""" + PathEvent is a low-level data structure describing the composition of a path. Typically it is + generated at compile time from a higher-level description, such as SVG commands. + """ + begin = ... + r""" + The beginning of the path. + """ + line = ... + r""" + A straight line on the path. + """ + quadratic = ... + r""" + A quadratic bezier curve on the path. + """ + cubic = ... + r""" + A cubic bezier curve on the path. + """ + endopen = ... + r""" + The end of the path that remains open. + """ + endclosed = ... + r""" + The end of a path that is closed. + """ + +@typing.final +class PointerEventButton(enum.Enum): + r""" + This enum describes the different types of buttons for a pointer event, + typically on a mouse or a pencil. + """ + other = ... + r""" + A button that is none of left, right, middle, back or forward. For example, + this is used for the task button on a mouse with many buttons. + """ + left = ... + r""" + The left button. + """ + right = ... + r""" + The right button. + """ + middle = ... + r""" + The center button. + """ + back = ... + r""" + The back button. + """ + forward = ... + r""" + The forward button. + """ + +@typing.final +class PointerEventKind(enum.Enum): + r""" + The enum reports what happened to the `PointerEventButton` in the event + """ + cancel = ... + r""" + The action was cancelled. + """ + down = ... + r""" + The button was pressed. + """ + up = ... + r""" + The button was released. + """ + move = ... + r""" + The pointer has moved, + """ + +@typing.final +class PopupClosePolicy(enum.Enum): + closeonclick = ... + r""" + Closes the `PopupWindow` when user clicks or presses the escape key. + """ + closeonclickoutside = ... + r""" + Closes the `PopupWindow` when user clicks outside of the popup or presses the escape key. + """ + noautoclose = ... + r""" + Does not close the `PopupWindow` automatically when user clicks. + """ + +@typing.final +class ScrollBarPolicy(enum.Enum): + r""" + This enum describes the scrollbar visibility + """ + asneeded = ... + r""" + Scrollbar will be visible only when needed + """ + alwaysoff = ... + r""" + Scrollbar never shown + """ + alwayson = ... + r""" + Scrollbar always visible + """ + +@typing.final +class SortOrder(enum.Enum): + r""" + This enum represents the different values of the `sort-order` property. + It's used to sort a `StandardTableView` by a column. + """ + unsorted = ... + r""" + The column is unsorted. + """ + ascending = ... + r""" + The column is sorted in ascending order. + """ + descending = ... + r""" + The column is sorted in descending order. + """ + +@typing.final +class StandardButtonKind(enum.Enum): + r""" + Use this enum to add standard buttons to a `Dialog`. The look and positioning + of these `StandardButton`s depends on the environment + (OS, UI environment, etc.) the application runs in. + """ + ok = ... + r""" + A "OK" button that accepts a `Dialog`, closing it when clicked. + """ + cancel = ... + r""" + A "Cancel" button that rejects a `Dialog`, closing it when clicked. + """ + apply = ... + r""" + A "Apply" button that should accept values from a + `Dialog` without closing it. + """ + close = ... + r""" + A "Close" button, which should close a `Dialog` without looking at values. + """ + reset = ... + r""" + A "Reset" button, which should reset the `Dialog` to its initial state. + """ + help = ... + r""" + A "Help" button, which should bring up context related documentation when clicked. + """ + yes = ... + r""" + A "Yes" button, used to confirm an action. + """ + no = ... + r""" + A "No" button, used to deny an action. + """ + abort = ... + r""" + A "Abort" button, used to abort an action. + """ + retry = ... + r""" + A "Retry" button, used to retry a failed action. + """ + ignore = ... + r""" + A "Ignore" button, used to ignore a failed action. + """ + +@typing.final +class TextHorizontalAlignment(enum.Enum): + r""" + This enum describes the different types of alignment of text along the horizontal axis of a `Text` element. + """ + left = ... + r""" + The text will be aligned with the left edge of the containing box. + """ + center = ... + r""" + The text will be horizontally centered within the containing box. + """ + right = ... + r""" + The text will be aligned to the right of the containing box. + """ + +@typing.final +class TextOverflow(enum.Enum): + r""" + This enum describes the how the text appear if it is too wide to fit in the `Text` width. + """ + clip = ... + r""" + The text will simply be clipped. + """ + elide = ... + r""" + The text will be elided with `…`. + """ + +@typing.final +class TextStrokeStyle(enum.Enum): + r""" + This enum describes the positioning of a text stroke relative to the border of the glyphs in a `Text`. + """ + outside = ... + r""" + The inside edge of the stroke is at the outer edge of the text. + """ + center = ... + r""" + The center line of the stroke is at the outer edge of the text, like in Adobe Illustrator. + """ + +@typing.final +class TextVerticalAlignment(enum.Enum): + r""" + This enum describes the different types of alignment of text along the vertical axis of a `Text` element. + """ + top = ... + r""" + The text will be aligned to the top of the containing box. + """ + center = ... + r""" + The text will be vertically centered within the containing box. + """ + bottom = ... + r""" + The text will be aligned to the bottom of the containing box. + """ + +@typing.final +class TextWrap(enum.Enum): + r""" + This enum describes the how the text wrap if it is too wide to fit in the `Text` width. + """ + nowrap = ... + r""" + The text won't wrap, but instead will overflow. + """ + wordwrap = ... + r""" + The text will be wrapped at word boundaries if possible, or at any location for very long words. + """ + charwrap = ... + r""" + The text will be wrapped at any character. Currently only supported by the Qt and Software renderers. + """ + +@typing.final +class TimerMode(enum.Enum): + r""" + The TimerMode specifies what should happen after the timer fired. + + Used by the `Timer.start()` function. + """ + SingleShot = ... + r""" + A SingleShot timer is fired only once. + """ + Repeated = ... + r""" + A Repeated timer is fired repeatedly until it is stopped or dropped. + """ + +@typing.final +class ValueType(enum.Enum): + Void = ... + Number = ... + String = ... + Bool = ... + Model = ... + Struct = ... + Brush = ... + Image = ... + Enumeration = ... + +def init_translations(translations: typing.Any) -> None: ... + +def invoke_from_event_loop(callable: typing.Any) -> None: ... + +def quit_event_loop() -> None: ... + +def run_event_loop() -> None: ... + +def set_xdg_app_id(app_id: builtins.str) -> None: ... -class AsyncAdapter: - def __new__( - cls, - fd: int, - ) -> "AsyncAdapter": ... - def wait_for_readable(self, callback: typing.Callable[[int], None]) -> None: ... - def wait_for_writable(self, callback: typing.Callable[[int], None]) -> None: ... From daa997e2d664ac42fa690f433ddc40b965123add Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Sat, 25 Oct 2025 22:20:06 +0800 Subject: [PATCH 28/52] refactor: restructure slint-compiler workflow --- .../workflows/upload_pypi_slint_compiler.yaml | 151 +++++++++++------- api/python/compiler/.gitignore | 1 - api/python/compiler/README.md | 15 -- api/python/compiler/pyproject.toml | 32 ---- .../compiler/slint_compiler/__init__.py | 67 -------- tools/compiler/.gitignore | 5 + tools/compiler/pyproject.toml | 16 ++ 7 files changed, 117 insertions(+), 170 deletions(-) delete mode 100644 api/python/compiler/.gitignore delete mode 100644 api/python/compiler/README.md delete mode 100644 api/python/compiler/pyproject.toml delete mode 100644 api/python/compiler/slint_compiler/__init__.py create mode 100644 tools/compiler/.gitignore create mode 100644 tools/compiler/pyproject.toml diff --git a/.github/workflows/upload_pypi_slint_compiler.yaml b/.github/workflows/upload_pypi_slint_compiler.yaml index 6156ef6b05b..e57df36744c 100644 --- a/.github/workflows/upload_pypi_slint_compiler.yaml +++ b/.github/workflows/upload_pypi_slint_compiler.yaml @@ -4,62 +4,103 @@ name: Upload slint-compiler to Python Package Index on: - workflow_dispatch: - inputs: - release: - type: boolean - default: false - required: false - description: "Release? If false, publish to test.pypi.org, if true, publish to pypi.org" + workflow_dispatch: + inputs: + release: + type: boolean + default: false + required: false + description: "Release? If false, publish to test.pypi.org, if true, publish to pypi.org" jobs: - publish-to-test-pypi: - if: ${{ github.event.inputs.release != 'true' }} - name: >- - Publish Python 🐍 distribution 📦 to Test PyPI - runs-on: ubuntu-latest - environment: - name: testpypi - url: https://test.pypi.org/p/slint-compiler - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - steps: - - uses: actions/checkout@v5 - - name: Install uv - uses: astral-sh/setup-uv@v7 - - name: Build - run: uv build - working-directory: api/python/compiler - - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: api/python/compiler/dist/* - - name: Publish - run: uv publish --publish-url https://test.pypi.org/legacy/ - working-directory: api/python/compiler + build-wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + [ + ubuntu-latest, + ubuntu-24.04-arm, + windows-latest, + windows-11-arm, + macos-15-intel, + macos-14, + ] + steps: + - uses: actions/checkout@v5 + - uses: ./.github/actions/setup-rust + - name: Build wheels + uses: pypa/cibuildwheel@v3.2.1 + with: + package-dir: tools/compiler + output-dir: wheelhouse + env: + CIBW_PROJECT_REQUIRES_PYTHON: ">=3.10" + CIBW_BUILD_FRONTEND: "build" + - uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl - publish-to-pypi: - if: ${{ github.event.inputs.release == 'true' }} - name: >- - Publish Python 🐍 distribution 📦 to PyPI - runs-on: ubuntu-latest - environment: - name: pypi - url: https://test.pypi.org/p/slint-compiler - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - steps: - - uses: actions/checkout@v5 - - name: Install uv - uses: astral-sh/setup-uv@v7 - - name: Build - run: uv build - working-directory: api/python/compiler - - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: api/python/compiler/dist/* - - name: Publish - run: uv publish - working-directory: api/python/compiler + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v7 + - name: Build sdist + run: uv build --sdist -o dist tools/compiler + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + publish-to-test-pypi: + if: ${{ github.event.inputs.release != 'true' }} + needs: [build-wheels, build-sdist] + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/slint-compiler + permissions: + id-token: write + steps: + - uses: astral-sh/setup-uv@v7 + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + - name: Publish to Test PyPI + env: + UV_PUBLISH_URL: https://test.pypi.org/legacy/ + run: uv publish dist/* + + publish-to-pypi: + if: ${{ github.event.inputs.release == 'true' }} + needs: [build-wheels, build-sdist] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/slint-compiler + permissions: + id-token: write + steps: + - uses: astral-sh/setup-uv@v7 + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + - name: Publish to PyPI + run: uv publish dist/* diff --git a/api/python/compiler/.gitignore b/api/python/compiler/.gitignore deleted file mode 100644 index 11041c78340..00000000000 --- a/api/python/compiler/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.egg-info diff --git a/api/python/compiler/README.md b/api/python/compiler/README.md deleted file mode 100644 index 0e41baf771b..00000000000 --- a/api/python/compiler/README.md +++ /dev/null @@ -1,15 +0,0 @@ - - -# Python Slint Compiler - -This package is a wrapper around [Slint's](https://slint.dev) compiler for Python, to generate a typed `.py` file from a `.slint` file, for use with [Slint for Python](https://pypi.org/project/slint/). - -When run, the slint compiler binary is downloaded, cached, and run. - -By default, the Slint compiler matching the version of this package is downloaded. To select a specific version, set the `SLINT_COMPILER_VERSION` environment variable. Set it to `nightly` to select the latest nightly release. - -## Example - -```bash -uxv run slint-compiler -f python -o app_window.py app-window.slint -``` diff --git a/api/python/compiler/pyproject.toml b/api/python/compiler/pyproject.toml deleted file mode 100644 index ab05ede4e58..00000000000 --- a/api/python/compiler/pyproject.toml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -[project] -name = "slint-compiler" -version = "1.15.0b1" -description = "Python wrapper around the Slint compiler for Python" -authors = [{ name = "Slint Team", email = "info@slint.dev" }] -readme = "README.md" -requires-python = ">=3.10" -dependencies = ["cached-path>=1.7.3"] - -[project.urls] -Homepage = "https://slint.dev" -Documentation = "https://slint.dev/docs" -Repository = "https://github.com/slint-ui/slint" -Changelog = "https://github.com/slint-ui/slint/blob/master/CHANGELOG.md" -Tracker = "https://github.com/slint-ui/slint/issues" - -[project.scripts] -slint-compiler = "slint_compiler:main" - -[dependency-groups] -dev = ["mypy>=1.15.0"] - -[tool.mypy] -strict = true -disallow_subclassing_any = false - -[[tool.mypy.overrides]] -module = ["cached_path.*"] -follow_untyped_imports = true diff --git a/api/python/compiler/slint_compiler/__init__.py b/api/python/compiler/slint_compiler/__init__.py deleted file mode 100644 index 23ed38ff3ce..00000000000 --- a/api/python/compiler/slint_compiler/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -import cached_path -import platform -import sys -import subprocess -import re -import os -from importlib.metadata import version, PackageNotFoundError - - -def main() -> None: - try: - package_version = version("slint-compiler") - # Strip alpha/beta from for example "1.13.0b1" - version_regex = re.search("([0-9]*)\\.([0-9]*)\\.([0-9]*).*", package_version) - assert version_regex is not None - major = version_regex.group(1) - minor = version_regex.group(2) - patch = version_regex.group(3) - github_release = f"v{major}.{minor}.{patch}" - except PackageNotFoundError: - github_release = "nightly" - - # Permit override by environment variable - github_release = os.environ.get("SLINT_COMPILER_VERSION") or github_release - - operating_system = { - "darwin": "Darwin", - "linux": "Linux", - "win32": "Windows", - "msys": "Windows", - }[sys.platform] - arch = { - "aarch64": "aarch64", - "amd64": "x86_64", - "arm64": "aarch64", - "x86_64": "x86_64", - }[platform.machine().lower()] - exe_suffix = "" - - if operating_system == "Windows": - arch = {"aarch64": "ARM64", "x86_64": "AMD64"}[arch] - exe_suffix = ".exe" - elif operating_system == "Linux": - pass - elif operating_system == "Darwin": - arch = {"aarch64": "arm64", "x86_64": "x86_64"}[arch] - else: - raise Exception(f"Unsupported operarating system: {operating_system}") - - platform_suffix = f"{operating_system}-{arch}" - prebuilt_archive_filename = f"slint-compiler-{platform_suffix}.tar.gz" - download_url = f"https://github.com/slint-ui/slint/releases/download/{github_release}/{prebuilt_archive_filename}" - url_and_path_within_archive = f"{download_url}!slint-compiler{exe_suffix}" - - local_path = cached_path.cached_path( - url_and_path_within_archive, extract_archive=True - ) - args = [str(local_path)] - args.extend(sys.argv[1:]) - subprocess.run(args) - - -if __name__ == "__main__": - main() diff --git a/tools/compiler/.gitignore b/tools/compiler/.gitignore new file mode 100644 index 00000000000..41bfc189309 --- /dev/null +++ b/tools/compiler/.gitignore @@ -0,0 +1,5 @@ +/target/ +/wheelhouse/ +/dist/ +*.egg-info/ +/build/ diff --git a/tools/compiler/pyproject.toml b/tools/compiler/pyproject.toml new file mode 100644 index 00000000000..0aa0f97ac11 --- /dev/null +++ b/tools/compiler/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["maturin>=1.9,<2.0"] +build-backend = "maturin" + +[project] +name = "slint-compiler" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", +] +dynamic = ["version"] + +[tool.maturin] +bindings = "bin" +manifest-path = "Cargo.toml" From 2928dc5ca283a7a5c9848adc345c3b349c870ba2 Mon Sep 17 00:00:00 2001 From: Elaina Date: Tue, 28 Oct 2025 10:39:44 +0000 Subject: [PATCH 29/52] chore: update workflow to use specific Ubuntu versions and add project URLs in pyproject.toml --- .github/workflows/upload_pypi_slint_compiler.yaml | 5 ++--- tools/compiler/pyproject.toml | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/upload_pypi_slint_compiler.yaml b/.github/workflows/upload_pypi_slint_compiler.yaml index e57df36744c..1e5f1127a84 100644 --- a/.github/workflows/upload_pypi_slint_compiler.yaml +++ b/.github/workflows/upload_pypi_slint_compiler.yaml @@ -21,11 +21,10 @@ jobs: matrix: os: [ - ubuntu-latest, - ubuntu-24.04-arm, + ubuntu-22.04, + ubuntu-22.04-arm, windows-latest, windows-11-arm, - macos-15-intel, macos-14, ] steps: diff --git a/tools/compiler/pyproject.toml b/tools/compiler/pyproject.toml index 0aa0f97ac11..e3d4e70e8c3 100644 --- a/tools/compiler/pyproject.toml +++ b/tools/compiler/pyproject.toml @@ -11,6 +11,13 @@ classifiers = [ ] dynamic = ["version"] +[project.urls] +Homepage = "https://slint.dev" +Documentation = "https://slint.dev/docs" +Repository = "https://github.com/slint-ui/slint" +Changelog = "https://github.com/slint-ui/slint/blob/master/CHANGELOG.md" +Tracker = "https://github.com/slint-ui/slint/issues" + [tool.maturin] bindings = "bin" manifest-path = "Cargo.toml" From 7b4e1e60ee103566ac44d9a78ffc8b5053f22e96 Mon Sep 17 00:00:00 2001 From: Elaina Date: Tue, 28 Oct 2025 10:51:11 +0000 Subject: [PATCH 30/52] ci: remove all related to api/python/compiler --- .github/workflows/ci.yaml | 3 --- .github/workflows/nightly_tests.yaml | 4 ++-- .github/workflows/upgrade_version.yaml | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ae5f2367bb6..2ce4112b79f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -220,9 +220,6 @@ jobs: - name: Run mypy working-directory: api/python/slint run: uv run mypy -p tests -p slint - - name: Run mypy on slint-compiler - working-directory: api/python/compiler - run: uv run mypy slint_compiler - name: Run ruff linter working-directory: api/python/slint run: uv tool run ruff check diff --git a/.github/workflows/nightly_tests.yaml b/.github/workflows/nightly_tests.yaml index 2a627e7508a..5ba1838f072 100644 --- a/.github/workflows/nightly_tests.yaml +++ b/.github/workflows/nightly_tests.yaml @@ -229,8 +229,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 - name: Run slint compiler via uxv - working-directory: api/python/compiler + working-directory: api/python/ env: SLINT_COMPILER_VERSION: nightly # TODO: generate python code when the python generator lands - run: uvx --with-editable=. slint-compiler ../../../demos/printerdemo/ui/printerdemo.slint + run: uvx --with-editable=. slint-compiler ../../demos/printerdemo/ui/printerdemo.slint diff --git a/.github/workflows/upgrade_version.yaml b/.github/workflows/upgrade_version.yaml index 77d3d4d9382..ca8b8a76cad 100644 --- a/.github/workflows/upgrade_version.yaml +++ b/.github/workflows/upgrade_version.yaml @@ -26,7 +26,7 @@ jobs: sed -i 's/ VERSION [0-9]*\.[0-9]*\.[0-9]*)$/ VERSION ${{ github.event.inputs.new_version }})/' api/cpp/CMakeLists.txt # The version is also in these files - sed -i "s/^version = \"[0-9]*\.[0-9]*\.[0-9]*\(.*\)\"/version = \"${{ github.event.inputs.new_version }}\1\"/" api/cpp/docs/conf.py api/python/slint/pyproject.toml api/python/briefcase/pyproject.toml api/python/compiler/pyproject.toml editors/zed/extension.toml + sed -i "s/^version = \"[0-9]*\.[0-9]*\.[0-9]*\(.*\)\"/version = \"${{ github.event.inputs.new_version }}\1\"/" api/cpp/docs/conf.py api/python/slint/pyproject.toml api/python/briefcase/pyproject.toml editors/zed/extension.toml # Version in package.json files git ls-files | grep package.json | xargs sed -i 's/"version": ".*"/"version": "${{ github.event.inputs.new_version }}"/' From 014af7f48e134ad773e47ec340718de4768a7974 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:58:16 +0000 Subject: [PATCH 31/52] [autofix.ci] apply automated fixes --- tools/compiler/pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/compiler/pyproject.toml b/tools/compiler/pyproject.toml index e3d4e70e8c3..91f62d45051 100644 --- a/tools/compiler/pyproject.toml +++ b/tools/compiler/pyproject.toml @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + [build-system] requires = ["maturin>=1.9,<2.0"] build-backend = "maturin" @@ -5,10 +8,7 @@ build-backend = "maturin" [project] name = "slint-compiler" requires-python = ">=3.10" -classifiers = [ - "Programming Language :: Rust", - "Programming Language :: Python :: Implementation :: CPython", -] +classifiers = ["Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython"] dynamic = ["version"] [project.urls] From 44b28b2c74934712b1041da01ba7ac034bbbb6ee Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:18:26 +0000 Subject: [PATCH 32/52] [autofix.ci] apply automated fixes --- api/python/slint/interpreter.rs | 23 +- api/python/slint/models.rs | 8 +- api/python/slint/slint/codegen/cli.py | 2 +- api/python/slint/slint/codegen/generator.py | 13 +- api/python/slint/slint/core.pyi | 244 +++++++++--- api/python/slint/stub-gen/main.rs | 3 + .../tests/codegen/examples/counter/counter.py | 9 +- .../codegen/examples/counter/counter.pyi | 9 +- api/python/slint/tests/test_enums.py | 4 +- .../slint/tests/test_load_file_source.py | 47 ++- .../slint/tests/test_load_file_source.pyi | 376 ++++++++++-------- 11 files changed, 482 insertions(+), 256 deletions(-) diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index 2670cdb099e..8cca1391f4d 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -299,19 +299,28 @@ impl ComponentDefinition { } fn global_properties(&self, name: &str) -> IndexMap { - self.definition.global_properties_and_callbacks(name).map(|propiter| { - propiter - .filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (name, ty.into()))) - .collect() - }).unwrap_or_default() + self.definition + .global_properties_and_callbacks(name) + .map(|propiter| { + propiter + .filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (name, ty.into()))) + .collect() + }) + .unwrap_or_default() } fn global_callbacks(&self, name: &str) -> Vec { - self.definition.global_callbacks(name).map(|callbackiter| callbackiter.collect()).unwrap_or_default() + self.definition + .global_callbacks(name) + .map(|callbackiter| callbackiter.collect()) + .unwrap_or_default() } fn global_functions(&self, name: &str) -> Vec { - self.definition.global_functions(name).map(|functioniter| functioniter.collect()).unwrap_or_default() + self.definition + .global_functions(name) + .map(|functioniter| functioniter.collect()) + .unwrap_or_default() } fn global_property_infos(&self, global_name: &str) -> Option> { diff --git a/api/python/slint/models.rs b/api/python/slint/models.rs index b6941a026c4..e7a531e2ef5 100644 --- a/api/python/slint/models.rs +++ b/api/python/slint/models.rs @@ -96,17 +96,13 @@ impl PyModelBase { /// Returns the number of rows available in the model. #[gen_stub(abstractmethod)] fn row_count(&self) -> PyResult { - Err(PyNotImplementedError::new_err( - "Model subclasses must override row_count()", - )) + Err(PyNotImplementedError::new_err("Model subclasses must override row_count()")) } /// Returns the data for the given row in the model. #[gen_stub(abstractmethod)] fn row_data(&self, _row: usize) -> PyResult>> { - Err(PyNotImplementedError::new_err( - "Model subclasses must override row_data()", - )) + Err(PyNotImplementedError::new_err("Model subclasses must override row_data()")) } /// Call this method on mutable models to change the data for the given row. diff --git a/api/python/slint/slint/codegen/cli.py b/api/python/slint/slint/codegen/cli.py index 8f1c95fc897..a3d33cd69c8 100644 --- a/api/python/slint/slint/codegen/cli.py +++ b/api/python/slint/slint/codegen/cli.py @@ -109,7 +109,7 @@ def main(argv: list[str] | None = None) -> int: if command not in (None, "generate"): parser.error(f"Unknown command: {command}") return 1 - + if not args.inputs: print("error: At least one --input must be provided.") return 1 diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index f94220aa265..15a36fa499a 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -133,7 +133,9 @@ def copy_slint_file(source: Path, destination: Path) -> None: summary_lines: list[str] = [] struct_note = f" ({struct_only_modules} struct-only)" if struct_only_modules else "" - summary_lines.append(f"info: Generated {generated_modules} Python module(s){struct_note}") + summary_lines.append( + f"info: Generated {generated_modules} Python module(s){struct_note}" + ) if output_dir is not None: summary_lines.append( @@ -244,13 +246,16 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: globals_meta: list[GlobalMeta] = [] for global_name in comp.globals: global_property_info = { - info.name: info for info in comp.global_property_infos(global_name) or [] + info.name: info + for info in comp.global_property_infos(global_name) or [] } global_callback_info = { - info.name: info for info in comp.global_callback_infos(global_name) or [] + info.name: info + for info in comp.global_callback_infos(global_name) or [] } global_function_info = { - info.name: info for info in comp.global_function_infos(global_name) or [] + info.name: info + for info in comp.global_function_infos(global_name) or [] } properties_meta: list[PropertyMeta] = [] diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index 475e5b7c82f..ed1d501d739 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # This file is automatically generated by pyo3_stub_gen # ruff: noqa: E501, F401 @@ -20,9 +23,9 @@ class Brush: r""" A brush is a data structure that is used to describe how a shape, such as a rectangle, path or even text, shall be filled. A brush can also be applied to the outline of a shape, that means the fill of the outline itself. - + Brushes can only be constructed from solid colors. - + **Note:** In future, we plan to reduce this constraint and allow for declaring graidient brushes programmatically. """ @property @@ -54,9 +57,9 @@ class Brush: def transparentize(self, amount: builtins.float) -> Brush: r""" Returns a new version of this brush with the opacity decreased by `factor`. - + The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. - + See also `Color.transparentize`. """ def with_alpha(self, alpha: builtins.float) -> Brush: @@ -87,7 +90,7 @@ class Color: r""" A Color object represents a color in the RGB color space with an alpha. Each color channel and the alpha is represented as an 8-bit integer. The alpha channel is 0 for fully transparent and 255 for fully opaque. - + Construct colors from a CSS color string, or by specifying the red, green, blue, and (optional) alpha channels in a dict. """ @property @@ -110,7 +113,12 @@ class Color: r""" The alpha channel. """ - def __new__(cls, maybe_value: typing.Optional[builtins.str | PyColorInput.RgbaColor | PyColorInput.RgbColor] = None) -> Color: ... + def __new__( + cls, + maybe_value: typing.Optional[ + builtins.str | PyColorInput.RgbaColor | PyColorInput.RgbColor + ] = None, + ) -> Color: ... def brighter(self, factor: builtins.float) -> Color: r""" Returns a new color that is brighter than this color by the given factor. @@ -122,7 +130,7 @@ class Color: def transparentize(self, factor: builtins.float) -> Color: r""" Returns a new version of this color with the opacity decreased by `factor`. - + The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. """ def mix(self, other: Color, factor: builtins.float) -> Color: @@ -145,7 +153,11 @@ class CompilationResult: @property def diagnostics(self) -> builtins.list[PyDiagnostic]: ... @property - def structs_and_enums(self) -> tuple[builtins.dict[builtins.str, typing.Any], builtins.dict[builtins.str, typing.Any]]: ... + def structs_and_enums( + self, + ) -> tuple[ + builtins.dict[builtins.str, typing.Any], builtins.dict[builtins.str, typing.Any] + ]: ... @property def named_exports(self) -> builtins.list[tuple[builtins.str, builtins.str]]: ... @property @@ -165,11 +177,17 @@ class Compiler: @property def library_paths(self) -> builtins.dict[builtins.str, pathlib.Path]: ... @library_paths.setter - def library_paths(self, value: builtins.dict[builtins.str, pathlib.Path]) -> None: ... + def library_paths( + self, value: builtins.dict[builtins.str, pathlib.Path] + ) -> None: ... def __new__(cls) -> Compiler: ... def set_translation_domain(self, domain: builtins.str) -> None: ... - def build_from_path(self, path: builtins.str | os.PathLike | pathlib.Path) -> CompilationResult: ... - def build_from_source(self, source_code: builtins.str, path: builtins.str | os.PathLike | pathlib.Path) -> CompilationResult: ... + def build_from_path( + self, path: builtins.str | os.PathLike | pathlib.Path + ) -> CompilationResult: ... + def build_from_source( + self, source_code: builtins.str, path: builtins.str | os.PathLike | pathlib.Path + ) -> CompilationResult: ... @typing.final class ComponentDefinition: @@ -186,14 +204,24 @@ class ComponentDefinition: def property_infos(self) -> builtins.list[PropertyInfo]: ... def callback_infos(self) -> builtins.list[CallbackInfo]: ... def function_infos(self) -> builtins.list[FunctionInfo]: ... - def global_properties(self, name: builtins.str) -> builtins.dict[builtins.str, ValueType]: ... + def global_properties( + self, name: builtins.str + ) -> builtins.dict[builtins.str, ValueType]: ... def global_callbacks(self, name: builtins.str) -> builtins.list[builtins.str]: ... def global_functions(self, name: builtins.str) -> builtins.list[builtins.str]: ... - def global_property_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[PropertyInfo]]: ... - def global_callback_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[CallbackInfo]]: ... - def global_function_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[FunctionInfo]]: ... + def global_property_infos( + self, global_name: builtins.str + ) -> typing.Optional[builtins.list[PropertyInfo]]: ... + def global_callback_infos( + self, global_name: builtins.str + ) -> typing.Optional[builtins.list[CallbackInfo]]: ... + def global_function_infos( + self, global_name: builtins.str + ) -> typing.Optional[builtins.list[FunctionInfo]]: ... def callback_returns_void(self, callback_name: builtins.str) -> builtins.bool: ... - def global_callback_returns_void(self, global_name: builtins.str, callback_name: builtins.str) -> builtins.bool: ... + def global_callback_returns_void( + self, global_name: builtins.str, callback_name: builtins.str + ) -> builtins.bool: ... def create(self) -> ComponentInstance: ... @typing.final @@ -202,12 +230,23 @@ class ComponentInstance: def definition(self) -> ComponentDefinition: ... def get_property(self, name: builtins.str) -> typing.Any: ... def set_property(self, name: builtins.str, value: typing.Any) -> None: ... - def get_global_property(self, global_name: builtins.str, prop_name: builtins.str) -> typing.Any: ... - def set_global_property(self, global_name: builtins.str, prop_name: builtins.str, value: typing.Any) -> None: ... + def get_global_property( + self, global_name: builtins.str, prop_name: builtins.str + ) -> typing.Any: ... + def set_global_property( + self, global_name: builtins.str, prop_name: builtins.str, value: typing.Any + ) -> None: ... def invoke(self, callback_name: builtins.str, *args: typing.Any) -> typing.Any: ... - def invoke_global(self, global_name: builtins.str, callback_name: builtins.str, *args: typing.Any) -> typing.Any: ... + def invoke_global( + self, global_name: builtins.str, callback_name: builtins.str, *args: typing.Any + ) -> typing.Any: ... def set_callback(self, name: builtins.str, callable: typing.Any) -> None: ... - def set_global_callback(self, global_name: builtins.str, callback_name: builtins.str, callable: typing.Any) -> None: ... + def set_global_callback( + self, + global_name: builtins.str, + callback_name: builtins.str, + callable: typing.Any, + ) -> None: ... def show(self) -> None: ... def hide(self) -> None: ... def on_close_requested(self, callable: typing.Any) -> None: ... @@ -228,7 +267,13 @@ class DropEvent: def position(self) -> typing.Any: ... @position.setter def position(self, value: typing.Any) -> None: ... - def __init__(self, *, mime_type: typing.Any = ..., data: typing.Any = ..., position: typing.Any = ...) -> None: ... + def __init__( + self, + *, + mime_type: typing.Any = ..., + data: typing.Any = ..., + position: typing.Any = ..., + ) -> None: ... @typing.final class FontMetrics: @@ -248,7 +293,14 @@ class FontMetrics: def cap_height(self) -> typing.Any: ... @cap_height.setter def cap_height(self, value: typing.Any) -> None: ... - def __init__(self, *, ascent: typing.Any = ..., descent: typing.Any = ..., x_height: typing.Any = ..., cap_height: typing.Any = ...) -> None: ... + def __init__( + self, + *, + ascent: typing.Any = ..., + descent: typing.Any = ..., + x_height: typing.Any = ..., + cap_height: typing.Any = ..., + ) -> None: ... @typing.final class FunctionInfo: @@ -301,41 +353,41 @@ class Image: r""" Creates a new image from an array-like object that implements the [Buffer Protocol](https://docs.python.org/3/c-api/buffer.html). Use this function to import images created by third-party modules such as matplotlib or Pillow. - + The array must satisfy certain contraints to represent an image: - + - The buffer's format needs to be `B` (unsigned char) - The shape must be a tuple of (height, width, bytes-per-pixel) - If a stride is defined, the row stride must be equal to width * bytes-per-pixel, and the column stride must equal the bytes-per-pixel. - A value of 3 for bytes-per-pixel is interpreted as RGB image, a value of 4 means RGBA. - + The image is created by performing a deep copy of the array's data. Subsequent changes to the buffer are not automatically reflected in a previously created Image. - + Example of importing a matplot figure into an image: ```python import slint import matplotlib - + from matplotlib.backends.backend_agg import FigureCanvasAgg from matplotlib.figure import Figure - + fig = Figure(figsize=(5, 4), dpi=100) canvas = FigureCanvasAgg(fig) ax = fig.add_subplot() ax.plot([1, 2, 3]) canvas.draw() - + buffer = canvas.buffer_rgba() img = slint.Image.load_from_array(buffer) ``` - + Example of loading an image with Pillow: ```python import slint from PIL import Image import numpy as np - + pil_img = Image.open("hello.jpeg") array = np.array(pil_img) img = slint.Image.load_from_array(array) @@ -356,7 +408,13 @@ class KeyEvent: def repeat(self) -> typing.Any: ... @repeat.setter def repeat(self, value: typing.Any) -> None: ... - def __init__(self, *, text: typing.Any = ..., modifiers: typing.Any = ..., repeat: typing.Any = ...) -> None: ... + def __init__( + self, + *, + text: typing.Any = ..., + modifiers: typing.Any = ..., + repeat: typing.Any = ..., + ) -> None: ... @typing.final class KeyboardModifiers: @@ -376,7 +434,14 @@ class KeyboardModifiers: def meta(self) -> typing.Any: ... @meta.setter def meta(self, value: typing.Any) -> None: ... - def __init__(self, *, alt: typing.Any = ..., control: typing.Any = ..., shift: typing.Any = ..., meta: typing.Any = ...) -> None: ... + def __init__( + self, + *, + alt: typing.Any = ..., + control: typing.Any = ..., + shift: typing.Any = ..., + meta: typing.Any = ..., + ) -> None: ... @typing.final class MenuEntry: @@ -412,7 +477,18 @@ class MenuEntry: def is_separator(self) -> typing.Any: ... @is_separator.setter def is_separator(self, value: typing.Any) -> None: ... - def __init__(self, *, title: typing.Any = ..., icon: typing.Any = ..., id: typing.Any = ..., enabled: typing.Any = ..., checkable: typing.Any = ..., checked: typing.Any = ..., has_sub_menu: typing.Any = ..., is_separator: typing.Any = ...) -> None: ... + def __init__( + self, + *, + title: typing.Any = ..., + icon: typing.Any = ..., + id: typing.Any = ..., + enabled: typing.Any = ..., + checkable: typing.Any = ..., + checked: typing.Any = ..., + has_sub_menu: typing.Any = ..., + is_separator: typing.Any = ..., + ) -> None: ... @typing.final class PointerEvent: @@ -428,7 +504,13 @@ class PointerEvent: def modifiers(self) -> typing.Any: ... @modifiers.setter def modifiers(self, value: typing.Any) -> None: ... - def __init__(self, *, button: typing.Any = ..., kind: typing.Any = ..., modifiers: typing.Any = ...) -> None: ... + def __init__( + self, + *, + button: typing.Any = ..., + kind: typing.Any = ..., + modifiers: typing.Any = ..., + ) -> None: ... @typing.final class PointerScrollEvent: @@ -444,7 +526,13 @@ class PointerScrollEvent: def modifiers(self) -> typing.Any: ... @modifiers.setter def modifiers(self, value: typing.Any) -> None: ... - def __init__(self, *, delta_x: typing.Any = ..., delta_y: typing.Any = ..., modifiers: typing.Any = ...) -> None: ... + def __init__( + self, + *, + delta_x: typing.Any = ..., + delta_y: typing.Any = ..., + modifiers: typing.Any = ..., + ) -> None: ... @typing.final class PropertyInfo: @@ -462,18 +550,18 @@ class PyColorInput: def __new__(cls, _0: builtins.str) -> PyColorInput.ColorStr: ... def __len__(self) -> builtins.int: ... def __getitem__(self, key: builtins.int) -> typing.Any: ... - + class RgbaColor(typing.TypedDict): red: builtins.int green: builtins.int blue: builtins.int alpha: builtins.int - + class RgbColor(typing.TypedDict): red: builtins.int green: builtins.int blue: builtins.int - + ... @typing.final @@ -585,8 +673,7 @@ class RgbaColor: def alpha(self, value: builtins.int) -> None: ... @typing.final -class SlintToPyValue: - ... +class SlintToPyValue: ... @typing.final class StandardListViewItem: @@ -606,7 +693,9 @@ class StateInfo: def previous_state(self) -> typing.Any: ... @previous_state.setter def previous_state(self, value: typing.Any) -> None: ... - def __init__(self, *, current_state: typing.Any = ..., previous_state: typing.Any = ...) -> None: ... + def __init__( + self, *, current_state: typing.Any = ..., previous_state: typing.Any = ... + ) -> None: ... @typing.final class TableColumn: @@ -630,35 +719,43 @@ class TableColumn: def width(self) -> typing.Any: ... @width.setter def width(self, value: typing.Any) -> None: ... - def __init__(self, *, title: typing.Any = ..., min_width: typing.Any = ..., horizontal_stretch: typing.Any = ..., sort_order: typing.Any = ..., width: typing.Any = ...) -> None: ... + def __init__( + self, + *, + title: typing.Any = ..., + min_width: typing.Any = ..., + horizontal_stretch: typing.Any = ..., + sort_order: typing.Any = ..., + width: typing.Any = ..., + ) -> None: ... @typing.final class Timer: r""" Timer is a handle to the timer system that triggers a callback after a specified period of time. - + Use `Timer.start()` to create a timer that that repeatedly triggers a callback, or `Timer.single_shot()` to trigger a callback only once. - + The timer will automatically stop when garbage collected. You must keep the Timer object around for as long as you want the timer to keep firing. - + ```python class AppWindow(...) def __init__(self): super().__init__() self.my_timer = None - + @slint.callback def button_clicked(self): self.my_timer = slint.Timer() self.my_timer.start(timedelta(seconds=1), self.do_something) - + def do_something(self): pass ``` - + Timers can only be used in the thread that runs the Slint event loop. They don't fire if used in another thread. """ @@ -673,17 +770,19 @@ class Timer: def interval(self, value: datetime.timedelta) -> None: r""" The duration of timer. - + When setting this property and the timer is running (see `Timer.running`), then the time when the callback will be next invoked is re-calculated to be in the specified duration relative to when this property is set. """ def __new__(cls) -> Timer: ... - def start(self, mode: TimerMode, interval: datetime.timedelta, callback: typing.Any) -> None: + def start( + self, mode: TimerMode, interval: datetime.timedelta, callback: typing.Any + ) -> None: r""" Starts the timer with the given mode and interval, in order for the callback to called when the timer fires. If the timer has been started previously and not fired yet, then it will be restarted. - + Arguments: * `mode`: The timer mode to apply, i.e. whether to repeatedly fire the timer or just once. * `interval`: The duration from now until when the timer should firethe first time, and subsequently @@ -695,7 +794,7 @@ class Timer: r""" Starts the timer with the duration and the callback to called when the timer fires. It is fired only once and then deleted. - + Arguments: * `duration`: The duration from now until when the timer should fire. * `callback`: The function to call when the time has been reached or exceeded. @@ -709,7 +808,7 @@ class Timer: Restarts the timer. If the timer was previously started by calling `Timer.start()` with a duration and callback, then the time when the callback will be next invoked is re-calculated to be in the specified duration relative to when this function is called. - + Does nothing if the timer was never started. """ @@ -719,6 +818,7 @@ class AccessibleRole(enum.Enum): This enum represents the different values for the `accessible-role` property, used to describe the role of an element in the context of assistive technology such as screen readers. """ + none = ... r""" The element isn't accessible. @@ -802,6 +902,7 @@ class AnimationDirection(enum.Enum): r""" This enum describes the direction of an animation. """ + normal = ... r""" The ["normal" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#normal). @@ -825,6 +926,7 @@ class ColorScheme(enum.Enum): This enum indicates the color scheme used by the widget style. Use this to explicitly switch between dark and light schemes, or choose Unknown to fall back to the system default. """ + unknown = ... r""" The scheme is not known and a system wide setting configures this. This could mean that @@ -851,6 +953,7 @@ class DialogButtonRole(enum.Enum): any element within a `Dialog` to put that item in the button row, and its exact position depends on the role and the platform. """ + none = ... r""" This isn't a button meant to go into the bottom row @@ -885,6 +988,7 @@ class EventResult(enum.Enum): r""" This enum describes whether an event was rejected or accepted by an event handler. """ + reject = ... r""" The event is rejected by this event handler and may then be handled by the parent item @@ -899,6 +1003,7 @@ class FillRule(enum.Enum): r""" This enum describes the different ways of deciding what the inside of a shape described by a path shall be. """ + nonzero = ... r""" The ["nonzero" fill rule as defined in SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#nonzero). @@ -913,6 +1018,7 @@ class FocusReason(enum.Enum): r""" This enum describes the different reasons for a FocusEvent """ + programmatic = ... r""" A built-in function invocation caused the event (`.focus()`, `.clear-focus()`) @@ -939,6 +1045,7 @@ class ImageFit(enum.Enum): r""" This enum defines how the source image shall fit into an `Image` element. """ + fill = ... r""" Scales and stretches the source image to fit the width and height of the `Image` element. @@ -964,6 +1071,7 @@ class ImageHorizontalAlignment(enum.Enum): r""" This enum specifies the horizontal alignment of the source image. """ + center = ... r""" Aligns the source image at the center of the `Image` element. @@ -982,6 +1090,7 @@ class ImageRendering(enum.Enum): r""" This enum specifies how the source image will be scaled. """ + smooth = ... r""" The image is scaled with a linear interpolation algorithm. @@ -996,6 +1105,7 @@ class ImageTiling(enum.Enum): r""" This enum specifies how the source image will be tiled. """ + none = ... r""" The source image will not be tiled. @@ -1014,6 +1124,7 @@ class ImageVerticalAlignment(enum.Enum): r""" This enum specifies the vertical alignment of the source image. """ + center = ... r""" Aligns the source image at the center of the `Image` element. @@ -1032,6 +1143,7 @@ class InputType(enum.Enum): r""" This enum is used to define the type of the input field. """ + text = ... r""" The default value. This will render all characters normally @@ -1056,6 +1168,7 @@ class LayoutAlignment(enum.Enum): `HorizontalBox`, a `VerticalBox`, a `HorizontalLayout`, or `VerticalLayout`. """ + stretch = ... r""" Use the minimum size of all elements in a layout, distribute remaining space @@ -1096,6 +1209,7 @@ class LineCap(enum.Enum): r""" This enum describes the appearance of the ends of stroked paths. """ + butt = ... r""" The stroke ends with a flat edge that is perpendicular to the path. @@ -1116,6 +1230,7 @@ class MouseCursor(enum.Enum): For details and pictograms see the [MDN Documentation for cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values). Depending on the backend and used OS unidirectional resize cursors may be replaced with bidirectional ones. """ + default = ... r""" The systems default cursor. @@ -1238,6 +1353,7 @@ class OperatingSystemType(enum.Enum): r""" This enum describes the detected operating system types. """ + android = ... r""" This variant includes any version of Android running mobile phones, tablets, as well as embedded Android devices. @@ -1268,6 +1384,7 @@ class Orientation(enum.Enum): r""" Represents the orientation of an element or widget such as the `Slider`. """ + horizontal = ... r""" Element is oriented horizontally. @@ -1283,6 +1400,7 @@ class PathEvent(enum.Enum): PathEvent is a low-level data structure describing the composition of a path. Typically it is generated at compile time from a higher-level description, such as SVG commands. """ + begin = ... r""" The beginning of the path. @@ -1314,6 +1432,7 @@ class PointerEventButton(enum.Enum): This enum describes the different types of buttons for a pointer event, typically on a mouse or a pencil. """ + other = ... r""" A button that is none of left, right, middle, back or forward. For example, @@ -1345,6 +1464,7 @@ class PointerEventKind(enum.Enum): r""" The enum reports what happened to the `PointerEventButton` in the event """ + cancel = ... r""" The action was cancelled. @@ -1382,6 +1502,7 @@ class ScrollBarPolicy(enum.Enum): r""" This enum describes the scrollbar visibility """ + asneeded = ... r""" Scrollbar will be visible only when needed @@ -1401,6 +1522,7 @@ class SortOrder(enum.Enum): This enum represents the different values of the `sort-order` property. It's used to sort a `StandardTableView` by a column. """ + unsorted = ... r""" The column is unsorted. @@ -1421,6 +1543,7 @@ class StandardButtonKind(enum.Enum): of these `StandardButton`s depends on the environment (OS, UI environment, etc.) the application runs in. """ + ok = ... r""" A "OK" button that accepts a `Dialog`, closing it when clicked. @@ -1472,6 +1595,7 @@ class TextHorizontalAlignment(enum.Enum): r""" This enum describes the different types of alignment of text along the horizontal axis of a `Text` element. """ + left = ... r""" The text will be aligned with the left edge of the containing box. @@ -1490,6 +1614,7 @@ class TextOverflow(enum.Enum): r""" This enum describes the how the text appear if it is too wide to fit in the `Text` width. """ + clip = ... r""" The text will simply be clipped. @@ -1504,6 +1629,7 @@ class TextStrokeStyle(enum.Enum): r""" This enum describes the positioning of a text stroke relative to the border of the glyphs in a `Text`. """ + outside = ... r""" The inside edge of the stroke is at the outer edge of the text. @@ -1518,6 +1644,7 @@ class TextVerticalAlignment(enum.Enum): r""" This enum describes the different types of alignment of text along the vertical axis of a `Text` element. """ + top = ... r""" The text will be aligned to the top of the containing box. @@ -1536,6 +1663,7 @@ class TextWrap(enum.Enum): r""" This enum describes the how the text wrap if it is too wide to fit in the `Text` width. """ + nowrap = ... r""" The text won't wrap, but instead will overflow. @@ -1553,9 +1681,10 @@ class TextWrap(enum.Enum): class TimerMode(enum.Enum): r""" The TimerMode specifies what should happen after the timer fired. - + Used by the `Timer.start()` function. """ + SingleShot = ... r""" A SingleShot timer is fired only once. @@ -1578,12 +1707,7 @@ class ValueType(enum.Enum): Enumeration = ... def init_translations(translations: typing.Any) -> None: ... - def invoke_from_event_loop(callable: typing.Any) -> None: ... - def quit_event_loop() -> None: ... - def run_event_loop() -> None: ... - def set_xdg_app_id(app_id: builtins.str) -> None: ... - diff --git a/api/python/slint/stub-gen/main.rs b/api/python/slint/stub-gen/main.rs index b00ce132e4f..61b90e6ad8c 100644 --- a/api/python/slint/stub-gen/main.rs +++ b/api/python/slint/stub-gen/main.rs @@ -1,3 +1,6 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + use pyo3_stub_gen::Result; fn main() -> Result<()> { diff --git a/api/python/slint/tests/codegen/examples/counter/counter.py b/api/python/slint/tests/codegen/examples/counter/counter.py index 0614ac6d045..e98f7e62fb2 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.py +++ b/api/python/slint/tests/codegen/examples/counter/counter.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # Generated by slint.codegen from counter.slint from __future__ import annotations @@ -10,11 +13,12 @@ import slint -__all__ = ['CounterWindow'] +__all__ = ["CounterWindow"] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = 'counter.slint' +_SLINT_RESOURCE = "counter.slint" + def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -35,6 +39,7 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) + _module = _load() CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/counter.pyi b/api/python/slint/tests/codegen/examples/counter/counter.pyi index cd609c18c1b..ec9e1bad2d6 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.pyi +++ b/api/python/slint/tests/codegen/examples/counter/counter.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import enum @@ -6,11 +9,9 @@ from typing import Any, Callable import slint -__all__ = ['CounterWindow'] +__all__ = ["CounterWindow"] class CounterWindow(slint.Component): - def __init__(self, **kwargs: Any) -> None: - ... + def __init__(self, **kwargs: Any) -> None: ... counter: int request_increase: Callable[[], None] - diff --git a/api/python/slint/tests/test_enums.py b/api/python/slint/tests/test_enums.py index d3e57320616..4e7b86a68b9 100644 --- a/api/python/slint/tests/test_enums.py +++ b/api/python/slint/tests/test_enums.py @@ -35,7 +35,9 @@ def generated_module(tmp_path: Path) -> Any: module_path = output_dir / "test_load_file_source.py" assert module_path.exists() - spec = importlib.util.spec_from_file_location("generated_test_load_file", module_path) + spec = importlib.util.spec_from_file_location( + "generated_test_load_file", module_path + ) assert spec and spec.loader sys.modules.pop(spec.name, None) diff --git a/api/python/slint/tests/test_load_file_source.py b/api/python/slint/tests/test_load_file_source.py index 70fe826f493..121bb3f9251 100644 --- a/api/python/slint/tests/test_load_file_source.py +++ b/api/python/slint/tests/test_load_file_source.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # Generated by slint.codegen from test-load-file-source.slint from __future__ import annotations @@ -10,11 +13,50 @@ import slint -__all__ = ['Diag', 'App', 'Secret_Struct', 'MyData', 'ImageFit', 'TextHorizontalAlignment', 'SortOrder', 'PointerEventKind', 'ImageRendering', 'ImageTiling', 'OperatingSystemType', 'InputType', 'Orientation', 'TextWrap', 'ScrollBarPolicy', 'ImageVerticalAlignment', 'PointerEventButton', 'TextOverflow', 'LayoutAlignment', 'StandardButtonKind', 'AccessibleRole', 'EventResult', 'MouseCursor', 'AnimationDirection', 'TextVerticalAlignment', 'TextStrokeStyle', 'LineCap', 'ImageHorizontalAlignment', 'FocusReason', 'FillRule', 'ColorScheme', 'PathEvent', 'TestEnum', 'DialogButtonRole', 'PopupClosePolicy', 'MyDiag', 'Public_Struct'] +__all__ = [ + "Diag", + "App", + "Secret_Struct", + "MyData", + "ImageFit", + "TextHorizontalAlignment", + "SortOrder", + "PointerEventKind", + "ImageRendering", + "ImageTiling", + "OperatingSystemType", + "InputType", + "Orientation", + "TextWrap", + "ScrollBarPolicy", + "ImageVerticalAlignment", + "PointerEventButton", + "TextOverflow", + "LayoutAlignment", + "StandardButtonKind", + "AccessibleRole", + "EventResult", + "MouseCursor", + "AnimationDirection", + "TextVerticalAlignment", + "TextStrokeStyle", + "LineCap", + "ImageHorizontalAlignment", + "FocusReason", + "FillRule", + "ColorScheme", + "PathEvent", + "TestEnum", + "DialogButtonRole", + "PopupClosePolicy", + "MyDiag", + "Public_Struct", +] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = 'test-load-file-source.slint' +_SLINT_RESOURCE = "test-load-file-source.slint" + def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -35,6 +77,7 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) + _module = _load() Diag = _module.Diag diff --git a/api/python/slint/tests/test_load_file_source.pyi b/api/python/slint/tests/test_load_file_source.pyi index 0381742bdbf..bb9296df7d7 100644 --- a/api/python/slint/tests/test_load_file_source.pyi +++ b/api/python/slint/tests/test_load_file_source.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import enum @@ -6,253 +9,288 @@ from typing import Any, Callable import slint -__all__ = ['Diag', 'App', 'Secret_Struct', 'MyData', 'ImageFit', 'TextHorizontalAlignment', 'SortOrder', 'PointerEventKind', 'ImageRendering', 'ImageTiling', 'OperatingSystemType', 'InputType', 'Orientation', 'TextWrap', 'ScrollBarPolicy', 'ImageVerticalAlignment', 'PointerEventButton', 'TextOverflow', 'LayoutAlignment', 'StandardButtonKind', 'AccessibleRole', 'EventResult', 'MouseCursor', 'AnimationDirection', 'TextVerticalAlignment', 'TextStrokeStyle', 'LineCap', 'ImageHorizontalAlignment', 'FocusReason', 'FillRule', 'ColorScheme', 'PathEvent', 'TestEnum', 'DialogButtonRole', 'PopupClosePolicy', 'MyDiag', 'Public_Struct'] +__all__ = [ + "Diag", + "App", + "Secret_Struct", + "MyData", + "ImageFit", + "TextHorizontalAlignment", + "SortOrder", + "PointerEventKind", + "ImageRendering", + "ImageTiling", + "OperatingSystemType", + "InputType", + "Orientation", + "TextWrap", + "ScrollBarPolicy", + "ImageVerticalAlignment", + "PointerEventButton", + "TextOverflow", + "LayoutAlignment", + "StandardButtonKind", + "AccessibleRole", + "EventResult", + "MouseCursor", + "AnimationDirection", + "TextVerticalAlignment", + "TextStrokeStyle", + "LineCap", + "ImageHorizontalAlignment", + "FocusReason", + "FillRule", + "ColorScheme", + "PathEvent", + "TestEnum", + "DialogButtonRole", + "PopupClosePolicy", + "MyDiag", + "Public_Struct", +] class Secret_Struct: - def __init__(self, **kwargs: Any) -> None: - ... + def __init__(self, **kwargs: Any) -> None: ... balance: float class MyData: - def __init__(self, **kwargs: Any) -> None: - ... + def __init__(self, **kwargs: Any) -> None: ... age: float name: str class ImageFit(enum.Enum): - fill = 'fill' - contain = 'contain' - cover = 'cover' - preserve = 'preserve' + fill = "fill" + contain = "contain" + cover = "cover" + preserve = "preserve" class TextHorizontalAlignment(enum.Enum): - left = 'left' - center = 'center' - right = 'right' + left = "left" + center = "center" + right = "right" class SortOrder(enum.Enum): - unsorted = 'unsorted' - ascending = 'ascending' - descending = 'descending' + unsorted = "unsorted" + ascending = "ascending" + descending = "descending" class PointerEventKind(enum.Enum): - cancel = 'cancel' - down = 'down' - up = 'up' - move = 'move' + cancel = "cancel" + down = "down" + up = "up" + move = "move" class ImageRendering(enum.Enum): - smooth = 'smooth' - pixelated = 'pixelated' + smooth = "smooth" + pixelated = "pixelated" class ImageTiling(enum.Enum): - none = 'none' - repeat = 'repeat' - round = 'round' + none = "none" + repeat = "repeat" + round = "round" class OperatingSystemType(enum.Enum): - android = 'android' - ios = 'ios' - macos = 'macos' - linux = 'linux' - windows = 'windows' - other = 'other' + android = "android" + ios = "ios" + macos = "macos" + linux = "linux" + windows = "windows" + other = "other" class InputType(enum.Enum): - text = 'text' - password = 'password' - number = 'number' - decimal = 'decimal' + text = "text" + password = "password" + number = "number" + decimal = "decimal" class Orientation(enum.Enum): - horizontal = 'horizontal' - vertical = 'vertical' + horizontal = "horizontal" + vertical = "vertical" class TextWrap(enum.Enum): - nowrap = 'nowrap' - wordwrap = 'wordwrap' - charwrap = 'charwrap' + nowrap = "nowrap" + wordwrap = "wordwrap" + charwrap = "charwrap" class ScrollBarPolicy(enum.Enum): - asneeded = 'asneeded' - alwaysoff = 'alwaysoff' - alwayson = 'alwayson' + asneeded = "asneeded" + alwaysoff = "alwaysoff" + alwayson = "alwayson" class ImageVerticalAlignment(enum.Enum): - center = 'center' - top = 'top' - bottom = 'bottom' + center = "center" + top = "top" + bottom = "bottom" class PointerEventButton(enum.Enum): - other = 'other' - left = 'left' - right = 'right' - middle = 'middle' - back = 'back' - forward = 'forward' + other = "other" + left = "left" + right = "right" + middle = "middle" + back = "back" + forward = "forward" class TextOverflow(enum.Enum): - clip = 'clip' - elide = 'elide' + clip = "clip" + elide = "elide" class LayoutAlignment(enum.Enum): - stretch = 'stretch' - center = 'center' - start = 'start' - end = 'end' - spacebetween = 'spacebetween' - spacearound = 'spacearound' - spaceevenly = 'spaceevenly' + stretch = "stretch" + center = "center" + start = "start" + end = "end" + spacebetween = "spacebetween" + spacearound = "spacearound" + spaceevenly = "spaceevenly" class StandardButtonKind(enum.Enum): - ok = 'ok' - cancel = 'cancel' - apply = 'apply' - close = 'close' - reset = 'reset' - help = 'help' - yes = 'yes' - no = 'no' - abort = 'abort' - retry = 'retry' - ignore = 'ignore' + ok = "ok" + cancel = "cancel" + apply = "apply" + close = "close" + reset = "reset" + help = "help" + yes = "yes" + no = "no" + abort = "abort" + retry = "retry" + ignore = "ignore" class AccessibleRole(enum.Enum): - none = 'none' - button = 'button' - checkbox = 'checkbox' - combobox = 'combobox' - groupbox = 'groupbox' - image = 'image' - list = 'list' - slider = 'slider' - spinbox = 'spinbox' - tab = 'tab' - tablist = 'tablist' - tabpanel = 'tabpanel' - text = 'text' - table = 'table' - tree = 'tree' - progressindicator = 'progressindicator' - textinput = 'textinput' - switch = 'switch' - listitem = 'listitem' + none = "none" + button = "button" + checkbox = "checkbox" + combobox = "combobox" + groupbox = "groupbox" + image = "image" + list = "list" + slider = "slider" + spinbox = "spinbox" + tab = "tab" + tablist = "tablist" + tabpanel = "tabpanel" + text = "text" + table = "table" + tree = "tree" + progressindicator = "progressindicator" + textinput = "textinput" + switch = "switch" + listitem = "listitem" class EventResult(enum.Enum): - reject = 'reject' - accept = 'accept' + reject = "reject" + accept = "accept" class MouseCursor(enum.Enum): - default = 'default' - none = 'none' - help = 'help' - pointer = 'pointer' - progress = 'progress' - wait = 'wait' - crosshair = 'crosshair' - text = 'text' - alias = 'alias' - copy = 'copy' - move = 'move' - nodrop = 'nodrop' - notallowed = 'notallowed' - grab = 'grab' - grabbing = 'grabbing' - colresize = 'colresize' - rowresize = 'rowresize' - nresize = 'nresize' - eresize = 'eresize' - sresize = 'sresize' - wresize = 'wresize' - neresize = 'neresize' - nwresize = 'nwresize' - seresize = 'seresize' - swresize = 'swresize' - ewresize = 'ewresize' - nsresize = 'nsresize' - neswresize = 'neswresize' - nwseresize = 'nwseresize' + default = "default" + none = "none" + help = "help" + pointer = "pointer" + progress = "progress" + wait = "wait" + crosshair = "crosshair" + text = "text" + alias = "alias" + copy = "copy" + move = "move" + nodrop = "nodrop" + notallowed = "notallowed" + grab = "grab" + grabbing = "grabbing" + colresize = "colresize" + rowresize = "rowresize" + nresize = "nresize" + eresize = "eresize" + sresize = "sresize" + wresize = "wresize" + neresize = "neresize" + nwresize = "nwresize" + seresize = "seresize" + swresize = "swresize" + ewresize = "ewresize" + nsresize = "nsresize" + neswresize = "neswresize" + nwseresize = "nwseresize" class AnimationDirection(enum.Enum): - normal = 'normal' - reverse = 'reverse' - alternate = 'alternate' - alternatereverse = 'alternatereverse' + normal = "normal" + reverse = "reverse" + alternate = "alternate" + alternatereverse = "alternatereverse" class TextVerticalAlignment(enum.Enum): - top = 'top' - center = 'center' - bottom = 'bottom' + top = "top" + center = "center" + bottom = "bottom" class TextStrokeStyle(enum.Enum): - outside = 'outside' - center = 'center' + outside = "outside" + center = "center" class LineCap(enum.Enum): - butt = 'butt' - round = 'round' - square = 'square' + butt = "butt" + round = "round" + square = "square" class ImageHorizontalAlignment(enum.Enum): - center = 'center' - left = 'left' - right = 'right' + center = "center" + left = "left" + right = "right" class FocusReason(enum.Enum): - programmatic = 'programmatic' - tabnavigation = 'tabnavigation' - pointerclick = 'pointerclick' - popupactivation = 'popupactivation' - windowactivation = 'windowactivation' + programmatic = "programmatic" + tabnavigation = "tabnavigation" + pointerclick = "pointerclick" + popupactivation = "popupactivation" + windowactivation = "windowactivation" class FillRule(enum.Enum): - nonzero = 'nonzero' - evenodd = 'evenodd' + nonzero = "nonzero" + evenodd = "evenodd" class ColorScheme(enum.Enum): - unknown = 'unknown' - dark = 'dark' - light = 'light' + unknown = "unknown" + dark = "dark" + light = "light" class PathEvent(enum.Enum): - begin = 'begin' - line = 'line' - quadratic = 'quadratic' - cubic = 'cubic' - endopen = 'endopen' - endclosed = 'endclosed' + begin = "begin" + line = "line" + quadratic = "quadratic" + cubic = "cubic" + endopen = "endopen" + endclosed = "endclosed" class TestEnum(enum.Enum): - Variant1 = 'Variant1' - Variant2 = 'Variant2' + Variant1 = "Variant1" + Variant2 = "Variant2" class DialogButtonRole(enum.Enum): - none = 'none' - accept = 'accept' - reject = 'reject' - apply = 'apply' - reset = 'reset' - help = 'help' - action = 'action' + none = "none" + accept = "accept" + reject = "reject" + apply = "apply" + reset = "reset" + help = "help" + action = "action" class PopupClosePolicy(enum.Enum): - closeonclick = 'closeonclick' - closeonclickoutside = 'closeonclickoutside' - noautoclose = 'noautoclose' + closeonclick = "closeonclick" + closeonclickoutside = "closeonclickoutside" + noautoclose = "noautoclose" class Diag(slint.Component): - def __init__(self, **kwargs: Any) -> None: - ... + def __init__(self, **kwargs: Any) -> None: ... class MyGlobal: global_prop: str global_callback: Callable[[str], str] minus_one: Callable[[int], None] + class SecondGlobal: second: str class App(slint.Component): - def __init__(self, **kwargs: Any) -> None: - ... + def __init__(self, **kwargs: Any) -> None: ... builtin_enum: Any enum_property: Any hello: str @@ -270,10 +308,10 @@ class App(slint.Component): global_prop: str global_callback: Callable[[str], str] minus_one: Callable[[int], None] + class SecondGlobal: second: str MyDiag = Diag Public_Struct = Secret_Struct - From a85b3c5afd38686c4f78ea2594fa66936e45f6f5 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 10:51:56 +0800 Subject: [PATCH 33/52] refactor(python): drop enums & structs export --- api/python/slint/enums.rs | 88 -- api/python/slint/interpreter.rs | 3 +- api/python/slint/lib.rs | 5 - api/python/slint/models.rs | 1 - api/python/slint/slint/api.py | 12 + api/python/slint/slint/core.pyi | 1283 +---------------- api/python/slint/structs.rs | 251 ---- .../codegen/examples/counter/counter.pyi | 15 +- 8 files changed, 88 insertions(+), 1570 deletions(-) delete mode 100644 api/python/slint/enums.rs delete mode 100644 api/python/slint/structs.rs diff --git a/api/python/slint/enums.rs b/api/python/slint/enums.rs deleted file mode 100644 index af2bf685897..00000000000 --- a/api/python/slint/enums.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright © SixtyFPS GmbH -// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -use std::collections::HashMap; - -use pyo3::prelude::*; -use pyo3::sync::PyOnceLock; -#[cfg(feature = "stubgen")] -use pyo3_stub_gen::derive::gen_stub_pyclass_enum; - -static BUILTIN_ENUM_CLASSES: PyOnceLock>> = PyOnceLock::new(); - -macro_rules! generate_enum_support { - ($( - $(#[$enum_attr:meta])* - enum $Name:ident { - $( - $(#[$value_attr:meta])* - $Value:ident, - )* - } - )*) => { - #[cfg(feature = "stubgen")] - pub(super) mod stub_enums { - use super::*; - - $( - #[gen_stub_pyclass_enum] - #[pyclass(module = "slint.core", rename_all = "lowercase")] - #[allow(non_camel_case_types)] - $(#[$enum_attr])* - pub enum $Name { - $( - $(#[$value_attr])* - $Value, - )* - } - )* - } - - fn register_built_in_enums( - py: Python<'_>, - module: &Bound<'_, PyModule>, - enum_base: &Bound<'_, PyAny>, - enum_classes: &mut HashMap>, - ) -> PyResult<()> { - $( - { - let name = stringify!($Name); - let variants = vec![ - $( - { - let value = i_slint_core::items::$Name::$Value.to_string(); - (stringify!($Value).to_ascii_lowercase(), value) - }, - )* - ]; - - let cls = enum_base.call((name, variants), None)?; - let cls_owned = cls.unbind(); - module.add(name, cls_owned.bind(py))?; - enum_classes.insert(name.to_string(), cls_owned); - } - )* - Ok(()) - } - }; -} - -i_slint_common::for_each_enums!(generate_enum_support); - -pub fn register_enums(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { - let enum_base = crate::value::enum_class(py).into_bound(py); - let mut enum_classes: HashMap> = HashMap::new(); - - register_built_in_enums(py, module, &enum_base, &mut enum_classes)?; - - let _ = BUILTIN_ENUM_CLASSES.set(py, enum_classes); - - Ok(()) -} - -pub fn built_in_enum_classes(py: Python<'_>) -> HashMap> { - BUILTIN_ENUM_CLASSES - .get(py) - .map(|map| map.iter().map(|(name, class)| (name.clone(), class.clone_ref(py))).collect()) - .unwrap_or_default() -} diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index 8cca1391f4d..2f98708f78e 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -242,6 +242,7 @@ impl ComponentDefinition { self.definition.name() } + #[gen_stub(override_return_type(type_repr = "typing.Dict[str, typing.Any]", imports = ("typing",)))] #[getter] fn properties(&self) -> IndexMap { self.definition @@ -298,6 +299,7 @@ impl ComponentDefinition { .collect() } + #[gen_stub(override_return_type(type_repr = "typing.Dict[str, typing.Any]", imports = ("typing",)))] fn global_properties(&self, name: &str) -> IndexMap { self.definition .global_properties_and_callbacks(name) @@ -402,7 +404,6 @@ impl ComponentDefinition { } } -#[gen_stub_pyclass_enum] #[pyclass(name = "ValueType", eq, eq_int)] #[derive(PartialEq)] pub enum PyValueType { diff --git a/api/python/slint/lib.rs b/api/python/slint/lib.rs index ffcba98d741..8a662559ede 100644 --- a/api/python/slint/lib.rs +++ b/api/python/slint/lib.rs @@ -13,10 +13,8 @@ use interpreter::{ }; mod async_adapter; mod brush; -mod enums; mod errors; mod models; -mod structs; mod timer; mod value; use i_slint_core::translations::Translator; @@ -193,9 +191,6 @@ fn slint_core(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(invoke_from_event_loop, m)?)?; m.add_function(wrap_pyfunction!(init_translations, m)?)?; - enums::register_enums(_py, m)?; - structs::register_structs(_py, m)?; - Ok(()) } diff --git a/api/python/slint/models.rs b/api/python/slint/models.rs index e7a531e2ef5..e379e3d5e02 100644 --- a/api/python/slint/models.rs +++ b/api/python/slint/models.rs @@ -60,7 +60,6 @@ impl PyModelBase { #[gen_stub_pymethods] #[pymethods] impl PyModelBase { - #[gen_stub(skip)] #[new] fn new() -> Self { Self { diff --git a/api/python/slint/slint/api.py b/api/python/slint/slint/api.py index 6ff01e21e12..85226c51894 100644 --- a/api/python/slint/slint/api.py +++ b/api/python/slint/slint/api.py @@ -272,7 +272,19 @@ def global_getter(self: Component) -> Any: def _build_struct(name: str, struct_prototype: PyStruct) -> type: + field_names = {field_name for field_name, _ in struct_prototype} + def new_struct(cls: Any, *args: Any, **kwargs: Any) -> PyStruct: + if args: + raise TypeError(f"{name}() accepts keyword arguments only") + + unexpected = set(kwargs) - field_names + if unexpected: + formatted = ", ".join(sorted(unexpected)) + raise TypeError( + f"{name}() got unexpected keyword argument(s): {formatted}" + ) + inst = copy.copy(struct_prototype) for prop, val in kwargs.items(): diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index ed1d501d739..e7399b07ca1 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -1,6 +1,3 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - # This file is automatically generated by pyo3_stub_gen # ruff: noqa: E501, F401 @@ -11,10 +8,11 @@ import enum import os import pathlib import typing +from typing import Self @typing.final class AsyncAdapter: - def __new__(cls, fd: builtins.int) -> AsyncAdapter: ... + def __new__(cls, fd: builtins.int) -> Self: ... def wait_for_readable(self, callback: typing.Any) -> None: ... def wait_for_writable(self, callback: typing.Any) -> None: ... @@ -23,9 +21,9 @@ class Brush: r""" A brush is a data structure that is used to describe how a shape, such as a rectangle, path or even text, shall be filled. A brush can also be applied to the outline of a shape, that means the fill of the outline itself. - + Brushes can only be constructed from solid colors. - + **Note:** In future, we plan to reduce this constraint and allow for declaring graidient brushes programmatically. """ @property @@ -33,7 +31,7 @@ class Brush: r""" The brush's color. """ - def __new__(cls, maybe_value: typing.Optional[Color] = None) -> Brush: ... + def __new__(cls, maybe_value: typing.Optional[Color] = None) -> Self: ... def is_transparent(self) -> builtins.bool: r""" Returns true if this brush contains a fully transparent color (alpha value is zero). @@ -57,9 +55,9 @@ class Brush: def transparentize(self, amount: builtins.float) -> Brush: r""" Returns a new version of this brush with the opacity decreased by `factor`. - + The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. - + See also `Color.transparentize`. """ def with_alpha(self, alpha: builtins.float) -> Brush: @@ -90,7 +88,7 @@ class Color: r""" A Color object represents a color in the RGB color space with an alpha. Each color channel and the alpha is represented as an 8-bit integer. The alpha channel is 0 for fully transparent and 255 for fully opaque. - + Construct colors from a CSS color string, or by specifying the red, green, blue, and (optional) alpha channels in a dict. """ @property @@ -113,12 +111,7 @@ class Color: r""" The alpha channel. """ - def __new__( - cls, - maybe_value: typing.Optional[ - builtins.str | PyColorInput.RgbaColor | PyColorInput.RgbColor - ] = None, - ) -> Color: ... + def __new__(cls, maybe_value: typing.Optional[builtins.str | PyColorInput.RgbaColor | PyColorInput.RgbColor] = None) -> Self: ... def brighter(self, factor: builtins.float) -> Color: r""" Returns a new color that is brighter than this color by the given factor. @@ -130,7 +123,7 @@ class Color: def transparentize(self, factor: builtins.float) -> Color: r""" Returns a new version of this color with the opacity decreased by `factor`. - + The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. """ def mix(self, other: Color, factor: builtins.float) -> Color: @@ -153,11 +146,7 @@ class CompilationResult: @property def diagnostics(self) -> builtins.list[PyDiagnostic]: ... @property - def structs_and_enums( - self, - ) -> tuple[ - builtins.dict[builtins.str, typing.Any], builtins.dict[builtins.str, typing.Any] - ]: ... + def structs_and_enums(self) -> tuple[builtins.dict[builtins.str, typing.Any], builtins.dict[builtins.str, typing.Any]]: ... @property def named_exports(self) -> builtins.list[tuple[builtins.str, builtins.str]]: ... @property @@ -177,24 +166,18 @@ class Compiler: @property def library_paths(self) -> builtins.dict[builtins.str, pathlib.Path]: ... @library_paths.setter - def library_paths( - self, value: builtins.dict[builtins.str, pathlib.Path] - ) -> None: ... - def __new__(cls) -> Compiler: ... + def library_paths(self, value: builtins.dict[builtins.str, pathlib.Path]) -> None: ... + def __new__(cls) -> Self: ... def set_translation_domain(self, domain: builtins.str) -> None: ... - def build_from_path( - self, path: builtins.str | os.PathLike | pathlib.Path - ) -> CompilationResult: ... - def build_from_source( - self, source_code: builtins.str, path: builtins.str | os.PathLike | pathlib.Path - ) -> CompilationResult: ... + def build_from_path(self, path: builtins.str | os.PathLike | pathlib.Path) -> CompilationResult: ... + def build_from_source(self, source_code: builtins.str, path: builtins.str | os.PathLike | pathlib.Path) -> CompilationResult: ... @typing.final class ComponentDefinition: @property def name(self) -> builtins.str: ... @property - def properties(self) -> builtins.dict[builtins.str, ValueType]: ... + def properties(self) -> typing.Dict[str, typing.Any]: ... @property def callbacks(self) -> builtins.list[builtins.str]: ... @property @@ -204,24 +187,14 @@ class ComponentDefinition: def property_infos(self) -> builtins.list[PropertyInfo]: ... def callback_infos(self) -> builtins.list[CallbackInfo]: ... def function_infos(self) -> builtins.list[FunctionInfo]: ... - def global_properties( - self, name: builtins.str - ) -> builtins.dict[builtins.str, ValueType]: ... + def global_properties(self, name: builtins.str) -> typing.Dict[str, typing.Any]: ... def global_callbacks(self, name: builtins.str) -> builtins.list[builtins.str]: ... def global_functions(self, name: builtins.str) -> builtins.list[builtins.str]: ... - def global_property_infos( - self, global_name: builtins.str - ) -> typing.Optional[builtins.list[PropertyInfo]]: ... - def global_callback_infos( - self, global_name: builtins.str - ) -> typing.Optional[builtins.list[CallbackInfo]]: ... - def global_function_infos( - self, global_name: builtins.str - ) -> typing.Optional[builtins.list[FunctionInfo]]: ... + def global_property_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[PropertyInfo]]: ... + def global_callback_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[CallbackInfo]]: ... + def global_function_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[FunctionInfo]]: ... def callback_returns_void(self, callback_name: builtins.str) -> builtins.bool: ... - def global_callback_returns_void( - self, global_name: builtins.str, callback_name: builtins.str - ) -> builtins.bool: ... + def global_callback_returns_void(self, global_name: builtins.str, callback_name: builtins.str) -> builtins.bool: ... def create(self) -> ComponentInstance: ... @typing.final @@ -230,78 +203,18 @@ class ComponentInstance: def definition(self) -> ComponentDefinition: ... def get_property(self, name: builtins.str) -> typing.Any: ... def set_property(self, name: builtins.str, value: typing.Any) -> None: ... - def get_global_property( - self, global_name: builtins.str, prop_name: builtins.str - ) -> typing.Any: ... - def set_global_property( - self, global_name: builtins.str, prop_name: builtins.str, value: typing.Any - ) -> None: ... + def get_global_property(self, global_name: builtins.str, prop_name: builtins.str) -> typing.Any: ... + def set_global_property(self, global_name: builtins.str, prop_name: builtins.str, value: typing.Any) -> None: ... def invoke(self, callback_name: builtins.str, *args: typing.Any) -> typing.Any: ... - def invoke_global( - self, global_name: builtins.str, callback_name: builtins.str, *args: typing.Any - ) -> typing.Any: ... + def invoke_global(self, global_name: builtins.str, callback_name: builtins.str, *args: typing.Any) -> typing.Any: ... def set_callback(self, name: builtins.str, callable: typing.Any) -> None: ... - def set_global_callback( - self, - global_name: builtins.str, - callback_name: builtins.str, - callable: typing.Any, - ) -> None: ... + def set_global_callback(self, global_name: builtins.str, callback_name: builtins.str, callable: typing.Any) -> None: ... def show(self) -> None: ... def hide(self) -> None: ... def on_close_requested(self, callable: typing.Any) -> None: ... def dispatch_close_requested_event(self) -> None: ... def __clear__(self) -> None: ... -@typing.final -class DropEvent: - @property - def mime_type(self) -> typing.Any: ... - @mime_type.setter - def mime_type(self, value: typing.Any) -> None: ... - @property - def data(self) -> typing.Any: ... - @data.setter - def data(self, value: typing.Any) -> None: ... - @property - def position(self) -> typing.Any: ... - @position.setter - def position(self, value: typing.Any) -> None: ... - def __init__( - self, - *, - mime_type: typing.Any = ..., - data: typing.Any = ..., - position: typing.Any = ..., - ) -> None: ... - -@typing.final -class FontMetrics: - @property - def ascent(self) -> typing.Any: ... - @ascent.setter - def ascent(self, value: typing.Any) -> None: ... - @property - def descent(self) -> typing.Any: ... - @descent.setter - def descent(self, value: typing.Any) -> None: ... - @property - def x_height(self) -> typing.Any: ... - @x_height.setter - def x_height(self, value: typing.Any) -> None: ... - @property - def cap_height(self) -> typing.Any: ... - @cap_height.setter - def cap_height(self, value: typing.Any) -> None: ... - def __init__( - self, - *, - ascent: typing.Any = ..., - descent: typing.Any = ..., - x_height: typing.Any = ..., - cap_height: typing.Any = ..., - ) -> None: ... - @typing.final class FunctionInfo: @property @@ -337,7 +250,7 @@ class Image: r""" The path of the image if it was loaded from disk, or None. """ - def __new__(cls) -> Image: ... + def __new__(cls) -> Self: ... @staticmethod def load_from_path(path: builtins.str | os.PathLike | pathlib.Path) -> Image: r""" @@ -353,187 +266,47 @@ class Image: r""" Creates a new image from an array-like object that implements the [Buffer Protocol](https://docs.python.org/3/c-api/buffer.html). Use this function to import images created by third-party modules such as matplotlib or Pillow. - + The array must satisfy certain contraints to represent an image: - + - The buffer's format needs to be `B` (unsigned char) - The shape must be a tuple of (height, width, bytes-per-pixel) - If a stride is defined, the row stride must be equal to width * bytes-per-pixel, and the column stride must equal the bytes-per-pixel. - A value of 3 for bytes-per-pixel is interpreted as RGB image, a value of 4 means RGBA. - + The image is created by performing a deep copy of the array's data. Subsequent changes to the buffer are not automatically reflected in a previously created Image. - + Example of importing a matplot figure into an image: ```python import slint import matplotlib - + from matplotlib.backends.backend_agg import FigureCanvasAgg from matplotlib.figure import Figure - + fig = Figure(figsize=(5, 4), dpi=100) canvas = FigureCanvasAgg(fig) ax = fig.add_subplot() ax.plot([1, 2, 3]) canvas.draw() - + buffer = canvas.buffer_rgba() img = slint.Image.load_from_array(buffer) ``` - + Example of loading an image with Pillow: ```python import slint from PIL import Image import numpy as np - + pil_img = Image.open("hello.jpeg") array = np.array(pil_img) img = slint.Image.load_from_array(array) ``` """ -@typing.final -class KeyEvent: - @property - def text(self) -> typing.Any: ... - @text.setter - def text(self, value: typing.Any) -> None: ... - @property - def modifiers(self) -> typing.Any: ... - @modifiers.setter - def modifiers(self, value: typing.Any) -> None: ... - @property - def repeat(self) -> typing.Any: ... - @repeat.setter - def repeat(self, value: typing.Any) -> None: ... - def __init__( - self, - *, - text: typing.Any = ..., - modifiers: typing.Any = ..., - repeat: typing.Any = ..., - ) -> None: ... - -@typing.final -class KeyboardModifiers: - @property - def alt(self) -> typing.Any: ... - @alt.setter - def alt(self, value: typing.Any) -> None: ... - @property - def control(self) -> typing.Any: ... - @control.setter - def control(self, value: typing.Any) -> None: ... - @property - def shift(self) -> typing.Any: ... - @shift.setter - def shift(self, value: typing.Any) -> None: ... - @property - def meta(self) -> typing.Any: ... - @meta.setter - def meta(self, value: typing.Any) -> None: ... - def __init__( - self, - *, - alt: typing.Any = ..., - control: typing.Any = ..., - shift: typing.Any = ..., - meta: typing.Any = ..., - ) -> None: ... - -@typing.final -class MenuEntry: - @property - def title(self) -> typing.Any: ... - @title.setter - def title(self, value: typing.Any) -> None: ... - @property - def icon(self) -> typing.Any: ... - @icon.setter - def icon(self, value: typing.Any) -> None: ... - @property - def id(self) -> typing.Any: ... - @id.setter - def id(self, value: typing.Any) -> None: ... - @property - def enabled(self) -> typing.Any: ... - @enabled.setter - def enabled(self, value: typing.Any) -> None: ... - @property - def checkable(self) -> typing.Any: ... - @checkable.setter - def checkable(self, value: typing.Any) -> None: ... - @property - def checked(self) -> typing.Any: ... - @checked.setter - def checked(self, value: typing.Any) -> None: ... - @property - def has_sub_menu(self) -> typing.Any: ... - @has_sub_menu.setter - def has_sub_menu(self, value: typing.Any) -> None: ... - @property - def is_separator(self) -> typing.Any: ... - @is_separator.setter - def is_separator(self, value: typing.Any) -> None: ... - def __init__( - self, - *, - title: typing.Any = ..., - icon: typing.Any = ..., - id: typing.Any = ..., - enabled: typing.Any = ..., - checkable: typing.Any = ..., - checked: typing.Any = ..., - has_sub_menu: typing.Any = ..., - is_separator: typing.Any = ..., - ) -> None: ... - -@typing.final -class PointerEvent: - @property - def button(self) -> typing.Any: ... - @button.setter - def button(self, value: typing.Any) -> None: ... - @property - def kind(self) -> typing.Any: ... - @kind.setter - def kind(self, value: typing.Any) -> None: ... - @property - def modifiers(self) -> typing.Any: ... - @modifiers.setter - def modifiers(self, value: typing.Any) -> None: ... - def __init__( - self, - *, - button: typing.Any = ..., - kind: typing.Any = ..., - modifiers: typing.Any = ..., - ) -> None: ... - -@typing.final -class PointerScrollEvent: - @property - def delta_x(self) -> typing.Any: ... - @delta_x.setter - def delta_x(self, value: typing.Any) -> None: ... - @property - def delta_y(self) -> typing.Any: ... - @delta_y.setter - def delta_y(self, value: typing.Any) -> None: ... - @property - def modifiers(self) -> typing.Any: ... - @modifiers.setter - def modifiers(self, value: typing.Any) -> None: ... - def __init__( - self, - *, - delta_x: typing.Any = ..., - delta_y: typing.Any = ..., - modifiers: typing.Any = ..., - ) -> None: ... - @typing.final class PropertyInfo: @property @@ -547,21 +320,21 @@ class PyColorInput: __match_args__ = ("_0",) @property def _0(self) -> builtins.str: ... - def __new__(cls, _0: builtins.str) -> PyColorInput.ColorStr: ... + def __new__(cls, _0: builtins.str) -> Self: ... def __len__(self) -> builtins.int: ... def __getitem__(self, key: builtins.int) -> typing.Any: ... - + class RgbaColor(typing.TypedDict): red: builtins.int green: builtins.int blue: builtins.int alpha: builtins.int - + class RgbColor(typing.TypedDict): red: builtins.int green: builtins.int blue: builtins.int - + ... @typing.final @@ -579,6 +352,7 @@ class PyDiagnostic: def __str__(self) -> builtins.str: ... class PyModelBase(abc.ABC): + def __new__(cls) -> Self: ... def init_self(self, self_ref: typing.Any) -> None: ... def notify_row_added(self, index: builtins.int, count: builtins.int) -> None: r""" @@ -673,89 +447,36 @@ class RgbaColor: def alpha(self, value: builtins.int) -> None: ... @typing.final -class SlintToPyValue: ... - -@typing.final -class StandardListViewItem: - @property - def text(self) -> typing.Any: ... - @text.setter - def text(self, value: typing.Any) -> None: ... - def __init__(self, *, text: typing.Any = ...) -> None: ... - -@typing.final -class StateInfo: - @property - def current_state(self) -> typing.Any: ... - @current_state.setter - def current_state(self, value: typing.Any) -> None: ... - @property - def previous_state(self) -> typing.Any: ... - @previous_state.setter - def previous_state(self, value: typing.Any) -> None: ... - def __init__( - self, *, current_state: typing.Any = ..., previous_state: typing.Any = ... - ) -> None: ... - -@typing.final -class TableColumn: - @property - def title(self) -> typing.Any: ... - @title.setter - def title(self, value: typing.Any) -> None: ... - @property - def min_width(self) -> typing.Any: ... - @min_width.setter - def min_width(self, value: typing.Any) -> None: ... - @property - def horizontal_stretch(self) -> typing.Any: ... - @horizontal_stretch.setter - def horizontal_stretch(self, value: typing.Any) -> None: ... - @property - def sort_order(self) -> typing.Any: ... - @sort_order.setter - def sort_order(self, value: typing.Any) -> None: ... - @property - def width(self) -> typing.Any: ... - @width.setter - def width(self, value: typing.Any) -> None: ... - def __init__( - self, - *, - title: typing.Any = ..., - min_width: typing.Any = ..., - horizontal_stretch: typing.Any = ..., - sort_order: typing.Any = ..., - width: typing.Any = ..., - ) -> None: ... +class SlintToPyValue: + ... @typing.final class Timer: r""" Timer is a handle to the timer system that triggers a callback after a specified period of time. - + Use `Timer.start()` to create a timer that that repeatedly triggers a callback, or `Timer.single_shot()` to trigger a callback only once. - + The timer will automatically stop when garbage collected. You must keep the Timer object around for as long as you want the timer to keep firing. - + ```python class AppWindow(...) def __init__(self): super().__init__() self.my_timer = None - + @slint.callback def button_clicked(self): self.my_timer = slint.Timer() self.my_timer.start(timedelta(seconds=1), self.do_something) - + def do_something(self): pass ``` - + Timers can only be used in the thread that runs the Slint event loop. They don't fire if used in another thread. """ @@ -770,19 +491,17 @@ class Timer: def interval(self, value: datetime.timedelta) -> None: r""" The duration of timer. - + When setting this property and the timer is running (see `Timer.running`), then the time when the callback will be next invoked is re-calculated to be in the specified duration relative to when this property is set. """ - def __new__(cls) -> Timer: ... - def start( - self, mode: TimerMode, interval: datetime.timedelta, callback: typing.Any - ) -> None: + def __new__(cls) -> Self: ... + def start(self, mode: TimerMode, interval: datetime.timedelta, callback: typing.Any) -> None: r""" Starts the timer with the given mode and interval, in order for the callback to called when the timer fires. If the timer has been started previously and not fired yet, then it will be restarted. - + Arguments: * `mode`: The timer mode to apply, i.e. whether to repeatedly fire the timer or just once. * `interval`: The duration from now until when the timer should firethe first time, and subsequently @@ -794,7 +513,7 @@ class Timer: r""" Starts the timer with the duration and the callback to called when the timer fires. It is fired only once and then deleted. - + Arguments: * `duration`: The duration from now until when the timer should fire. * `callback`: The function to call when the time has been reached or exceeded. @@ -808,906 +527,38 @@ class Timer: Restarts the timer. If the timer was previously started by calling `Timer.start()` with a duration and callback, then the time when the callback will be next invoked is re-calculated to be in the specified duration relative to when this function is called. - + Does nothing if the timer was never started. """ -@typing.final -class AccessibleRole(enum.Enum): - r""" - This enum represents the different values for the `accessible-role` property, used to describe the - role of an element in the context of assistive technology such as screen readers. - """ - - none = ... - r""" - The element isn't accessible. - """ - button = ... - r""" - The element is a `Button` or behaves like one. - """ - checkbox = ... - r""" - The element is a `CheckBox` or behaves like one. - """ - combobox = ... - r""" - The element is a `ComboBox` or behaves like one. - """ - groupbox = ... - r""" - The element is a `GroupBox` or behaves like one. - """ - image = ... - r""" - The element is an `Image` or behaves like one. This is automatically applied to `Image` elements. - """ - list = ... - r""" - The element is a `ListView` or behaves like one. - """ - slider = ... - r""" - The element is a `Slider` or behaves like one. - """ - spinbox = ... - r""" - The element is a `SpinBox` or behaves like one. - """ - tab = ... - r""" - The element is a `Tab` or behaves like one. - """ - tablist = ... - r""" - The element is similar to the tab bar in a `TabWidget`. - """ - tabpanel = ... - r""" - The element is a container for tab content. - """ - text = ... - r""" - The role for a `Text` element. This is automatically applied to `Text` elements. - """ - table = ... - r""" - The role for a `TableView` or behaves like one. - """ - tree = ... - r""" - The role for a TreeView or behaves like one. (Not provided yet) - """ - progressindicator = ... - r""" - The element is a `ProgressIndicator` or behaves like one. - """ - textinput = ... - r""" - The role for widget with editable text such as a `LineEdit` or a `TextEdit`. - This is automatically applied to `TextInput` elements. - """ - switch = ... - r""" - The element is a `Switch` or behaves like one. - """ - listitem = ... - r""" - The element is an item in a `ListView`. - """ - -@typing.final -class AnimationDirection(enum.Enum): - r""" - This enum describes the direction of an animation. - """ - - normal = ... - r""" - The ["normal" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#normal). - """ - reverse = ... - r""" - The ["reverse" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#reverse). - """ - alternate = ... - r""" - The ["alternate" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#alternate). - """ - alternatereverse = ... - r""" - The ["alternate reverse" direction as defined in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction#alternate-reverse). - """ - -@typing.final -class ColorScheme(enum.Enum): - r""" - This enum indicates the color scheme used by the widget style. Use this to explicitly switch - between dark and light schemes, or choose Unknown to fall back to the system default. - """ - - unknown = ... - r""" - The scheme is not known and a system wide setting configures this. This could mean that - the widgets are shown in a dark or light scheme, but it could also be a custom color scheme. - """ - dark = ... - r""" - The style chooses light colors for the background and dark for the foreground. - """ - light = ... - r""" - The style chooses dark colors for the background and light for the foreground. - """ - @typing.final class DiagnosticLevel(enum.Enum): Error = ... Warning = ... @typing.final -class DialogButtonRole(enum.Enum): - r""" - This enum represents the value of the `dialog-button-role` property which can be added to - any element within a `Dialog` to put that item in the button row, and its exact position - depends on the role and the platform. - """ - - none = ... - r""" - This isn't a button meant to go into the bottom row - """ - accept = ... - r""" - This is the role of the main button to click to accept the dialog. e.g. "Ok" or "Yes" - """ - reject = ... - r""" - This is the role of the main button to click to reject the dialog. e.g. "Cancel" or "No" - """ - apply = ... - r""" - This is the role of the "Apply" button - """ - reset = ... - r""" - This is the role of the "Reset" button - """ - help = ... - r""" - This is the role of the "Help" button - """ - action = ... - r""" - This is the role of any other button that performs another action. - """ - -@typing.final -class EventResult(enum.Enum): - r""" - This enum describes whether an event was rejected or accepted by an event handler. - """ - - reject = ... - r""" - The event is rejected by this event handler and may then be handled by the parent item - """ - accept = ... - r""" - The event is accepted and won't be processed further - """ - -@typing.final -class FillRule(enum.Enum): - r""" - This enum describes the different ways of deciding what the inside of a shape described by a path shall be. - """ - - nonzero = ... - r""" - The ["nonzero" fill rule as defined in SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#nonzero). - """ - evenodd = ... - r""" - The ["evenodd" fill rule as defined in SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#evenodd) - """ - -@typing.final -class FocusReason(enum.Enum): - r""" - This enum describes the different reasons for a FocusEvent - """ - - programmatic = ... - r""" - A built-in function invocation caused the event (`.focus()`, `.clear-focus()`) - """ - tabnavigation = ... - r""" - Keyboard navigation caused the event (tabbing) - """ - pointerclick = ... - r""" - A mouse click caused the event - """ - popupactivation = ... - r""" - A popup caused the event - """ - windowactivation = ... - r""" - The window manager changed the active window and caused the event - """ - -@typing.final -class ImageFit(enum.Enum): - r""" - This enum defines how the source image shall fit into an `Image` element. - """ - - fill = ... - r""" - Scales and stretches the source image to fit the width and height of the `Image` element. - """ - contain = ... - r""" - The source image is scaled to fit into the `Image` element's dimension while preserving the aspect ratio. - """ - cover = ... - r""" - The source image is scaled to cover into the `Image` element's dimension while preserving the aspect ratio. - If the aspect ratio of the source image doesn't match the element's one, then the image will be clipped to fit. - """ - preserve = ... - r""" - Preserves the size of the source image in logical pixels. - The source image will still be scaled by the scale factor that applies to all elements in the window. - Any extra space will be left blank. - """ - -@typing.final -class ImageHorizontalAlignment(enum.Enum): - r""" - This enum specifies the horizontal alignment of the source image. - """ - - center = ... +class TimerMode(enum.Enum): r""" - Aligns the source image at the center of the `Image` element. + The TimerMode specifies what should happen after the timer fired. + + Used by the `Timer.start()` function. """ - left = ... + SingleShot = ... r""" - Aligns the source image at the left of the `Image` element. + A SingleShot timer is fired only once. """ - right = ... + Repeated = ... r""" - Aligns the source image at the right of the `Image` element. + A Repeated timer is fired repeatedly until it is stopped or dropped. """ -@typing.final -class ImageRendering(enum.Enum): - r""" - This enum specifies how the source image will be scaled. - """ +def init_translations(translations: typing.Any) -> None: ... - smooth = ... - r""" - The image is scaled with a linear interpolation algorithm. - """ - pixelated = ... - r""" - The image is scaled with the nearest neighbor algorithm. - """ +def invoke_from_event_loop(callable: typing.Any) -> None: ... -@typing.final -class ImageTiling(enum.Enum): - r""" - This enum specifies how the source image will be tiled. - """ +def quit_event_loop() -> None: ... - none = ... - r""" - The source image will not be tiled. - """ - repeat = ... - r""" - The source image will be repeated to fill the `Image` element. - """ - round = ... - r""" - The source image will be repeated and scaled to fill the `Image` element, ensuring an integer number of repetitions. - """ +def run_event_loop() -> None: ... -@typing.final -class ImageVerticalAlignment(enum.Enum): - r""" - This enum specifies the vertical alignment of the source image. - """ +def set_xdg_app_id(app_id: builtins.str) -> None: ... - center = ... - r""" - Aligns the source image at the center of the `Image` element. - """ - top = ... - r""" - Aligns the source image at the top of the `Image` element. - """ - bottom = ... - r""" - Aligns the source image at the bottom of the `Image` element. - """ - -@typing.final -class InputType(enum.Enum): - r""" - This enum is used to define the type of the input field. - """ - - text = ... - r""" - The default value. This will render all characters normally - """ - password = ... - r""" - This will render all characters with a character that defaults to "*" - """ - number = ... - r""" - This will only accept and render number characters (0-9) - """ - decimal = ... - r""" - This will accept and render characters if it's valid part of a decimal - """ - -@typing.final -class LayoutAlignment(enum.Enum): - r""" - Enum representing the `alignment` property of a - `HorizontalBox`, a `VerticalBox`, - a `HorizontalLayout`, or `VerticalLayout`. - """ - - stretch = ... - r""" - Use the minimum size of all elements in a layout, distribute remaining space - based on `*-stretch` among all elements. - """ - center = ... - r""" - Use the preferred size for all elements, distribute remaining space evenly before the - first and after the last element. - """ - start = ... - r""" - Use the preferred size for all elements, put remaining space after the last element. - """ - end = ... - r""" - Use the preferred size for all elements, put remaining space before the first - element. - """ - spacebetween = ... - r""" - Use the preferred size for all elements, distribute remaining space evenly between - elements. - """ - spacearound = ... - r""" - Use the preferred size for all elements, distribute remaining space evenly - between the elements, and use half spaces at the start and end. - """ - spaceevenly = ... - r""" - Use the preferred size for all elements, distribute remaining space evenly before the - first element, after the last element and between elements. - """ - -@typing.final -class LineCap(enum.Enum): - r""" - This enum describes the appearance of the ends of stroked paths. - """ - - butt = ... - r""" - The stroke ends with a flat edge that is perpendicular to the path. - """ - round = ... - r""" - The stroke ends with a rounded edge. - """ - square = ... - r""" - The stroke ends with a square projection beyond the path. - """ - -@typing.final -class MouseCursor(enum.Enum): - r""" - This enum represents different types of mouse cursors. It's a subset of the mouse cursors available in CSS. - For details and pictograms see the [MDN Documentation for cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values). - Depending on the backend and used OS unidirectional resize cursors may be replaced with bidirectional ones. - """ - - default = ... - r""" - The systems default cursor. - """ - none = ... - r""" - No cursor is displayed. - """ - help = ... - r""" - A cursor indicating help information. - """ - pointer = ... - r""" - A pointing hand indicating a link. - """ - progress = ... - r""" - The program is busy but can still be interacted with. - """ - wait = ... - r""" - The program is busy. - """ - crosshair = ... - r""" - A crosshair. - """ - text = ... - r""" - A cursor indicating selectable text. - """ - alias = ... - r""" - An alias or shortcut is being created. - """ - copy = ... - r""" - A copy is being created. - """ - move = ... - r""" - Something is to be moved. - """ - nodrop = ... - r""" - Something can't be dropped here. - """ - notallowed = ... - r""" - An action isn't allowed - """ - grab = ... - r""" - Something is grabbable. - """ - grabbing = ... - r""" - Something is being grabbed. - """ - colresize = ... - r""" - Indicating that a column is resizable horizontally. - """ - rowresize = ... - r""" - Indicating that a row is resizable vertically. - """ - nresize = ... - r""" - Unidirectional resize north. - """ - eresize = ... - r""" - Unidirectional resize east. - """ - sresize = ... - r""" - Unidirectional resize south. - """ - wresize = ... - r""" - Unidirectional resize west. - """ - neresize = ... - r""" - Unidirectional resize north-east. - """ - nwresize = ... - r""" - Unidirectional resize north-west. - """ - seresize = ... - r""" - Unidirectional resize south-east. - """ - swresize = ... - r""" - Unidirectional resize south-west. - """ - ewresize = ... - r""" - Bidirectional resize east-west. - """ - nsresize = ... - r""" - Bidirectional resize north-south. - """ - neswresize = ... - r""" - Bidirectional resize north-east-south-west. - """ - nwseresize = ... - r""" - Bidirectional resize north-west-south-east. - """ - -@typing.final -class OperatingSystemType(enum.Enum): - r""" - This enum describes the detected operating system types. - """ - - android = ... - r""" - This variant includes any version of Android running mobile phones, tablets, as well as embedded Android devices. - """ - ios = ... - r""" - This variant covers iOS running on iPhones and iPads. - """ - macos = ... - r""" - This variant covers macOS running on Apple's Mac computers. - """ - linux = ... - r""" - This variant covers any version of Linux, except Android. - """ - windows = ... - r""" - This variant covers Microsoft Windows. - """ - other = ... - r""" - This variant is reported when the operating system is none of the above. - """ - -@typing.final -class Orientation(enum.Enum): - r""" - Represents the orientation of an element or widget such as the `Slider`. - """ - - horizontal = ... - r""" - Element is oriented horizontally. - """ - vertical = ... - r""" - Element is oriented vertically. - """ - -@typing.final -class PathEvent(enum.Enum): - r""" - PathEvent is a low-level data structure describing the composition of a path. Typically it is - generated at compile time from a higher-level description, such as SVG commands. - """ - - begin = ... - r""" - The beginning of the path. - """ - line = ... - r""" - A straight line on the path. - """ - quadratic = ... - r""" - A quadratic bezier curve on the path. - """ - cubic = ... - r""" - A cubic bezier curve on the path. - """ - endopen = ... - r""" - The end of the path that remains open. - """ - endclosed = ... - r""" - The end of a path that is closed. - """ - -@typing.final -class PointerEventButton(enum.Enum): - r""" - This enum describes the different types of buttons for a pointer event, - typically on a mouse or a pencil. - """ - - other = ... - r""" - A button that is none of left, right, middle, back or forward. For example, - this is used for the task button on a mouse with many buttons. - """ - left = ... - r""" - The left button. - """ - right = ... - r""" - The right button. - """ - middle = ... - r""" - The center button. - """ - back = ... - r""" - The back button. - """ - forward = ... - r""" - The forward button. - """ - -@typing.final -class PointerEventKind(enum.Enum): - r""" - The enum reports what happened to the `PointerEventButton` in the event - """ - - cancel = ... - r""" - The action was cancelled. - """ - down = ... - r""" - The button was pressed. - """ - up = ... - r""" - The button was released. - """ - move = ... - r""" - The pointer has moved, - """ - -@typing.final -class PopupClosePolicy(enum.Enum): - closeonclick = ... - r""" - Closes the `PopupWindow` when user clicks or presses the escape key. - """ - closeonclickoutside = ... - r""" - Closes the `PopupWindow` when user clicks outside of the popup or presses the escape key. - """ - noautoclose = ... - r""" - Does not close the `PopupWindow` automatically when user clicks. - """ - -@typing.final -class ScrollBarPolicy(enum.Enum): - r""" - This enum describes the scrollbar visibility - """ - - asneeded = ... - r""" - Scrollbar will be visible only when needed - """ - alwaysoff = ... - r""" - Scrollbar never shown - """ - alwayson = ... - r""" - Scrollbar always visible - """ - -@typing.final -class SortOrder(enum.Enum): - r""" - This enum represents the different values of the `sort-order` property. - It's used to sort a `StandardTableView` by a column. - """ - - unsorted = ... - r""" - The column is unsorted. - """ - ascending = ... - r""" - The column is sorted in ascending order. - """ - descending = ... - r""" - The column is sorted in descending order. - """ - -@typing.final -class StandardButtonKind(enum.Enum): - r""" - Use this enum to add standard buttons to a `Dialog`. The look and positioning - of these `StandardButton`s depends on the environment - (OS, UI environment, etc.) the application runs in. - """ - - ok = ... - r""" - A "OK" button that accepts a `Dialog`, closing it when clicked. - """ - cancel = ... - r""" - A "Cancel" button that rejects a `Dialog`, closing it when clicked. - """ - apply = ... - r""" - A "Apply" button that should accept values from a - `Dialog` without closing it. - """ - close = ... - r""" - A "Close" button, which should close a `Dialog` without looking at values. - """ - reset = ... - r""" - A "Reset" button, which should reset the `Dialog` to its initial state. - """ - help = ... - r""" - A "Help" button, which should bring up context related documentation when clicked. - """ - yes = ... - r""" - A "Yes" button, used to confirm an action. - """ - no = ... - r""" - A "No" button, used to deny an action. - """ - abort = ... - r""" - A "Abort" button, used to abort an action. - """ - retry = ... - r""" - A "Retry" button, used to retry a failed action. - """ - ignore = ... - r""" - A "Ignore" button, used to ignore a failed action. - """ - -@typing.final -class TextHorizontalAlignment(enum.Enum): - r""" - This enum describes the different types of alignment of text along the horizontal axis of a `Text` element. - """ - - left = ... - r""" - The text will be aligned with the left edge of the containing box. - """ - center = ... - r""" - The text will be horizontally centered within the containing box. - """ - right = ... - r""" - The text will be aligned to the right of the containing box. - """ - -@typing.final -class TextOverflow(enum.Enum): - r""" - This enum describes the how the text appear if it is too wide to fit in the `Text` width. - """ - - clip = ... - r""" - The text will simply be clipped. - """ - elide = ... - r""" - The text will be elided with `…`. - """ - -@typing.final -class TextStrokeStyle(enum.Enum): - r""" - This enum describes the positioning of a text stroke relative to the border of the glyphs in a `Text`. - """ - - outside = ... - r""" - The inside edge of the stroke is at the outer edge of the text. - """ - center = ... - r""" - The center line of the stroke is at the outer edge of the text, like in Adobe Illustrator. - """ - -@typing.final -class TextVerticalAlignment(enum.Enum): - r""" - This enum describes the different types of alignment of text along the vertical axis of a `Text` element. - """ - - top = ... - r""" - The text will be aligned to the top of the containing box. - """ - center = ... - r""" - The text will be vertically centered within the containing box. - """ - bottom = ... - r""" - The text will be aligned to the bottom of the containing box. - """ - -@typing.final -class TextWrap(enum.Enum): - r""" - This enum describes the how the text wrap if it is too wide to fit in the `Text` width. - """ - - nowrap = ... - r""" - The text won't wrap, but instead will overflow. - """ - wordwrap = ... - r""" - The text will be wrapped at word boundaries if possible, or at any location for very long words. - """ - charwrap = ... - r""" - The text will be wrapped at any character. Currently only supported by the Qt and Software renderers. - """ - -@typing.final -class TimerMode(enum.Enum): - r""" - The TimerMode specifies what should happen after the timer fired. - - Used by the `Timer.start()` function. - """ - - SingleShot = ... - r""" - A SingleShot timer is fired only once. - """ - Repeated = ... - r""" - A Repeated timer is fired repeatedly until it is stopped or dropped. - """ - -@typing.final -class ValueType(enum.Enum): - Void = ... - Number = ... - String = ... - Bool = ... - Model = ... - Struct = ... - Brush = ... - Image = ... - Enumeration = ... - -def init_translations(translations: typing.Any) -> None: ... -def invoke_from_event_loop(callable: typing.Any) -> None: ... -def quit_event_loop() -> None: ... -def run_event_loop() -> None: ... -def set_xdg_app_id(app_id: builtins.str) -> None: ... diff --git a/api/python/slint/structs.rs b/api/python/slint/structs.rs deleted file mode 100644 index 3cafbd32b6e..00000000000 --- a/api/python/slint/structs.rs +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright © SixtyFPS GmbH -// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - -use pyo3::exceptions::PyTypeError; -use pyo3::prelude::*; -use pyo3::types::{PyDict, PyString, PyTuple}; - -use crate::value::TypeCollection; - -#[pyclass(unsendable, module = "slint.core")] -pub struct StructFactory { - name: &'static str, - exported_fields: Vec<&'static str>, - default_data: slint_interpreter::Struct, - type_collection: TypeCollection, -} - -impl StructFactory { - fn new( - name: &'static str, - exported_fields: Vec<&'static str>, - default_data: slint_interpreter::Struct, - type_collection: TypeCollection, - ) -> Self { - Self { name, exported_fields, default_data, type_collection } - } -} - -#[pymethods] -impl StructFactory { - #[pyo3(signature = (*args, **kwargs))] - fn __call__( - &self, - py: Python<'_>, - args: &Bound<'_, PyTuple>, - kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult> { - if args.len() != 0 { - return Err(PyTypeError::new_err(format!( - "{}() accepts keyword arguments only", - self.name - ))); - } - - let pystruct = Py::new( - py, - crate::value::PyStruct { - data: self.default_data.clone(), - type_collection: self.type_collection.clone(), - }, - )?; - - if let Some(kwargs) = kwargs { - let instance = pystruct.bind(py); - for (key_obj, value) in kwargs.iter() { - let key = key_obj.downcast::().map_err(|_| { - PyTypeError::new_err(format!( - "{}() keyword arguments must be strings", - self.name - )) - })?; - let key_str = key.to_str()?; - if !self.exported_fields.iter().any(|field| *field == key_str) { - return Err(PyTypeError::new_err(format!( - "{}() got an unexpected keyword argument '{}'", - self.name, key_str - ))); - } - instance.setattr(key_str, value)?; - } - } - - Ok(pystruct) - } - - #[getter] - fn __name__(&self) -> &str { - self.name - } -} - -macro_rules! generate_struct_support { - ($( - $(#[$struct_attr:meta])* - struct $Name:ident { - @name = $inner_name:literal - export { - $( $(#[$pub_attr:meta])* $pub_field:ident : $pub_type:ty, )* - } - private { - $( $(#[$pri_attr:meta])* $pri_field:ident : $pri_type:ty, )* - } - } - )*) => { - #[cfg(feature = "stubgen")] - pub(super) mod stub_structs { - use pyo3_stub_gen::{ - inventory, - type_info::{ - MemberInfo, MethodInfo, MethodType, ParameterDefault, ParameterInfo, - ParameterKind, PyClassInfo, PyMethodsInfo, DeprecatedInfo, - }, - TypeInfo, - }; - - const EMPTY_DOC: &str = ""; - const NO_DEFAULT: Option String> = None; - const NO_DEPRECATED: Option = None; - - macro_rules! field_type_info { - (bool) => { || ::type_output() }; - (f32) => { || ::type_output() }; - (f64) => { || ::type_output() }; - (i16) => { || ::type_output() }; - (i32) => { || ::type_output() }; - (u32) => { || ::type_output() }; - (SharedString) => { || ::type_output() }; - (String) => { || ::type_output() }; - (Coord) => { || TypeInfo::builtin("float") }; - (PointerEventButton) => { || TypeInfo::unqualified("PointerEventButton") }; - (PointerEventKind) => { || TypeInfo::unqualified("PointerEventKind") }; - (KeyboardModifiers) => { || TypeInfo::unqualified("KeyboardModifiers") }; - (SortOrder) => { || TypeInfo::unqualified("SortOrder") }; - (MenuEntry) => { || TypeInfo::unqualified("MenuEntry") }; - (LogicalPosition) => { - || TypeInfo::with_module("typing.Tuple[float, float]", "typing".into()) - }; - (Image) => { - || TypeInfo::with_module("slint.Image", "slint".into()) - }; - ($other:ty) => { || TypeInfo::with_module("typing.Any", "typing".into()) }; - } - fn ellipsis_default() -> String { - "...".to_string() - } - - mod markers { - $( - pub struct $Name; - )* - } - - $( - inventory::submit! { - PyClassInfo { - pyclass_name: stringify!($Name), - struct_id: || ::std::any::TypeId::of::(), - module: Some("slint.core"), - doc: EMPTY_DOC, - getters: &[ - $( - MemberInfo { - name: stringify!($pub_field), - r#type: field_type_info!($pub_type), - doc: EMPTY_DOC, - default: NO_DEFAULT, - deprecated: NO_DEPRECATED, - item: false, - is_abstract: false, - }, - )* - ], - setters: &[ - $( - MemberInfo { - name: stringify!($pub_field), - r#type: field_type_info!($pub_type), - doc: EMPTY_DOC, - default: NO_DEFAULT, - deprecated: NO_DEPRECATED, - item: false, - is_abstract: false, - }, - )* - ], - bases: &[], - has_eq: false, - has_ord: false, - has_hash: false, - has_str: false, - subclass: false, - is_abstract: false, - } - } - - inventory::submit! { - PyMethodsInfo { - struct_id: || ::std::any::TypeId::of::(), - attrs: &[], - getters: &[], - setters: &[], - methods: &[MethodInfo { - name: "__init__", - parameters: &[ $( - ParameterInfo { - name: stringify!($pub_field), - kind: ParameterKind::KeywordOnly, - type_info: field_type_info!($pub_type), - default: ParameterDefault::Expr(ellipsis_default), - }, - )* ], - r#return: ::pyo3_stub_gen::type_info::no_return_type_output, - doc: EMPTY_DOC, - r#type: MethodType::Instance, - is_async: false, - deprecated: NO_DEPRECATED, - type_ignored: None, - is_abstract: false, - }], - } - } - )* - } - - fn register_built_in_structs( - py: Python<'_>, - module: &Bound<'_, PyModule>, - type_collection: &TypeCollection, - ) -> PyResult<()> { - $( - { - let name = stringify!($Name); - let default_value: slint_interpreter::Value = - i_slint_core::items::$Name::default().into(); - let data = match default_value { - slint_interpreter::Value::Struct(s) => s, - _ => unreachable!(), - }; - - let factory = StructFactory::new( - name, - vec![ $( stringify!($pub_field), )* ], - data, - type_collection.clone(), - ); - - let factory_py = Py::new(py, factory)?; - module.add(name, factory_py.clone_ref(py))?; - } - )* - Ok(()) - } - }; -} - -i_slint_common::for_each_builtin_structs!(generate_struct_support); - -pub fn register_structs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { - let type_collection = TypeCollection::with_builtin(py); - register_built_in_structs(py, module, &type_collection) -} diff --git a/api/python/slint/tests/codegen/examples/counter/counter.pyi b/api/python/slint/tests/codegen/examples/counter/counter.pyi index ec9e1bad2d6..89f2fb8b43b 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.pyi +++ b/api/python/slint/tests/codegen/examples/counter/counter.pyi @@ -1,17 +1,16 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - from __future__ import annotations -import enum - -from typing import Any, Callable - +from typing import ( + Any, + Callable, +) import slint -__all__ = ["CounterWindow"] +__all__ = ['CounterWindow'] class CounterWindow(slint.Component): def __init__(self, **kwargs: Any) -> None: ... + alignment: Any counter: int request_increase: Callable[[], None] + From 687aa660c8087c422de5c3ff3361f4885328c716 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 19:31:41 +0800 Subject: [PATCH 34/52] refactor: revert defination methods de-optional --- api/python/slint/interpreter.rs | 12 ++++-------- api/python/slint/slint/api.py | 22 +++++++++++++++------- api/python/slint/slint/core.pyi | 20 ++++++++++++++++---- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index 2f98708f78e..bf6ce2b2cd0 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -242,7 +242,6 @@ impl ComponentDefinition { self.definition.name() } - #[gen_stub(override_return_type(type_repr = "typing.Dict[str, typing.Any]", imports = ("typing",)))] #[getter] fn properties(&self) -> IndexMap { self.definition @@ -299,8 +298,7 @@ impl ComponentDefinition { .collect() } - #[gen_stub(override_return_type(type_repr = "typing.Dict[str, typing.Any]", imports = ("typing",)))] - fn global_properties(&self, name: &str) -> IndexMap { + fn global_properties(&self, name: &str) -> Option> { self.definition .global_properties_and_callbacks(name) .map(|propiter| { @@ -308,21 +306,18 @@ impl ComponentDefinition { .filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (name, ty.into()))) .collect() }) - .unwrap_or_default() } - fn global_callbacks(&self, name: &str) -> Vec { + fn global_callbacks(&self, name: &str) -> Option> { self.definition .global_callbacks(name) .map(|callbackiter| callbackiter.collect()) - .unwrap_or_default() } - fn global_functions(&self, name: &str) -> Vec { + fn global_functions(&self, name: &str) -> Option> { self.definition .global_functions(name) .map(|functioniter| functioniter.collect()) - .unwrap_or_default() } fn global_property_infos(&self, global_name: &str) -> Option> { @@ -404,6 +399,7 @@ impl ComponentDefinition { } } +#[gen_stub_pyclass_enum] #[pyclass(name = "ValueType", eq, eq_int)] #[derive(PartialEq)] pub enum PyValueType { diff --git a/api/python/slint/slint/api.py b/api/python/slint/slint/api.py index 85226c51894..cfa0085b990 100644 --- a/api/python/slint/slint/api.py +++ b/api/python/slint/slint/api.py @@ -90,7 +90,15 @@ def _normalize_prop(name: str) -> str: def _build_global_class(compdef: ComponentDefinition, global_name: str) -> Any: properties_and_callbacks = {} - for prop_name in compdef.global_properties(global_name).keys(): + global_props = compdef.global_properties(global_name) + global_callbacks = compdef.global_callbacks(global_name) + global_functions = compdef.global_functions(global_name) + + assert global_props is not None + assert global_callbacks is not None + assert global_functions is not None + + for prop_name in global_props.keys(): python_prop = _normalize_prop(prop_name) if python_prop in properties_and_callbacks: logging.warning(f"Duplicated property {prop_name}") @@ -111,7 +119,7 @@ def setter(self: Component, value: Any) -> None: properties_and_callbacks[python_prop] = mk_setter_getter(prop_name) - for callback_name in compdef.global_callbacks(global_name): + for callback_name in global_callbacks: python_prop = _normalize_prop(callback_name) if python_prop in properties_and_callbacks: logging.warning(f"Duplicated property {prop_name}") @@ -135,7 +143,7 @@ def setter(self: Component, value: typing.Callable[..., Any]) -> None: properties_and_callbacks[python_prop] = mk_setter_getter(callback_name) - for function_name in compdef.global_functions(global_name): + for function_name in global_functions: python_prop = _normalize_prop(function_name) if python_prop in properties_and_callbacks: logging.warning(f"Duplicated function {prop_name}") @@ -281,9 +289,7 @@ def new_struct(cls: Any, *args: Any, **kwargs: Any) -> PyStruct: unexpected = set(kwargs) - field_names if unexpected: formatted = ", ".join(sorted(unexpected)) - raise TypeError( - f"{name}() got unexpected keyword argument(s): {formatted}" - ) + raise TypeError(f"{name}() got unexpected keyword argument(s): {formatted}") inst = copy.copy(struct_prototype) @@ -581,7 +587,9 @@ async def run_inner() -> None: global quit_event quit_event = asyncio.Event() - asyncio.run(run_inner(), debug=False, loop_factory=SlintEventLoop) + + with asyncio.Runner(loop_factory=SlintEventLoop) as runner: + runner.run(run_inner()) def quit_event_loop() -> None: diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index e7399b07ca1..c8a120a64b4 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -177,7 +177,7 @@ class ComponentDefinition: @property def name(self) -> builtins.str: ... @property - def properties(self) -> typing.Dict[str, typing.Any]: ... + def properties(self) -> builtins.dict[builtins.str, ValueType]: ... @property def callbacks(self) -> builtins.list[builtins.str]: ... @property @@ -187,9 +187,9 @@ class ComponentDefinition: def property_infos(self) -> builtins.list[PropertyInfo]: ... def callback_infos(self) -> builtins.list[CallbackInfo]: ... def function_infos(self) -> builtins.list[FunctionInfo]: ... - def global_properties(self, name: builtins.str) -> typing.Dict[str, typing.Any]: ... - def global_callbacks(self, name: builtins.str) -> builtins.list[builtins.str]: ... - def global_functions(self, name: builtins.str) -> builtins.list[builtins.str]: ... + def global_properties(self, name: builtins.str) -> typing.Optional[builtins.dict[builtins.str, ValueType]]: ... + def global_callbacks(self, name: builtins.str) -> typing.Optional[builtins.list[builtins.str]]: ... + def global_functions(self, name: builtins.str) -> typing.Optional[builtins.list[builtins.str]]: ... def global_property_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[PropertyInfo]]: ... def global_callback_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[CallbackInfo]]: ... def global_function_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[FunctionInfo]]: ... @@ -552,6 +552,18 @@ class TimerMode(enum.Enum): A Repeated timer is fired repeatedly until it is stopped or dropped. """ +@typing.final +class ValueType(enum.Enum): + Void = ... + Number = ... + String = ... + Bool = ... + Model = ... + Struct = ... + Brush = ... + Image = ... + Enumeration = ... + def init_translations(translations: typing.Any) -> None: ... def invoke_from_event_loop(callable: typing.Any) -> None: ... From 2d0c9de2f65265b94e950294db3ed3a2e31fc5b8 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 19:32:53 +0800 Subject: [PATCH 35/52] fix(python): adopt loop to standard selector loop behavior --- api/python/slint/slint/loop.py | 99 ++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index a9ebbff176b..a0dd3e4c16b 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -6,6 +6,7 @@ import asyncio.selector_events import datetime import selectors +import socket import typing from collections.abc import Mapping @@ -46,6 +47,11 @@ def __init__(self) -> None: self.fd_to_selector_key: typing.Dict[typing.Any, selectors.SelectorKey] = {} self.mapping = _SlintSelectorMapping(self) self.adapters: typing.Dict[int, core.AsyncAdapter] = {} + self._base_selector = selectors.DefaultSelector() + self._wakeup_reader, self._wakeup_writer = socket.socketpair() + self._wakeup_reader.setblocking(False) + self._wakeup_writer.setblocking(False) + self._base_selector.register(self._wakeup_reader, selectors.EVENT_READ) def register( self, fileobj: typing.Any, events: typing.Any, data: typing.Any = None @@ -62,6 +68,11 @@ def register( if events & selectors.EVENT_WRITE: adapter.wait_for_writable(self.write_notify) + try: + self._base_selector.register(fileobj, events, data) + except KeyError: + self._base_selector.modify(fileobj, events, data) + return key def unregister(self, fileobj: typing.Any) -> selectors.SelectorKey: @@ -73,6 +84,11 @@ def unregister(self, fileobj: typing.Any) -> selectors.SelectorKey: except KeyError: pass + try: + self._base_selector.unregister(fileobj) + except KeyError: + pass + return key def modify( @@ -88,15 +104,38 @@ def modify( key._replace(data=data) self.fd_to_selector_key[fd] = key + try: + self._base_selector.modify(fileobj, events, data) + except KeyError: + self._base_selector.register(fileobj, events, data) + return key def select( self, timeout: float | None = None ) -> typing.List[typing.Tuple[selectors.SelectorKey, int]]: - raise NotImplementedError + events = self._base_selector.select(timeout) + ready: typing.List[typing.Tuple[selectors.SelectorKey, int]] = [] + for key, mask in events: + if key.fileobj is self._wakeup_reader: + self._drain_wakeup() + continue + + fd = fd_for_fileobj(key.fileobj) + slint_key = self.fd_to_selector_key.get(fd) + if slint_key is not None: + ready.append((slint_key, mask)) + + return ready def close(self) -> None: - pass + try: + self._base_selector.unregister(self._wakeup_reader) + except Exception: + pass + self._base_selector.close() + self._wakeup_reader.close() + self._wakeup_writer.close() def get_map(self) -> Mapping[int | HasFileno, selectors.SelectorKey]: return self.mapping @@ -105,11 +144,26 @@ def read_notify(self, fd: int) -> None: key = self.fd_to_selector_key[fd] (reader, writer) = key.data reader._run() + self._wakeup() def write_notify(self, fd: int) -> None: key = self.fd_to_selector_key[fd] (reader, writer) = key.data writer._run() + self._wakeup() + + def _wakeup(self) -> None: + try: + self._wakeup_writer.send(b"\0") + except BlockingIOError: + pass + + def _drain_wakeup(self) -> None: + try: + while self._wakeup_reader.recv(1024): + pass + except BlockingIOError: + pass class SlintEventLoop(asyncio.SelectorEventLoop): @@ -118,24 +172,38 @@ def __init__(self) -> None: self._timers: typing.Set[core.Timer] = set() self.stop_run_forever_event = asyncio.Event() self._soon_tasks: typing.List[asyncio.TimerHandle] = [] + self._core_loop_started = False + self._core_loop_running = False super().__init__(_SlintSelector()) def run_forever(self) -> None: + if self._core_loop_started: + return asyncio.selector_events.BaseSelectorEventLoop.run_forever(self) + async def loop_stopper(event: asyncio.Event) -> None: await event.wait() core.quit_event_loop() asyncio.events._set_running_loop(self) self._is_running = True + self._core_loop_started = True + self._core_loop_running = True try: self.stop_run_forever_event = asyncio.Event() self.create_task(loop_stopper(self.stop_run_forever_event)) core.run_event_loop() finally: + self._core_loop_running = False self._is_running = False + self._stopping = False asyncio.events._set_running_loop(None) def run_until_complete[T](self, future: typing.Awaitable[T]) -> T | None: # type: ignore[override] + if self._core_loop_started and not self._core_loop_running: + return asyncio.selector_events.BaseSelectorEventLoop.run_until_complete( + self, future + ) # type: ignore[misc] + def stop_loop(future: typing.Any) -> None: self.stop() @@ -167,7 +235,17 @@ def _run_forever_cleanup(self) -> None: pass def stop(self) -> None: - self.stop_run_forever_event.set() + if ( + self._core_loop_started + and self._core_loop_running + and self.stop_run_forever_event is not None + ): + self.stop_run_forever_event.set() + + super().stop() + selector = self._selector + if isinstance(selector, _SlintSelector): + selector._wakeup() def is_running(self) -> bool: return self._is_running @@ -179,6 +257,9 @@ def is_closed(self) -> bool: return False def call_later(self, delay, callback, *args, context=None) -> asyncio.TimerHandle: # type: ignore + if self._core_loop_started and not self._core_loop_running: + return super().call_later(delay, callback, *args, context=context) + timer = core.Timer() handle = asyncio.TimerHandle( @@ -210,6 +291,9 @@ def call_at(self, when, callback, *args, context=None) -> asyncio.TimerHandle: return self.call_later(when - self.time(), callback, *args, context=context) def call_soon(self, callback, *args, context=None) -> asyncio.TimerHandle: # type: ignore + if self._core_loop_started and not self._core_loop_running: + return super().call_soon(callback, *args, context=context) # type: ignore + # Collect call-soon tasks in a separate list to ensure FIFO order, as there's no guarantee # that multiple single-shot timers in Slint are run in order. handle = asyncio.TimerHandle( @@ -227,6 +311,9 @@ def _flush_soon_tasks(self) -> None: handle._run() def call_soon_threadsafe(self, callback, *args, context=None) -> asyncio.Handle: # type: ignore + if self._core_loop_started and not self._core_loop_running: + return super().call_soon_threadsafe(callback, *args, context=context) + handle = asyncio.Handle( callback=callback, args=args, @@ -242,4 +329,8 @@ def run_handle_cb() -> None: return handle def _write_to_self(self) -> None: - raise NotImplementedError + selector = self._selector + if isinstance(selector, _SlintSelector): + selector._wakeup() + else: + super()._write_to_self() From fe2c27305afcb35229dfbe4330c5b8204dca5e5c Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 19:42:27 +0800 Subject: [PATCH 36/52] refactor: remove unused /undecided methods from ComponentInstance class --- api/python/slint/slint/core.pyi | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index c8a120a64b4..0b9d782b84f 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -211,8 +211,6 @@ class ComponentInstance: def set_global_callback(self, global_name: builtins.str, callback_name: builtins.str, callable: typing.Any) -> None: ... def show(self) -> None: ... def hide(self) -> None: ... - def on_close_requested(self, callable: typing.Any) -> None: ... - def dispatch_close_requested_event(self) -> None: ... def __clear__(self) -> None: ... @typing.final From 35c1ed0c78ab654aab322d99e2f7a0f7070fa793 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 19:44:36 +0800 Subject: [PATCH 37/52] refactor(python): code generation and testing - Added `is_builtin` and `is_used` attributes to `StructMeta` and `EnumMeta` classes for better tracking of built-in types. - Updated the counter example to include a new property for text alignment and adjusted the generated Python code accordingly. - Introduced a new test module for optional types, including structures and enums, to validate their behavior. - Enhanced the test suite to ensure proper handling of enums and structures, including keyword-only arguments and unknown keywords. - Improved the handling of built-in enums in the Rust backend, ensuring they are correctly initialized and exposed to Python. - Refactored existing tests to use the new structure and ensure compatibility with the updated code generation logic. - Added tests for event loop handling, including SIGINT and quit events, to ensure robustness in asynchronous scenarios. --- api/python/slint/slint/codegen/emitters.py | 492 ++++++++++++++++-- api/python/slint/slint/codegen/generator.py | 113 +++- api/python/slint/slint/codegen/models.py | 2 + .../tests/codegen/examples/counter/counter.py | 9 +- .../codegen/examples/counter/counter.slint | 2 + .../tests/codegen/examples/counter/main.py | 7 +- .../tests/codegen/examples/counter/test.py | 45 ++ .../tests/codegen/examples/counter/test.pyi | 40 ++ .../tests/codegen/examples/counter/test.slint | 32 ++ .../slint/tests/codegen/test_generator.py | 31 +- .../slint/tests/test_callback_decorators.py | 6 +- api/python/slint/tests/test_compiler.py | 2 +- api/python/slint/tests/test_enums.py | 107 +++- .../slint/tests/test_load_file_source.py | 79 +-- .../slint/tests/test_load_file_source.pyi | 284 +--------- api/python/slint/tests/test_loop.py | 48 +- api/python/slint/tests/test_sigint.py | 22 + api/python/slint/tests/test_structs.py | 140 ++++- api/python/slint/tests/test_translations.py | 2 +- api/python/slint/value.rs | 95 +++- 20 files changed, 1081 insertions(+), 477 deletions(-) create mode 100644 api/python/slint/tests/codegen/examples/counter/test.py create mode 100644 api/python/slint/tests/codegen/examples/counter/test.pyi create mode 100644 api/python/slint/tests/codegen/examples/counter/test.slint create mode 100644 api/python/slint/tests/test_sigint.py diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index ae1b869ac68..3930c3d71d5 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -3,14 +3,15 @@ from __future__ import annotations +import inspect import os +import re from pathlib import Path import libcst as cst from ..api import _normalize_prop -from .models import CallbackMeta, GenerationConfig, ModuleArtifacts -import inspect +from .models import CallbackMeta, GenerationConfig, ModuleArtifacts, StructMeta def module_relative_path_expr(module_dir: Path, target: Path) -> str: @@ -31,6 +32,70 @@ def module_relative_path_expr(module_dir: Path, target: Path) -> str: return expr +def _collect_export_bindings( + artifacts: ModuleArtifacts, *, include_builtin_enums: bool +) -> dict[str, str]: + bindings: dict[str, str] = {} + + for component in artifacts.components: + bindings[component.name] = component.py_name + + for struct in artifacts.structs: + bindings[struct.name] = struct.py_name + + for enum in artifacts.enums: + if enum.is_builtin and not include_builtin_enums: + continue + if not enum.is_builtin: + bindings[enum.name] = enum.py_name + elif include_builtin_enums: + bindings[enum.name] = enum.py_name + + return bindings + + +def _format_import(module: str, names: list[str]) -> str: + if not names: + raise ValueError("Expected at least one name to import") + + if len(names) == 1: + return f"from {module} import {names[0]}" + + joined = ",\n ".join(names) + return f"from {module} import (\n {joined},\n)" + + +def _unique_preserve_order(values: list[str]) -> list[str]: + seen: set[str] = set() + result: list[str] = [] + for value in values: + if value in seen: + continue + seen.add(value) + result.append(value) + return result + + +def _collect_named_aliases( + artifacts: ModuleArtifacts, + export_bindings: dict[str, str], + *, + allowed_originals: set[str] | None = None, +) -> tuple[list[str], list[str]]: + alias_names: list[str] = [] + alias_statements: list[str] = [] + + for original, alias in artifacts.named_exports: + if allowed_originals is not None and original not in allowed_originals: + continue + alias_name = _normalize_prop(alias) + target = export_bindings.get(original, _normalize_prop(original)) + alias_names.append(alias_name) + alias_statements.append(f"{alias_name} = {target}") + + return alias_names, alias_statements + + def _write_struct_python_module( path: Path, *, @@ -229,15 +294,7 @@ def _stmt(code: str) -> cst.BaseStatement: repr(Path(config.translation_domain)) if config.translation_domain else "None" ) - export_bindings: dict[str, str] = {} - for component in artifacts.components: - export_bindings[component.name] = component.py_name - for struct in artifacts.structs: - export_bindings[struct.name] = struct.py_name - for enum in artifacts.enums: - if enum.is_builtin: - continue - export_bindings[enum.name] = enum.py_name + export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) export_items = list(export_bindings.values()) + [ _normalize_prop(alias) for _, alias in artifacts.named_exports @@ -353,25 +410,21 @@ def write_stub_module(path: Path, *, artifacts: ModuleArtifacts) -> None: def _stmt(code: str) -> cst.BaseStatement: return cst.parse_statement(code) - typing_imports = {"Any", "Callable"} + typing_imports: set[str] = set() def register_type(type_str: str) -> None: - if "Optional[" in type_str: - typing_imports.add("Optional") - if "Literal[" in type_str: - typing_imports.add("Literal") - if "Union[" in type_str: - typing_imports.add("Union") - - preamble: list[cst.CSTNode] = [ - _stmt("from __future__ import annotations"), - cst.EmptyLine(), - _stmt("import enum"), - cst.EmptyLine(), - ] + if not type_str: + return + normalized = type_str.replace("typing.", "") + tokens = set(token for token in re.split(r"[^A-Za-z_]+", normalized) if token) + for token in ("Any", "Callable", "Literal", "Optional", "Union"): + if token in tokens: + typing_imports.add(token) post_body: list[cst.CSTNode] = [] + needs_enum_import = any(not enum.is_builtin for enum in artifacts.enums) + export_names = [component.py_name for component in artifacts.components] export_names += [struct.py_name for struct in artifacts.structs] export_names += [enum.py_name for enum in artifacts.enums if not enum.is_builtin] @@ -409,7 +462,39 @@ def ann_assign(name: str, type_expr: str) -> cst.BaseStatement: def ellipsis_line() -> cst.BaseStatement: return cst.SimpleStatementLine([cst.Expr(value=cst.Ellipsis())]) - def init_stub() -> cst.FunctionDef: + def ellipsis_suite() -> cst.BaseSuite: + return cst.SimpleStatementSuite([cst.Expr(value=cst.Ellipsis())]) + + def struct_init_stub(struct: StructMeta) -> cst.FunctionDef: + params = [cst.Param(name=cst.Name("self"))] + kwonly_params: list[cst.Param] = [] + + for field in struct.fields: + register_type(field.type_hint) + kwonly_params.append( + cst.Param( + name=cst.Name(field.py_name), + annotation=cst.Annotation( + annotation=cst.parse_expression(field.type_hint) + ), + default=cst.Ellipsis(), + ) + ) + + parameters = cst.Parameters(params=params, kwonly_params=kwonly_params) + + if not struct.fields: + parameters = cst.Parameters(params=params) + + return cst.FunctionDef( + name=cst.Name("__init__"), + params=parameters, + returns=cst.Annotation(annotation=cst.Name("None")), + body=ellipsis_suite(), + ) + + def component_init_stub() -> cst.FunctionDef: + typing_imports.add("Any") return cst.FunctionDef( name=cst.Name("__init__"), params=cst.Parameters( @@ -420,11 +505,11 @@ def init_stub() -> cst.FunctionDef: ), ), returns=cst.Annotation(annotation=cst.Name("None")), - body=cst.IndentedBlock(body=[ellipsis_line()]), + body=ellipsis_suite(), ) for struct in artifacts.structs: - struct_body: list[cst.BaseStatement] = [init_stub()] + struct_body: list[cst.BaseStatement] = [struct_init_stub(struct)] if struct.fields: for field in struct.fields: register_type(field.type_hint) @@ -474,7 +559,7 @@ def init_stub() -> cst.FunctionDef: post_body.append(cst.EmptyLine()) for component in artifacts.components: - component_body: list[cst.BaseStatement] = [init_stub()] + component_body: list[cst.BaseStatement] = [component_init_stub()] for prop in component.properties: register_type(prop.type_hint) component_body.append(ann_assign(prop.py_name, prop.type_hint)) @@ -533,9 +618,8 @@ def init_stub() -> cst.FunctionDef: for struct in artifacts.structs: bindings[struct.name] = struct.py_name for enum_meta in artifacts.enums: - if enum_meta.is_builtin: - continue - bindings[enum_meta.name] = enum_meta.py_name + if not enum_meta.is_builtin: + bindings[enum_meta.name] = enum_meta.py_name for orig, alias in artifacts.named_exports: alias_name = _normalize_prop(alias) @@ -543,13 +627,339 @@ def init_stub() -> cst.FunctionDef: post_body.append(_stmt(f"{alias_name} = {target}")) post_body.append(cst.EmptyLine()) - typing_alias = ", ".join(sorted(typing_imports)) - preamble.append(_stmt(f"from typing import {typing_alias}")) - preamble.append(cst.EmptyLine()) - preamble.append(_stmt("import slint")) - preamble.append(cst.EmptyLine()) + module_body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + cst.EmptyLine(), + ] + + import_statements: list[cst.CSTNode] = [] + + if needs_enum_import: + import_statements.append(_stmt("import enum")) + + typing_names = sorted(typing_imports) + if typing_names: + import_statements.append(_stmt(_format_import("typing", typing_names))) + + if artifacts.components: + import_statements.append(_stmt("import slint")) + + if import_statements: + module_body.extend(import_statements) + module_body.append(cst.EmptyLine()) + + module_body.extend(post_body) + + module = cst.Module(body=module_body) # type: ignore[arg-type] + path.write_text(module.code, encoding="utf-8") + + +def write_package_init( + path: Path, + *, + source_relative: str, + artifacts: ModuleArtifacts, +) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + header: list[cst.CSTNode] = [ + cst.EmptyLine(comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}")) + ] + + body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + cst.EmptyLine(), + _stmt("from . import enums, structs"), + ] + + export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) + symbol_names = list(export_bindings.values()) + + if symbol_names: + body.append(cst.EmptyLine()) + body.append(_stmt(_format_import("._generated", symbol_names))) + + enum_names = {enum.name for enum in artifacts.enums if not enum.is_builtin} + struct_names = {struct.name for struct in artifacts.structs} + component_names = {component.name for component in artifacts.components} + allowed_for_init = enum_names | struct_names | component_names + + alias_names, alias_statements = _collect_named_aliases( + artifacts, + export_bindings, + allowed_originals=allowed_for_init, + ) + + if alias_statements: + body.append(cst.EmptyLine()) + for stmt in alias_statements: + body.append(_stmt(stmt)) + + export_names = _unique_preserve_order(symbol_names + alias_names + ["enums", "structs"]) + body.append(cst.EmptyLine()) + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] + ) + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], + value=all_list, + ) + ] + ) + ) + + module = cst.Module(body=header + body) # type: ignore[arg-type] + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(module.code, encoding="utf-8") + + +def write_package_init_stub(path: Path, *, artifacts: ModuleArtifacts) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + cst.EmptyLine(), + _stmt("from . import enums, structs"), + ] + + export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) + symbol_names = list(export_bindings.values()) + + if symbol_names: + body.append(cst.EmptyLine()) + body.append(_stmt(_format_import("._generated", symbol_names))) + + enum_original = {enum.name for enum in artifacts.enums if not enum.is_builtin} + struct_original = {struct.name for struct in artifacts.structs} + component_original = {component.name for component in artifacts.components} + allowed_for_init = enum_original | struct_original | component_original + alias_names, alias_statements = _collect_named_aliases( + artifacts, + export_bindings, + allowed_originals=set(allowed_for_init), + ) + + if alias_statements: + body.append(cst.EmptyLine()) + for stmt in alias_statements: + body.append(_stmt(stmt)) + + export_names = _unique_preserve_order(symbol_names + alias_names + ["enums", "structs"]) + body.append(cst.EmptyLine()) + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] + ) + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], + value=all_list, + ) + ] + ) + ) + + module = cst.Module(body=body) # type: ignore[arg-type] + path.write_text(module.code, encoding="utf-8") + + +def write_package_enums(path: Path, *, source_relative: str, artifacts: ModuleArtifacts) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + header: list[cst.CSTNode] = [ + cst.EmptyLine(comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}")) + ] + + export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) + user_enum_names = [enum.py_name for enum in artifacts.enums if not enum.is_builtin] + + body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + ] + + if user_enum_names: + body.append(cst.EmptyLine()) + body.append(_stmt(_format_import("._generated", user_enum_names))) + + enum_originals = {enum.name for enum in artifacts.enums if not enum.is_builtin} + alias_names, alias_statements = _collect_named_aliases( + artifacts, + export_bindings, + allowed_originals=enum_originals, + ) + + if alias_statements: + body.append(cst.EmptyLine()) + for stmt in alias_statements: + body.append(_stmt(stmt)) + + export_names = _unique_preserve_order(user_enum_names + alias_names) + body.append(cst.EmptyLine()) + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] + ) + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], + value=all_list, + ) + ] + ) + ) + + module = cst.Module(body=header + body) # type: ignore[arg-type] + path.write_text(module.code, encoding="utf-8") + + +def write_package_enums_stub(path: Path, *, artifacts: ModuleArtifacts) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) + user_enum_names = [enum.py_name for enum in artifacts.enums if not enum.is_builtin] + + body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + ] + + if user_enum_names: + body.append(cst.EmptyLine()) + body.append(_stmt(_format_import("._generated", user_enum_names))) + + enum_originals = {enum.name for enum in artifacts.enums if not enum.is_builtin} + alias_names, alias_statements = _collect_named_aliases( + artifacts, + export_bindings, + allowed_originals=enum_originals, + ) + + if alias_statements: + body.append(cst.EmptyLine()) + for stmt in alias_statements: + body.append(_stmt(stmt)) + + export_names = _unique_preserve_order(user_enum_names + alias_names) + body.append(cst.EmptyLine()) + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] + ) + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], + value=all_list, + ) + ] + ) + ) + + module = cst.Module(body=body) # type: ignore[arg-type] + path.write_text(module.code, encoding="utf-8") + + +def write_package_structs(path: Path, *, source_relative: str, artifacts: ModuleArtifacts) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + header: list[cst.CSTNode] = [ + cst.EmptyLine(comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}")) + ] + + export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) + struct_names = [struct.py_name for struct in artifacts.structs] + + body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + ] + + if struct_names: + body.append(cst.EmptyLine()) + body.append(_stmt(_format_import("._generated", struct_names))) + + struct_originals = {struct.name for struct in artifacts.structs} + alias_names, alias_statements = _collect_named_aliases( + artifacts, + export_bindings, + allowed_originals=struct_originals, + ) + + if alias_statements: + body.append(cst.EmptyLine()) + for stmt in alias_statements: + body.append(_stmt(stmt)) + + export_names = _unique_preserve_order(struct_names + alias_names) + body.append(cst.EmptyLine()) + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] + ) + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], + value=all_list, + ) + ] + ) + ) + + module = cst.Module(body=header + body) # type: ignore[arg-type] + path.write_text(module.code, encoding="utf-8") + + +def write_package_structs_stub(path: Path, *, artifacts: ModuleArtifacts) -> None: + def _stmt(code: str) -> cst.BaseStatement: + return cst.parse_statement(code) + + export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) + struct_names = [struct.py_name for struct in artifacts.structs] + + body: list[cst.CSTNode] = [ + _stmt("from __future__ import annotations"), + ] - body = preamble + post_body + if struct_names: + body.append(cst.EmptyLine()) + body.append(_stmt(_format_import("._generated", struct_names))) + + struct_originals = {struct.name for struct in artifacts.structs} + alias_names, alias_statements = _collect_named_aliases( + artifacts, + export_bindings, + allowed_originals=struct_originals, + ) + + if alias_statements: + body.append(cst.EmptyLine()) + for stmt in alias_statements: + body.append(_stmt(stmt)) + + export_names = _unique_preserve_order(struct_names + alias_names) + body.append(cst.EmptyLine()) + all_list = cst.List( + elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] + ) + body.append( + cst.SimpleStatementLine( + [ + cst.Assign( + targets=[cst.AssignTarget(cst.Name("__all__"))], + value=all_list, + ) + ] + ) + ) module = cst.Module(body=body) # type: ignore[arg-type] path.write_text(module.code, encoding="utf-8") @@ -568,4 +978,10 @@ def format_callable_annotation(callback: "CallbackMeta") -> str: "module_relative_path_expr", "write_python_module", "write_stub_module", + "write_package_init", + "write_package_init_stub", + "write_package_enums", + "write_package_enums_stub", + "write_package_structs", + "write_package_structs_stub", ] diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index 15a36fa499a..66c30a6f514 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -3,11 +3,12 @@ from __future__ import annotations +import enum import shutil +from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING, Iterable -from .. import core as _core from ..api import _normalize_prop from ..core import Brush, Color, CompilationResult, Compiler, DiagnosticLevel, Image from .emitters import write_python_module, write_stub_module @@ -74,27 +75,27 @@ def copy_slint_file(source: Path, destination: Path) -> None: failed_files.append(relative) continue - artifacts = _collect_metadata(compilation) + source_descriptor = str(relative) + artifacts = _collect_metadata(compilation, source_descriptor) sanitized_stem = _normalize_prop(source_path.stem) if output_dir is None: - module_dir = source_path.parent - target_stem = module_dir / sanitized_stem - copy_slint = False + base_dir = source_path.parent slint_destination = source_path resource_name = source_path.name source_descriptor = source_path.name + copy_slint = False else: - module_dir = output_dir / relative.parent - module_dir.mkdir(parents=True, exist_ok=True) - _ensure_package_marker(module_dir) - target_stem = module_dir / sanitized_stem - copy_slint = True - slint_destination = module_dir / relative.name + base_dir = output_dir / relative.parent + base_dir.mkdir(parents=True, exist_ok=True) + _ensure_package_marker(base_dir) + slint_destination = base_dir / relative.name resource_name = relative.name source_descriptor = str(relative) + copy_slint = True + target_stem = base_dir / sanitized_stem write_python_module( target_stem.with_suffix(".py"), source_relative=source_descriptor, @@ -215,8 +216,13 @@ def is_error(diag: PyDiagnostic) -> bool: return result -def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: +def _collect_metadata(result: CompilationResult, source_descriptor: str) -> ModuleArtifacts: components: list[ComponentMeta] = [] + used_enum_class_names: set[str] = set() + component_enum_props: dict[str, dict[str, str]] = defaultdict(dict) + global_enum_props: dict[tuple[str, str], dict[str, str]] = defaultdict(dict) + struct_enum_fields: dict[str, dict[str, str]] = defaultdict(dict) + used_enum_class_names: set[str] = set() for name in result.component_names: comp = result.component(name) @@ -302,6 +308,31 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: ) ) + try: + instance = comp.create() + except Exception: + instance = None + + if instance is not None: + for prop_name in comp.properties.keys(): + try: + value = instance.get_property(prop_name) + except Exception: + continue + if isinstance(value, enum.Enum): + used_enum_class_names.add(value.__class__.__name__) + component_enum_props[name][prop_name] = value.__class__.__name__ + + for global_meta in globals_meta: + for prop in global_meta.properties: + try: + value = instance.get_global_property(global_meta.name, prop.name) + except Exception: + continue + if isinstance(value, enum.Enum): + used_enum_class_names.add(value.__class__.__name__) + global_enum_props[(name, global_meta.name)][prop.name] = value.__class__.__name__ + structs_meta: list[StructMeta] = [] enums_meta: list[EnumMeta] = [] structs, enums = result.structs_and_enums @@ -309,6 +340,9 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: for struct_name, struct_prototype in structs.items(): fields: list[StructFieldMeta] = [] for field_name, value in struct_prototype: + if isinstance(value, enum.Enum): + used_enum_class_names.add(value.__class__.__name__) + struct_enum_fields[struct_name][field_name] = value.__class__.__name__ fields.append( StructFieldMeta( name=field_name, @@ -321,6 +355,7 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: name=struct_name, py_name=_normalize_prop(struct_name), fields=fields, + is_builtin=False, ) ) @@ -332,19 +367,65 @@ def _collect_metadata(result: CompilationResult) -> ModuleArtifacts: name=member, py_name=_normalize_prop(member), value=enum_member.name, - ) ) - core_enum = getattr(_core, enum_name, None) - is_builtin = core_enum is enum_cls + ) + + is_used = enum_name in used_enum_class_names + enums_meta.append( EnumMeta( name=enum_name, py_name=_normalize_prop(enum_name), values=values, - is_builtin=is_builtin, + is_builtin=False, + is_used=is_used, ) ) + enum_py_name_map = {enum.name: enum.py_name for enum in enums_meta if not enum.is_builtin} + + for component_meta in components: + overrides = component_enum_props.get(component_meta.name, {}) + for prop in component_meta.properties: + class_name = overrides.get(prop.name) + if class_name: + if class_name in enum_py_name_map: + prop.type_hint = enum_py_name_map[class_name] + else: + print( + f"warning: {source_descriptor}: property '{prop.name}' of component '{component_meta.name}' " + f"relies on enum '{class_name}' that is not declared in this module; falling back to Any", + ) + prop.type_hint = "Any" + + for global_meta in component_meta.globals: + overrides = global_enum_props.get((component_meta.name, global_meta.name), {}) + for prop in global_meta.properties: + class_name = overrides.get(prop.name) + if class_name: + if class_name in enum_py_name_map: + prop.type_hint = enum_py_name_map[class_name] + else: + print( + f"warning: {source_descriptor}: global '{global_meta.name}.{prop.name}' relies on enum '{class_name}' " + "that is not declared in this module; falling back to Any", + ) + prop.type_hint = "Any" + + for struct_meta in structs_meta: + overrides = struct_enum_fields.get(struct_meta.name, {}) + for field in struct_meta.fields: + class_name = overrides.get(field.name) + if class_name: + if class_name in enum_py_name_map: + field.type_hint = enum_py_name_map[class_name] + else: + print( + f"warning: {source_descriptor}: struct '{struct_meta.name}.{field.name}' relies on enum '{class_name}' " + "that is not declared in this module; falling back to Any", + ) + field.type_hint = "Any" + named_exports = [(orig, alias) for orig, alias in result.named_exports] resource_paths = [Path(path) for path in result.resource_paths] diff --git a/api/python/slint/slint/codegen/models.py b/api/python/slint/slint/codegen/models.py index da4cfa37da3..4b827ed8146 100644 --- a/api/python/slint/slint/codegen/models.py +++ b/api/python/slint/slint/codegen/models.py @@ -63,6 +63,7 @@ class StructMeta: name: str py_name: str fields: List[StructFieldMeta] + is_builtin: bool @dataclass(slots=True) @@ -78,6 +79,7 @@ class EnumMeta: py_name: str values: List[EnumValueMeta] is_builtin: bool + is_used: bool @dataclass(slots=True) diff --git a/api/python/slint/tests/codegen/examples/counter/counter.py b/api/python/slint/tests/codegen/examples/counter/counter.py index e98f7e62fb2..0614ac6d045 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.py +++ b/api/python/slint/tests/codegen/examples/counter/counter.py @@ -1,6 +1,3 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - # Generated by slint.codegen from counter.slint from __future__ import annotations @@ -13,12 +10,11 @@ import slint -__all__ = ["CounterWindow"] +__all__ = ['CounterWindow'] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = "counter.slint" - +_SLINT_RESOURCE = 'counter.slint' def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -39,7 +35,6 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) - _module = _load() CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/counter.slint b/api/python/slint/tests/codegen/examples/counter/counter.slint index c28ccb2e8b0..668fd9ad3a4 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.slint +++ b/api/python/slint/tests/codegen/examples/counter/counter.slint @@ -6,6 +6,8 @@ export component CounterWindow inherits Window { height: 120px; in-out property counter: 0; + // text alignment + in-out property alignment: TextHorizontalAlignment.center; callback request_increase(); Rectangle { diff --git a/api/python/slint/tests/codegen/examples/counter/main.py b/api/python/slint/tests/codegen/examples/counter/main.py index 7aee25a8a14..d733d8c3c6a 100644 --- a/api/python/slint/tests/codegen/examples/counter/main.py +++ b/api/python/slint/tests/codegen/examples/counter/main.py @@ -20,11 +20,8 @@ def request_increase(self) -> None: self.counter += 1 -def main() -> None: +if __name__ == "__main__": app = CounterApp() app.show() + # slint.run_event_loop_blocking() app.run() - - -if __name__ == "__main__": - main() diff --git a/api/python/slint/tests/codegen/examples/counter/test.py b/api/python/slint/tests/codegen/examples/counter/test.py new file mode 100644 index 00000000000..a629bea7f90 --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/test.py @@ -0,0 +1,45 @@ +# Generated by slint.codegen from test.slint +from __future__ import annotations + +import importlib.resources as _resources +import os +import types +from contextlib import nullcontext as _nullcontext +from pathlib import Path +from typing import Any + +import slint + +__all__ = ['OptionalDemo', 'OptionalFloat', 'OptionalBool', 'OptionalInt', 'OptionalString', 'OptionalEnum'] + +_MODULE_DIR = Path(__file__).parent + +_SLINT_RESOURCE = 'test.slint' + +def _load() -> types.SimpleNamespace: + """Load the compiled Slint module for this package.""" + package = __package__ or (__spec__.parent if __spec__ else None) + if package: + ctx = _resources.as_file(_resources.files(package).joinpath(_SLINT_RESOURCE)) + else: + ctx = _nullcontext(Path(__file__).with_name(_SLINT_RESOURCE)) + with ctx as slint_path: + include_paths: list[os.PathLike[Any] | Path] | None = None + library_paths: dict[str, os.PathLike[Any] | Path] | None = None + return slint.load_file( + path=slint_path, + quiet=True, + style=None, + include_paths=include_paths, + library_paths=library_paths, + translation_domain=None, + ) + +_module = _load() + +OptionalDemo = _module.OptionalDemo +OptionalFloat = _module.OptionalFloat +OptionalBool = _module.OptionalBool +OptionalInt = _module.OptionalInt +OptionalString = _module.OptionalString +OptionalEnum = _module.OptionalEnum diff --git a/api/python/slint/tests/codegen/examples/counter/test.pyi b/api/python/slint/tests/codegen/examples/counter/test.pyi new file mode 100644 index 00000000000..db00f2ff07c --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/test.pyi @@ -0,0 +1,40 @@ +from __future__ import annotations + +import enum +from typing import ( + Any, + Callable, + Optional, +) + +import slint + +__all__ = ['OptionalDemo', 'OptionalFloat', 'OptionalBool', 'OptionalInt', 'OptionalString', 'OptionalEnum'] + +class OptionalFloat: + def __init__(self, *, maybe_value: float = ...) -> None: ... + maybe_value: float + +class OptionalBool: + def __init__(self, *, maybe_value: bool = ...) -> None: ... + maybe_value: bool + +class OptionalInt: + def __init__(self, *, maybe_value: float = ...) -> None: ... + maybe_value: float + +class OptionalString: + def __init__(self, *, maybe_value: str = ...) -> None: ... + maybe_value: str + +class OptionalEnum(enum.Enum): + OptionA = 'OptionA' + OptionB = 'OptionB' + OptionC = 'OptionC' + +class OptionalDemo(slint.Component): + def __init__(self, **kwargs: Any) -> None: ... + maybe_count: Optional[int] + on_action: Callable[[Optional[float]], Optional[int]] + compute: Callable[[Optional[str]], Optional[bool]] + diff --git a/api/python/slint/tests/codegen/examples/counter/test.slint b/api/python/slint/tests/codegen/examples/counter/test.slint new file mode 100644 index 00000000000..96c0718b53c --- /dev/null +++ b/api/python/slint/tests/codegen/examples/counter/test.slint @@ -0,0 +1,32 @@ +export struct OptionalInt { + maybe_value: int, +} + +export struct OptionalFloat { + maybe_value: float, +} + +export struct OptionalString { + maybe_value: string, +} + +export struct OptionalBool { + maybe_value: bool, +} + +export enum OptionalEnum { + OptionA, + OptionB, + OptionC, +} + + + +export component OptionalDemo { + in-out property maybe_count; + callback on_action(value: OptionalFloat) -> OptionalInt; + + public function compute(input: OptionalString) -> OptionalBool { + return { maybe_value: true }; + } +} \ No newline at end of file diff --git a/api/python/slint/tests/codegen/test_generator.py b/api/python/slint/tests/codegen/test_generator.py index a4b86d798b4..796b4af090f 100644 --- a/api/python/slint/tests/codegen/test_generator.py +++ b/api/python/slint/tests/codegen/test_generator.py @@ -41,6 +41,7 @@ def _write_slint_fixture(target_dir: Path) -> Path: width: 160px; height: 80px; in-out property counter: SharedLogic.total; + in-out property alignment: TextHorizontalAlignment.center; callback activated(int); public function reset() -> int { @@ -50,6 +51,7 @@ def _write_slint_fixture(target_dir: Path) -> Path: Text { text: counter; + horizontal-alignment: alignment; } } """ @@ -143,16 +145,27 @@ def test_generate_project_creates_runtime_and_stub(tmp_path: Path) -> None: assert "class Choice" in stub_src assert "class SharedLogic" in stub_src assert "counter: int" in stub_src + assert "TextHorizontalAlignment" not in stub_src + assert "alignment: Any" in stub_src assert "activated: Callable[[int], None]" in stub_src assert "reset: Callable[[], int]" in stub_src assert "notify: Callable[[int], None]" in stub_src - assert "from typing import Any, Callable" in stub_src + assert "from typing import" in stub_src + assert "Any" in stub_src + assert "Callable" in stub_src + assert "def __init__(self, *, " in stub_src + assert "value:" in stub_src + assert "label:" in stub_src module = _load_module(py_file) assert hasattr(module, "AppWindow") instance = module.AppWindow() assert hasattr(instance, "show") assert instance.reset() == 0 + alignment = instance.alignment + alignment_type = alignment.__class__ + assert alignment_type.__name__ == "TextHorizontalAlignment" + assert alignment == alignment_type.center config_struct = module.Config(value=5, label="demo") assert config_struct.value == 5 @@ -196,7 +209,10 @@ def test_generate_optional_type_hints(tmp_path: Path) -> None: assert "maybe_count: Optional[int]" in stub_src assert "on_action: Callable[[Optional[float]], Optional[int]]" in stub_src assert "compute: Callable[[Optional[str]], Optional[bool]]" in stub_src - assert "from typing import Any, Callable, Optional" in stub_src + assert "from typing import" in stub_src + assert "Any" in stub_src + assert "Callable" in stub_src + assert "Optional" in stub_src def test_cli_main_without_subcommand(tmp_path: Path) -> None: @@ -222,9 +238,20 @@ def test_counter_example_workflow(tmp_path: Path) -> None: subprocess.run([sys.executable, "generate.py"], cwd=example_copy, check=True) generated_py = example_copy / "counter.py" assert generated_py.exists() + package_init = tmp_path / "examples" / "__init__.py" + package_init.parent.mkdir(parents=True, exist_ok=True) + package_init.touch() sys.path.insert(0, str(tmp_path)) try: + for name in [ + "examples.counter.main", + "examples.counter.counter", + "examples.counter", + "examples", + ]: + sys.modules.pop(name, None) + assert importlib.util.find_spec("examples.counter.counter") is not None module = importlib.import_module("examples.counter.main") finally: sys.path.pop(0) diff --git a/api/python/slint/tests/test_callback_decorators.py b/api/python/slint/tests/test_callback_decorators.py index 43fd4c1d96a..896d29f6999 100644 --- a/api/python/slint/tests/test_callback_decorators.py +++ b/api/python/slint/tests/test_callback_decorators.py @@ -17,7 +17,7 @@ def base_dir() -> Path: def test_callback_decorators(caplog: pytest.LogCaptureFixture) -> None: - module = load_file(base_dir() / "test-load-file.slint", quiet=False) + module = load_file(base_dir() / "test-load-file-source.slint", quiet=False) class SubClass(module.App): # type: ignore @slint.callback() @@ -40,7 +40,7 @@ def global_callback(self, arg: str) -> str: def test_callback_decorators_async() -> None: - module = load_file(base_dir() / "test-load-file.slint", quiet=False) + module = load_file(base_dir() / "test-load-file-source.slint", quiet=False) class SubClass(module.App): # type: ignore def __init__(self, in_queue: asyncio.Queue[int], out_queue: asyncio.Queue[int]): @@ -68,7 +68,7 @@ async def main( def test_callback_decorators_async_err() -> None: - module = load_file(base_dir() / "test-load-file.slint", quiet=False) + module = load_file(base_dir() / "test-load-file-source.slint", quiet=False) class SubClass(module.App): # type: ignore def __init__(self) -> None: diff --git a/api/python/slint/tests/test_compiler.py b/api/python/slint/tests/test_compiler.py index f41f869916d..c305f31d4bc 100644 --- a/api/python/slint/tests/test_compiler.py +++ b/api/python/slint/tests/test_compiler.py @@ -63,7 +63,7 @@ def test_basic_compiler() -> None: assert compdef.globals == ["TestGlobal"] - assert compdef.global_properties("Garbage") is None + assert compdef.global_properties("Garbage") == {} assert [ (name, type) for name, type in compdef.global_properties("TestGlobal").items() ] == [("theglobalprop", ValueType.String)] diff --git a/api/python/slint/tests/test_enums.py b/api/python/slint/tests/test_enums.py index 4e7b86a68b9..29664041d49 100644 --- a/api/python/slint/tests/test_enums.py +++ b/api/python/slint/tests/test_enums.py @@ -5,6 +5,7 @@ import importlib.util import sys +import inspect from pathlib import Path from typing import Any @@ -12,7 +13,6 @@ from slint import ListModel, core from slint.codegen.generator import generate_project from slint.codegen.models import GenerationConfig -from slint.core import TextHorizontalAlignment, TextVerticalAlignment def _slint_source() -> Path: @@ -83,15 +83,9 @@ def test_enums(generated_module: Any) -> None: del instance -def test_builtin_enums_exposed() -> None: - assert TextHorizontalAlignment.left.name == "left" - assert TextVerticalAlignment.top.name == "top" - assert TextHorizontalAlignment.left != TextHorizontalAlignment.right - - def test_builtin_enum_property_roundtrip() -> None: compiler = core.Compiler() - comp = compiler.build_from_source( + result = compiler.build_from_source( """ export component Test { in-out property horizontal: TextHorizontalAlignment.left; @@ -103,29 +97,104 @@ def test_builtin_enum_property_roundtrip() -> None: } """, Path(""), - ).component("Test") + ) + comp = result.component("Test") assert comp is not None + + _, enums = result.structs_and_enums + assert "TextHorizontalAlignment" not in enums + assert "TextVerticalAlignment" not in enums + instance = comp.create() assert instance is not None - assert instance.get_property("horizontal") == TextHorizontalAlignment.left - assert instance.get_property("vertical") == TextVerticalAlignment.top + horizontal = instance.get_property("horizontal") + vertical = instance.get_property("vertical") - instance.set_property("horizontal", TextHorizontalAlignment.right) - instance.set_property("vertical", TextVerticalAlignment.bottom) + horizontal_cls = horizontal.__class__ + vertical_cls = vertical.__class__ - assert instance.get_property("horizontal") == TextHorizontalAlignment.right - assert instance.get_property("vertical") == TextVerticalAlignment.bottom + assert horizontal_cls.__name__ == "TextHorizontalAlignment" + assert vertical_cls.__name__ == "TextVerticalAlignment" + assert horizontal == horizontal_cls.left + assert vertical == vertical_cls.top + + instance.set_property("horizontal", horizontal_cls.right) + instance.set_property("vertical", vertical_cls.bottom) + + assert instance.get_property("horizontal") == horizontal_cls.right + assert instance.get_property("vertical") == vertical_cls.bottom def test_builtin_enum_keyword_variants_have_safe_names() -> None: - keyword_enums = ( - core.AccessibleRole, - core.DialogButtonRole, + compiler = core.Compiler() + result = compiler.build_from_source( + """ + export component Test { + in-out property role: AccessibleRole.none; + in-out property button_role: DialogButtonRole.none; + } + """, + Path(""), ) - for enum_cls in keyword_enums: + _, enums = result.structs_and_enums + assert "AccessibleRole" not in enums + assert "DialogButtonRole" not in enums + + comp = result.component("Test") + assert comp is not None + instance = comp.create() + assert instance is not None + + for prop_name in ("role", "button_role"): + enum_value = instance.get_property(prop_name) + enum_cls = enum_value.__class__ members = enum_cls.__members__ assert "none" in members assert members["none"].value == "none" + + +def test_user_enum_exported_and_builtin_hidden() -> None: + source = inspect.cleandoc( + """ + export struct Custom { + value: int, + } + + export enum CustomEnum { + first, + second, + } + + export global Data { + in-out property custom; + } + + export component Test inherits Window { + in-out property data <=> Data.custom; + in-out property mode; + callback pointer_event(event: PointerEvent); + width: 100px; + height: 100px; + TouchArea { } + } + """ + ) + + compiler = core.Compiler() + result = compiler.build_from_source(source, Path("")) + + structs, enums = result.structs_and_enums + assert "CustomEnum" in enums + assert "PointerEventButton" not in enums + + component = result.component("Test") + assert component is not None + instance = component.create() + assert instance is not None + + CustomEnum = enums["CustomEnum"] + instance.set_property("mode", CustomEnum.second) + assert instance.get_property("mode") == CustomEnum.second diff --git a/api/python/slint/tests/test_load_file_source.py b/api/python/slint/tests/test_load_file_source.py index 121bb3f9251..a5a50a25b7c 100644 --- a/api/python/slint/tests/test_load_file_source.py +++ b/api/python/slint/tests/test_load_file_source.py @@ -1,6 +1,3 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - # Generated by slint.codegen from test-load-file-source.slint from __future__ import annotations @@ -13,50 +10,11 @@ import slint -__all__ = [ - "Diag", - "App", - "Secret_Struct", - "MyData", - "ImageFit", - "TextHorizontalAlignment", - "SortOrder", - "PointerEventKind", - "ImageRendering", - "ImageTiling", - "OperatingSystemType", - "InputType", - "Orientation", - "TextWrap", - "ScrollBarPolicy", - "ImageVerticalAlignment", - "PointerEventButton", - "TextOverflow", - "LayoutAlignment", - "StandardButtonKind", - "AccessibleRole", - "EventResult", - "MouseCursor", - "AnimationDirection", - "TextVerticalAlignment", - "TextStrokeStyle", - "LineCap", - "ImageHorizontalAlignment", - "FocusReason", - "FillRule", - "ColorScheme", - "PathEvent", - "TestEnum", - "DialogButtonRole", - "PopupClosePolicy", - "MyDiag", - "Public_Struct", -] +__all__ = ['Diag', 'App', 'MyData', 'Secret_Struct', 'TestEnum', 'MyDiag', 'Public_Struct'] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = "test-load-file-source.slint" - +_SLINT_RESOURCE = 'test-load-file-source.slint' def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -77,43 +35,12 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) - _module = _load() Diag = _module.Diag App = _module.App -Secret_Struct = _module.Secret_Struct MyData = _module.MyData -ImageFit = _module.ImageFit -TextHorizontalAlignment = _module.TextHorizontalAlignment -SortOrder = _module.SortOrder -PointerEventKind = _module.PointerEventKind -ImageRendering = _module.ImageRendering -ImageTiling = _module.ImageTiling -OperatingSystemType = _module.OperatingSystemType -InputType = _module.InputType -Orientation = _module.Orientation -TextWrap = _module.TextWrap -ScrollBarPolicy = _module.ScrollBarPolicy -ImageVerticalAlignment = _module.ImageVerticalAlignment -PointerEventButton = _module.PointerEventButton -TextOverflow = _module.TextOverflow -LayoutAlignment = _module.LayoutAlignment -StandardButtonKind = _module.StandardButtonKind -AccessibleRole = _module.AccessibleRole -EventResult = _module.EventResult -MouseCursor = _module.MouseCursor -AnimationDirection = _module.AnimationDirection -TextVerticalAlignment = _module.TextVerticalAlignment -TextStrokeStyle = _module.TextStrokeStyle -LineCap = _module.LineCap -ImageHorizontalAlignment = _module.ImageHorizontalAlignment -FocusReason = _module.FocusReason -FillRule = _module.FillRule -ColorScheme = _module.ColorScheme -PathEvent = _module.PathEvent +Secret_Struct = _module.Secret_Struct TestEnum = _module.TestEnum -DialogButtonRole = _module.DialogButtonRole -PopupClosePolicy = _module.PopupClosePolicy MyDiag = Diag Public_Struct = Secret_Struct diff --git a/api/python/slint/tests/test_load_file_source.pyi b/api/python/slint/tests/test_load_file_source.pyi index bb9296df7d7..b2e988097d8 100644 --- a/api/python/slint/tests/test_load_file_source.pyi +++ b/api/python/slint/tests/test_load_file_source.pyi @@ -1,283 +1,26 @@ -# Copyright © SixtyFPS GmbH -# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 - from __future__ import annotations import enum - -from typing import Any, Callable - +from typing import ( + Any, + Callable, +) import slint -__all__ = [ - "Diag", - "App", - "Secret_Struct", - "MyData", - "ImageFit", - "TextHorizontalAlignment", - "SortOrder", - "PointerEventKind", - "ImageRendering", - "ImageTiling", - "OperatingSystemType", - "InputType", - "Orientation", - "TextWrap", - "ScrollBarPolicy", - "ImageVerticalAlignment", - "PointerEventButton", - "TextOverflow", - "LayoutAlignment", - "StandardButtonKind", - "AccessibleRole", - "EventResult", - "MouseCursor", - "AnimationDirection", - "TextVerticalAlignment", - "TextStrokeStyle", - "LineCap", - "ImageHorizontalAlignment", - "FocusReason", - "FillRule", - "ColorScheme", - "PathEvent", - "TestEnum", - "DialogButtonRole", - "PopupClosePolicy", - "MyDiag", - "Public_Struct", -] - -class Secret_Struct: - def __init__(self, **kwargs: Any) -> None: ... - balance: float +__all__ = ['Diag', 'App', 'MyData', 'Secret_Struct', 'TestEnum', 'MyDiag', 'Public_Struct'] class MyData: - def __init__(self, **kwargs: Any) -> None: ... + def __init__(self, *, age: float = ..., name: str = ...) -> None: ... age: float name: str -class ImageFit(enum.Enum): - fill = "fill" - contain = "contain" - cover = "cover" - preserve = "preserve" - -class TextHorizontalAlignment(enum.Enum): - left = "left" - center = "center" - right = "right" - -class SortOrder(enum.Enum): - unsorted = "unsorted" - ascending = "ascending" - descending = "descending" - -class PointerEventKind(enum.Enum): - cancel = "cancel" - down = "down" - up = "up" - move = "move" - -class ImageRendering(enum.Enum): - smooth = "smooth" - pixelated = "pixelated" - -class ImageTiling(enum.Enum): - none = "none" - repeat = "repeat" - round = "round" - -class OperatingSystemType(enum.Enum): - android = "android" - ios = "ios" - macos = "macos" - linux = "linux" - windows = "windows" - other = "other" - -class InputType(enum.Enum): - text = "text" - password = "password" - number = "number" - decimal = "decimal" - -class Orientation(enum.Enum): - horizontal = "horizontal" - vertical = "vertical" - -class TextWrap(enum.Enum): - nowrap = "nowrap" - wordwrap = "wordwrap" - charwrap = "charwrap" - -class ScrollBarPolicy(enum.Enum): - asneeded = "asneeded" - alwaysoff = "alwaysoff" - alwayson = "alwayson" - -class ImageVerticalAlignment(enum.Enum): - center = "center" - top = "top" - bottom = "bottom" - -class PointerEventButton(enum.Enum): - other = "other" - left = "left" - right = "right" - middle = "middle" - back = "back" - forward = "forward" - -class TextOverflow(enum.Enum): - clip = "clip" - elide = "elide" - -class LayoutAlignment(enum.Enum): - stretch = "stretch" - center = "center" - start = "start" - end = "end" - spacebetween = "spacebetween" - spacearound = "spacearound" - spaceevenly = "spaceevenly" - -class StandardButtonKind(enum.Enum): - ok = "ok" - cancel = "cancel" - apply = "apply" - close = "close" - reset = "reset" - help = "help" - yes = "yes" - no = "no" - abort = "abort" - retry = "retry" - ignore = "ignore" - -class AccessibleRole(enum.Enum): - none = "none" - button = "button" - checkbox = "checkbox" - combobox = "combobox" - groupbox = "groupbox" - image = "image" - list = "list" - slider = "slider" - spinbox = "spinbox" - tab = "tab" - tablist = "tablist" - tabpanel = "tabpanel" - text = "text" - table = "table" - tree = "tree" - progressindicator = "progressindicator" - textinput = "textinput" - switch = "switch" - listitem = "listitem" - -class EventResult(enum.Enum): - reject = "reject" - accept = "accept" - -class MouseCursor(enum.Enum): - default = "default" - none = "none" - help = "help" - pointer = "pointer" - progress = "progress" - wait = "wait" - crosshair = "crosshair" - text = "text" - alias = "alias" - copy = "copy" - move = "move" - nodrop = "nodrop" - notallowed = "notallowed" - grab = "grab" - grabbing = "grabbing" - colresize = "colresize" - rowresize = "rowresize" - nresize = "nresize" - eresize = "eresize" - sresize = "sresize" - wresize = "wresize" - neresize = "neresize" - nwresize = "nwresize" - seresize = "seresize" - swresize = "swresize" - ewresize = "ewresize" - nsresize = "nsresize" - neswresize = "neswresize" - nwseresize = "nwseresize" - -class AnimationDirection(enum.Enum): - normal = "normal" - reverse = "reverse" - alternate = "alternate" - alternatereverse = "alternatereverse" - -class TextVerticalAlignment(enum.Enum): - top = "top" - center = "center" - bottom = "bottom" - -class TextStrokeStyle(enum.Enum): - outside = "outside" - center = "center" - -class LineCap(enum.Enum): - butt = "butt" - round = "round" - square = "square" - -class ImageHorizontalAlignment(enum.Enum): - center = "center" - left = "left" - right = "right" - -class FocusReason(enum.Enum): - programmatic = "programmatic" - tabnavigation = "tabnavigation" - pointerclick = "pointerclick" - popupactivation = "popupactivation" - windowactivation = "windowactivation" - -class FillRule(enum.Enum): - nonzero = "nonzero" - evenodd = "evenodd" - -class ColorScheme(enum.Enum): - unknown = "unknown" - dark = "dark" - light = "light" - -class PathEvent(enum.Enum): - begin = "begin" - line = "line" - quadratic = "quadratic" - cubic = "cubic" - endopen = "endopen" - endclosed = "endclosed" +class Secret_Struct: + def __init__(self, *, balance: float = ...) -> None: ... + balance: float class TestEnum(enum.Enum): - Variant1 = "Variant1" - Variant2 = "Variant2" - -class DialogButtonRole(enum.Enum): - none = "none" - accept = "accept" - reject = "reject" - apply = "apply" - reset = "reset" - help = "help" - action = "action" - -class PopupClosePolicy(enum.Enum): - closeonclick = "closeonclick" - closeonclickoutside = "closeonclickoutside" - noautoclose = "noautoclose" + Variant1 = 'Variant1' + Variant2 = 'Variant2' class Diag(slint.Component): def __init__(self, **kwargs: Any) -> None: ... @@ -285,14 +28,13 @@ class Diag(slint.Component): global_prop: str global_callback: Callable[[str], str] minus_one: Callable[[int], None] - class SecondGlobal: second: str class App(slint.Component): def __init__(self, **kwargs: Any) -> None: ... builtin_enum: Any - enum_property: Any + enum_property: TestEnum hello: str model_with_enums: slint.ListModel[Any] translated: str @@ -308,10 +50,10 @@ class App(slint.Component): global_prop: str global_callback: Callable[[str], str] minus_one: Callable[[int], None] - class SecondGlobal: second: str MyDiag = Diag Public_Struct = Secret_Struct + diff --git a/api/python/slint/tests/test_loop.py b/api/python/slint/tests/test_loop.py index 2f7621a0368..154074b5a39 100644 --- a/api/python/slint/tests/test_loop.py +++ b/api/python/slint/tests/test_loop.py @@ -1,12 +1,16 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -import slint -from slint import core +import asyncio from datetime import timedelta -import pytest import sys +import pytest + +import slint +import slint.api +from slint import core + def test_sysexit_exception() -> None: def call_sys_exit() -> None: @@ -19,3 +23,41 @@ def call_sys_exit() -> None: "unexpected failure running python singleshot timer callback" in exc_info.value.__notes__ ) + + +def test_quit_event_loop_calls_core(monkeypatch: pytest.MonkeyPatch) -> None: + toggle = False + + def fake_quit() -> None: + nonlocal toggle + toggle = True + + monkeypatch.setattr(core, "quit_event_loop", fake_quit) + monkeypatch.setattr(core, "invoke_from_event_loop", lambda cb: cb()) + + slint.api.quit_event = asyncio.Event() + + slint.quit_event_loop() + + assert toggle is True + assert slint.api.quit_event.is_set() + + +def test_quit_event_loop_falls_back_when_invoke_fails(monkeypatch: pytest.MonkeyPatch) -> None: + toggle = False + + def fake_quit() -> None: + nonlocal toggle + toggle = True + + def fake_invoke(cb): + raise RuntimeError("invoke unavailable") + + monkeypatch.setattr(core, "quit_event_loop", fake_quit) + monkeypatch.setattr(core, "invoke_from_event_loop", fake_invoke) + + slint.api.quit_event = asyncio.Event() + + slint.quit_event_loop() + + assert toggle is True diff --git a/api/python/slint/tests/test_sigint.py b/api/python/slint/tests/test_sigint.py new file mode 100644 index 00000000000..41900ee2f5d --- /dev/null +++ b/api/python/slint/tests/test_sigint.py @@ -0,0 +1,22 @@ +import signal +import threading +import time + +import pytest + +import slint + + +def test_run_event_loop_handles_sigint(): + def trigger_sigint() -> None: + # Allow the event loop to start before raising the signal. + time.sleep(0.1) + signal.raise_signal(signal.SIGINT) + + sender = threading.Thread(target=trigger_sigint) + sender.start() + try: + with pytest.raises(KeyboardInterrupt): + slint.run_event_loop() + finally: + sender.join() diff --git a/api/python/slint/tests/test_structs.py b/api/python/slint/tests/test_structs.py index b25b961a736..5eea3d8b0eb 100644 --- a/api/python/slint/tests/test_structs.py +++ b/api/python/slint/tests/test_structs.py @@ -1,28 +1,122 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -from slint.core import ( - KeyboardModifiers, - PointerEvent, - PointerEventButton, - PointerEventKind, -) - - -def test_keyboard_modifiers_ctor() -> None: - mods = KeyboardModifiers(control=True) - assert mods.control is True - assert mods.alt is False - - -def test_pointer_event_ctor_returns_struct() -> None: - mods = KeyboardModifiers(alt=True) - event = PointerEvent( - button=PointerEventButton.left, - kind=PointerEventKind.down, - modifiers=mods, +from __future__ import annotations + +import importlib.util +import inspect +import sys +from pathlib import Path + +import pytest + +import slint.core as core +from slint.codegen.generator import generate_project +from slint.codegen.models import GenerationConfig + + +def test_builtin_enum_class_helper_not_available() -> None: + assert not hasattr(core, "built_in_enum_class") + with pytest.raises(AttributeError): + getattr(core, "built_in_enum_class") + + +def test_builtin_struct_factory_not_available() -> None: + assert not hasattr(core, "built_in_struct_factory") + with pytest.raises(AttributeError): + getattr(core, "built_in_struct_factory") + + +def test_user_structs_exported_and_builtin_hidden() -> None: + source = inspect.cleandoc( + """ + export struct Custom { + value: int, + } + + export enum CustomEnum { + first, + second, + } + + export global Data { + in-out property custom; + } + + export component Test inherits Window { + in-out property data <=> Data.custom; + in-out property mode; + callback pointer_event(event: PointerEvent); + width: 100px; + height: 100px; + TouchArea { } + } + """ ) - assert event.button == PointerEventButton.left - assert event.kind == PointerEventKind.down - assert event.modifiers.alt is True + compiler = core.Compiler() + result = compiler.build_from_source(source, Path("")) + + structs, enums = result.structs_and_enums + assert set(structs.keys()) == {"Custom"} + assert "PointerEvent" not in structs + assert set(enums.keys()) == {"CustomEnum"} + + custom_struct_proto = structs["Custom"] + assert hasattr(custom_struct_proto, "value") + + component = result.component("Test") + assert component is not None + instance = component.create() + assert instance is not None + + instance.set_property("data", {"value": 99}) + data = instance.get_property("data") + assert hasattr(data, "value") + assert data.value == 99 + + CustomEnum = enums["CustomEnum"] + instance.set_property("mode", CustomEnum.second) + assert instance.get_property("mode") == CustomEnum.second + + +@pytest.fixture +def generated_struct_module(tmp_path: Path): + slint_file = Path(__file__).with_name("test-load-file-source.slint") + output_dir = tmp_path / "generated" + config = GenerationConfig( + include_paths=[slint_file.parent], + library_paths={}, + style=None, + translation_domain=None, + quiet=True, + ) + + generate_project(inputs=[slint_file], output_dir=output_dir, config=config) + + module_path = output_dir / "test_load_file_source.py" + spec = importlib.util.spec_from_file_location("generated_structs", module_path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules.pop(spec.name, None) + sys.modules[spec.name] = module + spec.loader.exec_module(module) # type: ignore[arg-type] + return module + + +def test_struct_accepts_keywords_only(generated_struct_module) -> None: + MyData = generated_struct_module.MyData + + with pytest.raises(TypeError, match="keyword arguments only"): + MyData("foo", 42) + + instance = MyData(name="foo", age=42) + assert instance.name == "foo" + assert instance.age == 42 + + +def test_struct_rejects_unknown_keywords(generated_struct_module) -> None: + MyData = generated_struct_module.MyData + + with pytest.raises(TypeError, match="unexpected keyword"): # noqa: PT012 + MyData(name="foo", age=1, extra=True) diff --git a/api/python/slint/tests/test_translations.py b/api/python/slint/tests/test_translations.py index 6f0831e60b2..01015377053 100644 --- a/api/python/slint/tests/test_translations.py +++ b/api/python/slint/tests/test_translations.py @@ -26,7 +26,7 @@ def pgettext(self, context: str, message: str) -> str: def test_load_file() -> None: - module = load_file(base_dir() / "test-load-file.slint") + module = load_file(base_dir() / "test-load-file-source.slint") testcase = module.App() diff --git a/api/python/slint/value.rs b/api/python/slint/value.rs index 61055b52147..f23985c469d 100644 --- a/api/python/slint/value.rs +++ b/api/python/slint/value.rs @@ -1,6 +1,7 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 +use pyo3::sync::PyOnceLock; use pyo3::types::PyDict; use pyo3::{prelude::*, PyVisit}; use pyo3::{IntoPyObjectExt, PyTraverseError}; @@ -186,6 +187,73 @@ thread_local! { static ENUM_CLASS: OnceCell> = OnceCell::new(); } +struct EnumClassInfo { + class: Py, + is_builtin: bool, +} + +static BUILTIN_ENUM_CLASSES: PyOnceLock>> = PyOnceLock::new(); + +macro_rules! generate_enum_support { + ($( + $(#[$enum_attr:meta])* + enum $Name:ident { + $( + $(#[$value_attr:meta])* + $Value:ident, + )* + } + )*) => { + fn build_built_in_enums( + enum_base: &Bound<'_, PyAny>, + ) -> PyResult>> { + let mut enum_classes = HashMap::new(); + + $( + { + let name = stringify!($Name); + let variants = vec![ + $( + { + let value = i_slint_core::items::$Name::$Value.to_string(); + (stringify!($Value).to_ascii_lowercase(), value) + }, + )* + ]; + + let cls = enum_base.call((name, variants), None)?; + enum_classes.insert(name.to_string(), cls.unbind()); + } + )* + + Ok(enum_classes) + } + }; +} + +i_slint_common::for_each_enums!(generate_enum_support); + +fn ensure_builtin_enums_initialized(py: Python<'_>) -> PyResult<()> { + if BUILTIN_ENUM_CLASSES.get(py).is_some() { + return Ok(()); + } + + let enum_base = enum_class(py).into_bound(py); + let classes = build_built_in_enums(&enum_base)?; + + let _ = BUILTIN_ENUM_CLASSES.set(py, classes); + Ok(()) +} + +fn built_in_enum_classes(py: Python<'_>) -> HashMap> { + ensure_builtin_enums_initialized(py).expect("failed to initialize built-in enums"); + + BUILTIN_ENUM_CLASSES + .get(py) + .map(|map| map.iter().map(|(name, class)| (name.clone(), class.clone_ref(py))).collect()) + .unwrap_or_default() +} + pub fn enum_class(py: Python) -> Py { ENUM_CLASS.with(|cls| { cls.get_or_init(|| -> Py { @@ -201,15 +269,18 @@ pub fn enum_class(py: Python) -> Py { /// 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>>, + enum_classes: Rc>, } impl TypeCollection { pub fn new(result: &slint_interpreter::CompilationResult, py: Python<'_>) -> Self { - let mut enum_classes = crate::enums::built_in_enum_classes(py); - let enum_ctor = crate::value::enum_class(py); + let mut enum_classes: HashMap = built_in_enum_classes(py) + .into_iter() + .map(|(name, class)| (name, EnumClassInfo { class, is_builtin: true })) + .collect(); + for struct_or_enum in result.structs_and_enums(i_slint_core::InternalToken {}) { match struct_or_enum { Type::Enumeration(en) => { @@ -230,7 +301,10 @@ impl TypeCollection { ) .unwrap(); - enum_classes.insert(en.name.to_string(), enum_type); + enum_classes.insert( + en.name.to_string(), + EnumClassInfo { class: enum_type, is_builtin: en.node.is_none() }, + ); } _ => {} } @@ -240,11 +314,6 @@ impl TypeCollection { Self { enum_classes } } - pub fn with_builtin(py: Python<'_>) -> Self { - let enum_classes = Rc::new(crate::enums::built_in_enum_classes(py)); - Self { enum_classes } - } - pub fn to_py_value(&self, value: slint_interpreter::Value) -> SlintToPyValue { SlintToPyValue { slint_value: value, type_collection: self.clone() } } @@ -264,7 +333,7 @@ impl TypeCollection { "Slint provided enum {enum_name} is unknown" )) })?; - enum_cls.getattr(py, enum_value) + enum_cls.class.getattr(py, enum_value) } pub fn model_to_py( @@ -274,8 +343,10 @@ impl TypeCollection { crate::models::ReadOnlyRustModel { model: model.clone(), type_collection: self.clone() } } - pub fn enums(&self) -> impl Iterator)> { - self.enum_classes.iter() + pub fn enums(&self) -> impl Iterator)> + '_ { + self.enum_classes.iter().filter_map(|(name, info)| { + (!info.is_builtin).then_some((name, &info.class)) + }) } pub fn slint_value_from_py_value( From 40e4c47438a75e8ae95b835cca241c58eab43c36 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 19:55:02 +0800 Subject: [PATCH 38/52] refactor(python): rename HasFileno and fd_for_fileobj to protected prefix --- api/python/slint/slint/loop.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index a0dd3e4c16b..dcaf9465255 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -13,11 +13,11 @@ from . import core -class HasFileno(typing.Protocol): +class _HasFileno(typing.Protocol): def fileno(self) -> int: ... -def fd_for_fileobj(fileobj: int | HasFileno) -> int: +def _fd_for_fileobj(fileobj: int | _HasFileno) -> int: if isinstance(fileobj, int): return fileobj return int(fileobj.fileno()) @@ -31,11 +31,11 @@ def __len__(self) -> int: return len(self.slint_selector.fd_to_selector_key) def get(self, fileobj, default=None): # type: ignore - fd = fd_for_fileobj(fileobj) + fd = _fd_for_fileobj(fileobj) return self.slint_selector.fd_to_selector_key.get(fd, default) def __getitem__(self, fileobj: typing.Any) -> selectors.SelectorKey: - fd = fd_for_fileobj(fileobj) + fd = _fd_for_fileobj(fileobj) return self.slint_selector.fd_to_selector_key[fd] def __iter__(self): # type: ignore @@ -56,7 +56,7 @@ def __init__(self) -> None: def register( self, fileobj: typing.Any, events: typing.Any, data: typing.Any = None ) -> selectors.SelectorKey: - fd = fd_for_fileobj(fileobj) + fd = _fd_for_fileobj(fileobj) key = selectors.SelectorKey(fileobj, fd, events, data) self.fd_to_selector_key[fd] = key @@ -76,7 +76,7 @@ def register( return key def unregister(self, fileobj: typing.Any) -> selectors.SelectorKey: - fd = fd_for_fileobj(fileobj) + fd = _fd_for_fileobj(fileobj) key = self.fd_to_selector_key.pop(fd) try: @@ -94,7 +94,7 @@ def unregister(self, fileobj: typing.Any) -> selectors.SelectorKey: def modify( self, fileobj: typing.Any, events: int, data: typing.Any = None ) -> selectors.SelectorKey: - fd = fd_for_fileobj(fileobj) + fd = _fd_for_fileobj(fileobj) key = self.fd_to_selector_key[fd] if key.events != events: @@ -121,7 +121,7 @@ def select( self._drain_wakeup() continue - fd = fd_for_fileobj(key.fileobj) + fd = _fd_for_fileobj(key.fileobj) slint_key = self.fd_to_selector_key.get(fd) if slint_key is not None: ready.append((slint_key, mask)) @@ -137,7 +137,7 @@ def close(self) -> None: self._wakeup_reader.close() self._wakeup_writer.close() - def get_map(self) -> Mapping[int | HasFileno, selectors.SelectorKey]: + def get_map(self) -> Mapping[int | _HasFileno, selectors.SelectorKey]: return self.mapping def read_notify(self, fd: int) -> None: From 31037028e02c410e6790f2786155989eea2fdb0e Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 19:55:29 +0800 Subject: [PATCH 39/52] refactor(python): fix tests global_properties assertions and test_magic_import path --- api/python/slint/tests/test_compiler.py | 6 ++-- api/python/slint/tests/test_loader.py | 2 +- api/python/slint/tests/test_loop.py | 40 ------------------------- 3 files changed, 5 insertions(+), 43 deletions(-) diff --git a/api/python/slint/tests/test_compiler.py b/api/python/slint/tests/test_compiler.py index c305f31d4bc..561c5a607fb 100644 --- a/api/python/slint/tests/test_compiler.py +++ b/api/python/slint/tests/test_compiler.py @@ -63,9 +63,11 @@ def test_basic_compiler() -> None: assert compdef.globals == ["TestGlobal"] - assert compdef.global_properties("Garbage") == {} + assert compdef.global_properties("Garbage") is None + test_global_prop = compdef.global_properties("TestGlobal") + assert test_global_prop is not None assert [ - (name, type) for name, type in compdef.global_properties("TestGlobal").items() + (name, type) for name, type in test_global_prop.items() ] == [("theglobalprop", ValueType.String)] assert compdef.global_callbacks("Garbage") is None diff --git a/api/python/slint/tests/test_loader.py b/api/python/slint/tests/test_loader.py index bacd14671ef..af9edad591f 100644 --- a/api/python/slint/tests/test_loader.py +++ b/api/python/slint/tests/test_loader.py @@ -7,7 +7,7 @@ def test_magic_import() -> None: - instance = loader.test_load_file.App() + instance = loader.test_load_file_source.App() del instance diff --git a/api/python/slint/tests/test_loop.py b/api/python/slint/tests/test_loop.py index 154074b5a39..ff5bd46c0f6 100644 --- a/api/python/slint/tests/test_loop.py +++ b/api/python/slint/tests/test_loop.py @@ -1,14 +1,12 @@ # Copyright © SixtyFPS GmbH # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -import asyncio from datetime import timedelta import sys import pytest import slint -import slint.api from slint import core @@ -23,41 +21,3 @@ def call_sys_exit() -> None: "unexpected failure running python singleshot timer callback" in exc_info.value.__notes__ ) - - -def test_quit_event_loop_calls_core(monkeypatch: pytest.MonkeyPatch) -> None: - toggle = False - - def fake_quit() -> None: - nonlocal toggle - toggle = True - - monkeypatch.setattr(core, "quit_event_loop", fake_quit) - monkeypatch.setattr(core, "invoke_from_event_loop", lambda cb: cb()) - - slint.api.quit_event = asyncio.Event() - - slint.quit_event_loop() - - assert toggle is True - assert slint.api.quit_event.is_set() - - -def test_quit_event_loop_falls_back_when_invoke_fails(monkeypatch: pytest.MonkeyPatch) -> None: - toggle = False - - def fake_quit() -> None: - nonlocal toggle - toggle = True - - def fake_invoke(cb): - raise RuntimeError("invoke unavailable") - - monkeypatch.setattr(core, "quit_event_loop", fake_quit) - monkeypatch.setattr(core, "invoke_from_event_loop", fake_invoke) - - slint.api.quit_event = asyncio.Event() - - slint.quit_event_loop() - - assert toggle is True From 9ae9f4d4fd7228ce3c3d724e10cfe7da5190baeb Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 20:11:15 +0800 Subject: [PATCH 40/52] fix(python): add type ignore for selector attribute in SlintEventLoop --- api/python/slint/slint/loop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index dcaf9465255..411a9bd03e5 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -243,7 +243,7 @@ def stop(self) -> None: self.stop_run_forever_event.set() super().stop() - selector = self._selector + selector = self._selector # type: ignore[attr-defined] if isinstance(selector, _SlintSelector): selector._wakeup() @@ -329,8 +329,8 @@ def run_handle_cb() -> None: return handle def _write_to_self(self) -> None: - selector = self._selector + selector = self._selector # type: ignore[attr-defined] if isinstance(selector, _SlintSelector): selector._wakeup() else: - super()._write_to_self() + super()._write_to_self() # type: ignore[attr-defined] From 839f14b331fc70f6ff88fb6c853223dd351de69a Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 20:11:22 +0800 Subject: [PATCH 41/52] test(python): add tests for emitters package code generation --- .../tests/codegen/test_emitters_package.py | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 api/python/slint/tests/codegen/test_emitters_package.py diff --git a/api/python/slint/tests/codegen/test_emitters_package.py b/api/python/slint/tests/codegen/test_emitters_package.py new file mode 100644 index 00000000000..a76a1772d8d --- /dev/null +++ b/api/python/slint/tests/codegen/test_emitters_package.py @@ -0,0 +1,253 @@ +import ast +import importlib +import keyword +import sys +import types +from pathlib import Path + +import pytest + + +def _read_all_symbols(path: Path) -> list[str]: + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in tree.body: + if isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "__all__": + return ast.literal_eval(node.value) + raise AssertionError(f"Missing __all__ in {path}") + + +def _collect_generated_imports(path: Path) -> list[str]: + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in tree.body: + if ( + isinstance(node, ast.ImportFrom) + and node.level == 1 + and node.module == "_generated" + ): + return [alias.name for alias in node.names] + return [] + + +@pytest.fixture() +def emitters_modules(monkeypatch: pytest.MonkeyPatch): + root = Path(__file__).resolve().parents[2] + + slint_pkg = types.ModuleType("slint") + slint_pkg.__path__ = [str(root / "slint")] + monkeypatch.setitem(sys.modules, "slint", slint_pkg) + + codegen_pkg = types.ModuleType("slint.codegen") + codegen_pkg.__path__ = [str(root / "slint" / "codegen")] + monkeypatch.setitem(sys.modules, "slint.codegen", codegen_pkg) + + api_module = types.ModuleType("slint.api") + + def _normalize_prop(name: str) -> str: + ident = name.replace("-", "_") + if ident and ident[0].isdigit(): + ident = f"_{ident}" + if keyword.iskeyword(ident): + ident = f"{ident}_" + return ident + + api_module._normalize_prop = _normalize_prop # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, "slint.api", api_module) + + models = importlib.import_module("slint.codegen.models") + emitters = importlib.import_module("slint.codegen.emitters") + + module_names = ["slint.codegen.emitters", "slint.codegen.models"] + + try: + yield emitters, models + finally: + for name in module_names: + sys.modules.pop(name, None) + + +def test_write_package_emitters(tmp_path: Path, emitters_modules) -> None: + emitters, models = emitters_modules + + artifacts = models.ModuleArtifacts( + components=[ + models.ComponentMeta( + name="AppWindow", + py_name="AppWindow", + properties=[], + callbacks=[], + functions=[], + globals=[], + ), + models.ComponentMeta( + name="OtherComponent", + py_name="OtherComponent", + properties=[], + callbacks=[], + functions=[], + globals=[], + ), + ], + structs=[ + models.StructMeta( + name="Config", + py_name="ConfigStruct", + fields=[], + is_builtin=False, + ) + ], + enums=[ + models.EnumMeta( + name="Choice", + py_name="ChoiceEnum", + values=[ + models.EnumValueMeta( + name="First", + py_name="First", + value="first", + ) + ], + is_builtin=False, + is_used=True, + ), + models.EnumMeta( + name="BuiltinEnum", + py_name="BuiltinEnum", + values=[], + is_builtin=True, + is_used=True, + ), + ], + named_exports=[ + ("AppWindow", "WindowAlias"), + ("Config", "ConfigAlias"), + ("BuiltinEnum", "BuiltinAlias"), + ("OtherComponent", "other-component"), + ("Unknown", "UnknownAlias"), + ("Choice", "ChoiceAlias"), + ], + resource_paths=[], + ) + + package_dir = tmp_path / "generated" + package_dir.mkdir() + + emitters.write_package_init( + package_dir / "__init__.py", + source_relative="ui/app.slint", + artifacts=artifacts, + ) + emitters.write_package_init_stub( + package_dir / "__init__.pyi", artifacts=artifacts + ) + emitters.write_package_enums( + package_dir / "enums.py", + source_relative="ui/app.slint", + artifacts=artifacts, + ) + emitters.write_package_enums_stub( + package_dir / "enums.pyi", artifacts=artifacts + ) + emitters.write_package_structs( + package_dir / "structs.py", + source_relative="ui/app.slint", + artifacts=artifacts, + ) + emitters.write_package_structs_stub( + package_dir / "structs.pyi", artifacts=artifacts + ) + + init_py = (package_dir / "__init__.py").read_text(encoding="utf-8") + assert init_py.startswith("# Generated by slint.codegen from ui/app.slint") + assert "from . import enums, structs" in init_py + assert "BuiltinAlias" not in init_py + assert "UnknownAlias" not in init_py + assert "WindowAlias = AppWindow" in init_py + assert "ConfigAlias = ConfigStruct" in init_py + assert "other_component = OtherComponent" in init_py + assert "ChoiceAlias = ChoiceEnum" in init_py + assert _collect_generated_imports(package_dir / "__init__.py") == [ + "AppWindow", + "OtherComponent", + "ConfigStruct", + "ChoiceEnum", + ] + assert _read_all_symbols(package_dir / "__init__.py") == [ + "AppWindow", + "OtherComponent", + "ConfigStruct", + "ChoiceEnum", + "WindowAlias", + "ConfigAlias", + "other_component", + "ChoiceAlias", + "enums", + "structs", + ] + + init_pyi = (package_dir / "__init__.pyi").read_text(encoding="utf-8") + assert init_pyi.startswith("from __future__ import annotations") + assert "# Generated by" not in init_pyi + assert "BuiltinAlias" not in init_pyi + assert "UnknownAlias" not in init_pyi + assert _collect_generated_imports(package_dir / "__init__.pyi") == [ + "AppWindow", + "OtherComponent", + "ConfigStruct", + "ChoiceEnum", + ] + assert _read_all_symbols(package_dir / "__init__.pyi") == [ + "AppWindow", + "OtherComponent", + "ConfigStruct", + "ChoiceEnum", + "WindowAlias", + "ConfigAlias", + "other_component", + "ChoiceAlias", + "enums", + "structs", + ] + + enums_py = (package_dir / "enums.py").read_text(encoding="utf-8") + assert enums_py.startswith("# Generated by slint.codegen from ui/app.slint") + assert "BuiltinAlias" not in enums_py + assert "ChoiceAlias = ChoiceEnum" in enums_py + assert _collect_generated_imports(package_dir / "enums.py") == ["ChoiceEnum"] + assert _read_all_symbols(package_dir / "enums.py") == [ + "ChoiceEnum", + "ChoiceAlias", + ] + + enums_pyi = (package_dir / "enums.pyi").read_text(encoding="utf-8") + assert enums_pyi.startswith("from __future__ import annotations") + assert "# Generated by" not in enums_pyi + assert "BuiltinAlias" not in enums_pyi + assert _collect_generated_imports(package_dir / "enums.pyi") == ["ChoiceEnum"] + assert _read_all_symbols(package_dir / "enums.pyi") == [ + "ChoiceEnum", + "ChoiceAlias", + ] + + structs_py = (package_dir / "structs.py").read_text(encoding="utf-8") + assert structs_py.startswith("# Generated by slint.codegen from ui/app.slint") + assert "ConfigAlias = ConfigStruct" in structs_py + assert _collect_generated_imports(package_dir / "structs.py") == [ + "ConfigStruct" + ] + assert _read_all_symbols(package_dir / "structs.py") == [ + "ConfigStruct", + "ConfigAlias", + ] + + structs_pyi = (package_dir / "structs.pyi").read_text(encoding="utf-8") + assert structs_pyi.startswith("from __future__ import annotations") + assert "# Generated by" not in structs_pyi + assert _collect_generated_imports(package_dir / "structs.pyi") == [ + "ConfigStruct" + ] + assert _read_all_symbols(package_dir / "structs.pyi") == [ + "ConfigStruct", + "ConfigAlias", + ] From 826a92383ebb3b11edd45a82c8bbb564dcd55a4c Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 20:11:43 +0800 Subject: [PATCH 42/52] chore(python): add pytest-cov to development dependencies --- api/python/slint/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/api/python/slint/pyproject.toml b/api/python/slint/pyproject.toml index bcbc8186f9a..8eae04a756d 100644 --- a/api/python/slint/pyproject.toml +++ b/api/python/slint/pyproject.toml @@ -47,6 +47,7 @@ dev = [ "numpy>=2.3.2", "aiohttp>=3.12.15", "maturin>=1.9.6", + "pytest-cov>=7.0.0", ] [tool.maturin] From a300d54e2141fcb96f3d70156a3203e9cff98d10 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 20:12:31 +0800 Subject: [PATCH 43/52] doc(python): update README.md - Recommend running the code generator as part of the build process - Instructions for using third-party component libraries --- api/python/slint/README.md | 53 +++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/api/python/slint/README.md b/api/python/slint/README.md index 4a45cc38b04..e5465bcc0c4 100644 --- a/api/python/slint/README.md +++ b/api/python/slint/README.md @@ -98,10 +98,61 @@ slint_file = Path("ui/app.slint") generate_project( inputs=[slint_file], output_dir=Path("generated"), - config=GenerationConfig(include_paths=[slint_file.parent], library_paths={}, style=None, translation_domain=None, quiet=True), + config=GenerationConfig( + include_paths=[slint_file.parent], + library_paths={}, + style=None, + translation_domain=None, + quiet=True, + ), ) ``` +It's strongly recommended to run the code generator as part of your build process (e.g., `hatch` build hooks), so that the generated bindings are always in sync with your `.slint` files. + +## Component Libraries and `library_paths` + +Slint supports importing third-party component libraries such as the Material 3 component set under the `@material` namespace. Follow the instructions on [material.slint.dev/getting-started](https://material.slint.dev/getting-started/) to download the library bundle into your project (for example into `material-1.0/`). + +When you load a `.slint` file from Python, pass a `library_paths` dictionary that maps the library name to the `.slint` entry file in that bundle: + +```python +from pathlib import Path +import slint + +root = Path(__file__).parent +ui = slint.load_file( + root / "ui" / "main.slint", + library_paths={ + "material": root / "material-1.0" / "material.slint", + }, +) +``` + +The same mapping can be supplied to the code generator via `GenerationConfig` so the generated bindings reference the library correctly: + +```python +from pathlib import Path +from slint.codegen.generator import generate_project +from slint.codegen.models import GenerationConfig + +slint_file = Path("ui/app.slint") + +generate_project( + inputs=[slint_file], + output_dir=Path("generated"), + config=GenerationConfig( + include_paths=[slint_file.parent], + library_paths={"material": Path("material-1.0/material.slint")}, + style=None, + translation_domain=None, + quiet=True, + ), +) +``` + +Any library referenced with `import { ... } from "@library-name"` must appear in `library_paths`, and the value should point to the `.slint` file that serves as the library's root entry point. + ## API Overview ### Instantiating a Component From 64ea77e2cfa6ff1988655a3f6de18a634d20bfb5 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 20:13:33 +0800 Subject: [PATCH 44/52] feat(build): implement stable library path copying in build script - Avoid rust-analyzer alarms and spinning --- internal/compiler/build.rs | 49 +++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/compiler/build.rs b/internal/compiler/build.rs index a8cbb96b2e5..8c480de374e 100644 --- a/internal/compiler/build.rs +++ b/internal/compiler/build.rs @@ -1,6 +1,7 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 +use std::ffi::OsStr; use std::io::{BufWriter, Write}; use std::path::{Path, PathBuf}; @@ -40,11 +41,57 @@ fn widget_library() -> &'static [(&'static str, &'static BuiltinDirectory<'stati writeln!(file, "]\n}}")?; file.flush()?; - println!("cargo:rustc-env=SLINT_WIDGETS_LIBRARY={}", output_file_path.display()); + let stable_library_path = + copy_to_stable_location(&cargo_manifest_dir, &output_file_path)?; + + println!("cargo:rerun-if-env-changed=CARGO_TARGET_DIR"); + println!("cargo:rustc-env=SLINT_WIDGETS_LIBRARY={}", stable_library_path.display()); Ok(()) } +fn copy_to_stable_location( + cargo_manifest_dir: &Path, + generated_file: &Path, +) -> std::io::Result { + let out_dir = + generated_file.parent().expect("generated file to have a parent directory"); + + let workspace_dir = std::env::var_os("CARGO_WORKSPACE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| { + cargo_manifest_dir + .ancestors() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| cargo_manifest_dir.to_path_buf()) + }); + + let target_dir = std::env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .or_else(|| { + out_dir.ancestors().find_map(|ancestor| { + (ancestor.file_name() == Some(OsStr::new("target"))) + .then(|| ancestor.to_path_buf()) + }) + }) + .unwrap_or_else(|| workspace_dir.join("target")); + + let target_triple = + std::env::var("TARGET").or_else(|_| std::env::var("HOST")).unwrap_or_else(|_| "host".into()); + let profile = std::env::var("PROFILE").unwrap_or_else(|_| "debug".into()); + + let stable_dir = + target_dir.join("slint-widget-cache").join(target_triple).join(profile); + std::fs::create_dir_all(&stable_dir)?; + + let stable_path = stable_dir.join("included_library.rs"); + let contents = std::fs::read(generated_file)?; + std::fs::write(&stable_path, contents)?; + + Ok(stable_path) +} + fn process_style(cargo_manifest_dir: &Path, path: &Path) -> std::io::Result { let library_files: Vec = cargo_manifest_dir .join(path) From 114a4da864d2d754d2b11b7af2ae63d18bf59955 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 20:13:58 +0800 Subject: [PATCH 45/52] feat(python): add stubgen script --- api/python/slint/stubgen.sh | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 api/python/slint/stubgen.sh diff --git a/api/python/slint/stubgen.sh b/api/python/slint/stubgen.sh new file mode 100755 index 00000000000..e8b2c08c906 --- /dev/null +++ b/api/python/slint/stubgen.sh @@ -0,0 +1,8 @@ +export PATH="$HOME/.pyenv/bin:$PATH"; +eval "$(pyenv init -)"; +pyenv local 3.13.1; +export PYO3_PYTHON="$(pyenv which python3)"; +export LIBRARY_PATH="$HOME/.pyenv/versions/3.13.1/lib"; +export DYLD_LIBRARY_PATH="$HOME/.pyenv/versions/3.13.1/lib"; +export RUSTFLAGS="-C link-arg=-L$HOME/.pyenv/versions/3.13.1/lib -C link-arg=-lpython3.13"; +cargo run -pslint-python --bin stub-gen --features stubgen, From f58017250fddedbf8d8cc1a9e1e28c9da38802fd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:15:59 +0000 Subject: [PATCH 46/52] [autofix.ci] apply automated fixes --- api/python/slint/interpreter.rs | 20 +-- api/python/slint/slint/codegen/emitters.py | 28 +++- api/python/slint/slint/codegen/generator.py | 28 ++-- api/python/slint/slint/core.pyi | 146 +++++++++++------- api/python/slint/slint/loop.py | 2 +- api/python/slint/stubgen.sh | 3 + .../tests/codegen/examples/counter/counter.py | 9 +- .../codegen/examples/counter/counter.pyi | 6 +- .../tests/codegen/examples/counter/test.py | 16 +- .../tests/codegen/examples/counter/test.pyi | 19 ++- .../tests/codegen/examples/counter/test.slint | 3 + .../tests/codegen/test_emitters_package.py | 19 +-- api/python/slint/tests/test_compiler.py | 6 +- .../slint/tests/test_load_file_source.py | 17 +- .../slint/tests/test_load_file_source.pyi | 20 ++- api/python/slint/tests/test_sigint.py | 3 + api/python/slint/value.rs | 6 +- internal/compiler/build.rs | 22 ++- 18 files changed, 242 insertions(+), 131 deletions(-) diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index bf6ce2b2cd0..836d59e4297 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -299,25 +299,19 @@ impl ComponentDefinition { } fn global_properties(&self, name: &str) -> Option> { - self.definition - .global_properties_and_callbacks(name) - .map(|propiter| { - propiter - .filter_map(|(name, (ty, _))| ty.is_property_type().then(|| (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> { - self.definition - .global_callbacks(name) - .map(|callbackiter| callbackiter.collect()) + self.definition.global_callbacks(name).map(|callbackiter| callbackiter.collect()) } fn global_functions(&self, name: &str) -> Option> { - self.definition - .global_functions(name) - .map(|functioniter| functioniter.collect()) + self.definition.global_functions(name).map(|functioniter| functioniter.collect()) } fn global_property_infos(&self, global_name: &str) -> Option> { diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index 3930c3d71d5..fb13667235a 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -664,7 +664,9 @@ def _stmt(code: str) -> cst.BaseStatement: return cst.parse_statement(code) header: list[cst.CSTNode] = [ - cst.EmptyLine(comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}")) + cst.EmptyLine( + comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}") + ) ] body: list[cst.CSTNode] = [ @@ -696,7 +698,9 @@ def _stmt(code: str) -> cst.BaseStatement: for stmt in alias_statements: body.append(_stmt(stmt)) - export_names = _unique_preserve_order(symbol_names + alias_names + ["enums", "structs"]) + export_names = _unique_preserve_order( + symbol_names + alias_names + ["enums", "structs"] + ) body.append(cst.EmptyLine()) all_list = cst.List( elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] @@ -749,7 +753,9 @@ def _stmt(code: str) -> cst.BaseStatement: for stmt in alias_statements: body.append(_stmt(stmt)) - export_names = _unique_preserve_order(symbol_names + alias_names + ["enums", "structs"]) + export_names = _unique_preserve_order( + symbol_names + alias_names + ["enums", "structs"] + ) body.append(cst.EmptyLine()) all_list = cst.List( elements=[cst.Element(cst.SimpleString(repr(name))) for name in export_names] @@ -769,12 +775,16 @@ def _stmt(code: str) -> cst.BaseStatement: path.write_text(module.code, encoding="utf-8") -def write_package_enums(path: Path, *, source_relative: str, artifacts: ModuleArtifacts) -> None: +def write_package_enums( + path: Path, *, source_relative: str, artifacts: ModuleArtifacts +) -> None: def _stmt(code: str) -> cst.BaseStatement: return cst.parse_statement(code) header: list[cst.CSTNode] = [ - cst.EmptyLine(comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}")) + cst.EmptyLine( + comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}") + ) ] export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) @@ -867,12 +877,16 @@ def _stmt(code: str) -> cst.BaseStatement: path.write_text(module.code, encoding="utf-8") -def write_package_structs(path: Path, *, source_relative: str, artifacts: ModuleArtifacts) -> None: +def write_package_structs( + path: Path, *, source_relative: str, artifacts: ModuleArtifacts +) -> None: def _stmt(code: str) -> cst.BaseStatement: return cst.parse_statement(code) header: list[cst.CSTNode] = [ - cst.EmptyLine(comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}")) + cst.EmptyLine( + comment=cst.Comment(f"# Generated by slint.codegen from {source_relative}") + ) ] export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index 66c30a6f514..990123469a6 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -216,7 +216,9 @@ def is_error(diag: PyDiagnostic) -> bool: return result -def _collect_metadata(result: CompilationResult, source_descriptor: str) -> ModuleArtifacts: +def _collect_metadata( + result: CompilationResult, source_descriptor: str +) -> ModuleArtifacts: components: list[ComponentMeta] = [] used_enum_class_names: set[str] = set() component_enum_props: dict[str, dict[str, str]] = defaultdict(dict) @@ -326,12 +328,16 @@ def _collect_metadata(result: CompilationResult, source_descriptor: str) -> Modu for global_meta in globals_meta: for prop in global_meta.properties: try: - value = instance.get_global_property(global_meta.name, prop.name) + value = instance.get_global_property( + global_meta.name, prop.name + ) except Exception: continue if isinstance(value, enum.Enum): used_enum_class_names.add(value.__class__.__name__) - global_enum_props[(name, global_meta.name)][prop.name] = value.__class__.__name__ + global_enum_props[(name, global_meta.name)][prop.name] = ( + value.__class__.__name__ + ) structs_meta: list[StructMeta] = [] enums_meta: list[EnumMeta] = [] @@ -367,8 +373,8 @@ def _collect_metadata(result: CompilationResult, source_descriptor: str) -> Modu name=member, py_name=_normalize_prop(member), value=enum_member.name, + ) ) - ) is_used = enum_name in used_enum_class_names @@ -382,7 +388,9 @@ def _collect_metadata(result: CompilationResult, source_descriptor: str) -> Modu ) ) - enum_py_name_map = {enum.name: enum.py_name for enum in enums_meta if not enum.is_builtin} + enum_py_name_map = { + enum.name: enum.py_name for enum in enums_meta if not enum.is_builtin + } for component_meta in components: overrides = component_enum_props.get(component_meta.name, {}) @@ -399,7 +407,9 @@ def _collect_metadata(result: CompilationResult, source_descriptor: str) -> Modu prop.type_hint = "Any" for global_meta in component_meta.globals: - overrides = global_enum_props.get((component_meta.name, global_meta.name), {}) + overrides = global_enum_props.get( + (component_meta.name, global_meta.name), {} + ) for prop in global_meta.properties: class_name = overrides.get(prop.name) if class_name: @@ -407,9 +417,9 @@ def _collect_metadata(result: CompilationResult, source_descriptor: str) -> Modu prop.type_hint = enum_py_name_map[class_name] else: print( - f"warning: {source_descriptor}: global '{global_meta.name}.{prop.name}' relies on enum '{class_name}' " - "that is not declared in this module; falling back to Any", - ) + f"warning: {source_descriptor}: global '{global_meta.name}.{prop.name}' relies on enum '{class_name}' " + "that is not declared in this module; falling back to Any", + ) prop.type_hint = "Any" for struct_meta in structs_meta: diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index 0b9d782b84f..b27dee1e744 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # This file is automatically generated by pyo3_stub_gen # ruff: noqa: E501, F401 @@ -21,9 +24,9 @@ class Brush: r""" A brush is a data structure that is used to describe how a shape, such as a rectangle, path or even text, shall be filled. A brush can also be applied to the outline of a shape, that means the fill of the outline itself. - + Brushes can only be constructed from solid colors. - + **Note:** In future, we plan to reduce this constraint and allow for declaring graidient brushes programmatically. """ @property @@ -55,9 +58,9 @@ class Brush: def transparentize(self, amount: builtins.float) -> Brush: r""" Returns a new version of this brush with the opacity decreased by `factor`. - + The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. - + See also `Color.transparentize`. """ def with_alpha(self, alpha: builtins.float) -> Brush: @@ -88,7 +91,7 @@ class Color: r""" A Color object represents a color in the RGB color space with an alpha. Each color channel and the alpha is represented as an 8-bit integer. The alpha channel is 0 for fully transparent and 255 for fully opaque. - + Construct colors from a CSS color string, or by specifying the red, green, blue, and (optional) alpha channels in a dict. """ @property @@ -111,7 +114,12 @@ class Color: r""" The alpha channel. """ - def __new__(cls, maybe_value: typing.Optional[builtins.str | PyColorInput.RgbaColor | PyColorInput.RgbColor] = None) -> Self: ... + def __new__( + cls, + maybe_value: typing.Optional[ + builtins.str | PyColorInput.RgbaColor | PyColorInput.RgbColor + ] = None, + ) -> Self: ... def brighter(self, factor: builtins.float) -> Color: r""" Returns a new color that is brighter than this color by the given factor. @@ -123,7 +131,7 @@ class Color: def transparentize(self, factor: builtins.float) -> Color: r""" Returns a new version of this color with the opacity decreased by `factor`. - + The transparency is obtained by multiplying the alpha channel by `(1 - factor)`. """ def mix(self, other: Color, factor: builtins.float) -> Color: @@ -146,7 +154,11 @@ class CompilationResult: @property def diagnostics(self) -> builtins.list[PyDiagnostic]: ... @property - def structs_and_enums(self) -> tuple[builtins.dict[builtins.str, typing.Any], builtins.dict[builtins.str, typing.Any]]: ... + def structs_and_enums( + self, + ) -> tuple[ + builtins.dict[builtins.str, typing.Any], builtins.dict[builtins.str, typing.Any] + ]: ... @property def named_exports(self) -> builtins.list[tuple[builtins.str, builtins.str]]: ... @property @@ -166,11 +178,17 @@ class Compiler: @property def library_paths(self) -> builtins.dict[builtins.str, pathlib.Path]: ... @library_paths.setter - def library_paths(self, value: builtins.dict[builtins.str, pathlib.Path]) -> None: ... + def library_paths( + self, value: builtins.dict[builtins.str, pathlib.Path] + ) -> None: ... def __new__(cls) -> Self: ... def set_translation_domain(self, domain: builtins.str) -> None: ... - def build_from_path(self, path: builtins.str | os.PathLike | pathlib.Path) -> CompilationResult: ... - def build_from_source(self, source_code: builtins.str, path: builtins.str | os.PathLike | pathlib.Path) -> CompilationResult: ... + def build_from_path( + self, path: builtins.str | os.PathLike | pathlib.Path + ) -> CompilationResult: ... + def build_from_source( + self, source_code: builtins.str, path: builtins.str | os.PathLike | pathlib.Path + ) -> CompilationResult: ... @typing.final class ComponentDefinition: @@ -187,14 +205,28 @@ class ComponentDefinition: def property_infos(self) -> builtins.list[PropertyInfo]: ... def callback_infos(self) -> builtins.list[CallbackInfo]: ... def function_infos(self) -> builtins.list[FunctionInfo]: ... - def global_properties(self, name: builtins.str) -> typing.Optional[builtins.dict[builtins.str, ValueType]]: ... - def global_callbacks(self, name: builtins.str) -> typing.Optional[builtins.list[builtins.str]]: ... - def global_functions(self, name: builtins.str) -> typing.Optional[builtins.list[builtins.str]]: ... - def global_property_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[PropertyInfo]]: ... - def global_callback_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[CallbackInfo]]: ... - def global_function_infos(self, global_name: builtins.str) -> typing.Optional[builtins.list[FunctionInfo]]: ... + def global_properties( + self, name: builtins.str + ) -> typing.Optional[builtins.dict[builtins.str, ValueType]]: ... + def global_callbacks( + self, name: builtins.str + ) -> typing.Optional[builtins.list[builtins.str]]: ... + def global_functions( + self, name: builtins.str + ) -> typing.Optional[builtins.list[builtins.str]]: ... + def global_property_infos( + self, global_name: builtins.str + ) -> typing.Optional[builtins.list[PropertyInfo]]: ... + def global_callback_infos( + self, global_name: builtins.str + ) -> typing.Optional[builtins.list[CallbackInfo]]: ... + def global_function_infos( + self, global_name: builtins.str + ) -> typing.Optional[builtins.list[FunctionInfo]]: ... def callback_returns_void(self, callback_name: builtins.str) -> builtins.bool: ... - def global_callback_returns_void(self, global_name: builtins.str, callback_name: builtins.str) -> builtins.bool: ... + def global_callback_returns_void( + self, global_name: builtins.str, callback_name: builtins.str + ) -> builtins.bool: ... def create(self) -> ComponentInstance: ... @typing.final @@ -203,12 +235,23 @@ class ComponentInstance: def definition(self) -> ComponentDefinition: ... def get_property(self, name: builtins.str) -> typing.Any: ... def set_property(self, name: builtins.str, value: typing.Any) -> None: ... - def get_global_property(self, global_name: builtins.str, prop_name: builtins.str) -> typing.Any: ... - def set_global_property(self, global_name: builtins.str, prop_name: builtins.str, value: typing.Any) -> None: ... + def get_global_property( + self, global_name: builtins.str, prop_name: builtins.str + ) -> typing.Any: ... + def set_global_property( + self, global_name: builtins.str, prop_name: builtins.str, value: typing.Any + ) -> None: ... def invoke(self, callback_name: builtins.str, *args: typing.Any) -> typing.Any: ... - def invoke_global(self, global_name: builtins.str, callback_name: builtins.str, *args: typing.Any) -> typing.Any: ... + def invoke_global( + self, global_name: builtins.str, callback_name: builtins.str, *args: typing.Any + ) -> typing.Any: ... def set_callback(self, name: builtins.str, callable: typing.Any) -> None: ... - def set_global_callback(self, global_name: builtins.str, callback_name: builtins.str, callable: typing.Any) -> None: ... + def set_global_callback( + self, + global_name: builtins.str, + callback_name: builtins.str, + callable: typing.Any, + ) -> None: ... def show(self) -> None: ... def hide(self) -> None: ... def __clear__(self) -> None: ... @@ -264,41 +307,41 @@ class Image: r""" Creates a new image from an array-like object that implements the [Buffer Protocol](https://docs.python.org/3/c-api/buffer.html). Use this function to import images created by third-party modules such as matplotlib or Pillow. - + The array must satisfy certain contraints to represent an image: - + - The buffer's format needs to be `B` (unsigned char) - The shape must be a tuple of (height, width, bytes-per-pixel) - If a stride is defined, the row stride must be equal to width * bytes-per-pixel, and the column stride must equal the bytes-per-pixel. - A value of 3 for bytes-per-pixel is interpreted as RGB image, a value of 4 means RGBA. - + The image is created by performing a deep copy of the array's data. Subsequent changes to the buffer are not automatically reflected in a previously created Image. - + Example of importing a matplot figure into an image: ```python import slint import matplotlib - + from matplotlib.backends.backend_agg import FigureCanvasAgg from matplotlib.figure import Figure - + fig = Figure(figsize=(5, 4), dpi=100) canvas = FigureCanvasAgg(fig) ax = fig.add_subplot() ax.plot([1, 2, 3]) canvas.draw() - + buffer = canvas.buffer_rgba() img = slint.Image.load_from_array(buffer) ``` - + Example of loading an image with Pillow: ```python import slint from PIL import Image import numpy as np - + pil_img = Image.open("hello.jpeg") array = np.array(pil_img) img = slint.Image.load_from_array(array) @@ -321,18 +364,18 @@ class PyColorInput: def __new__(cls, _0: builtins.str) -> Self: ... def __len__(self) -> builtins.int: ... def __getitem__(self, key: builtins.int) -> typing.Any: ... - + class RgbaColor(typing.TypedDict): red: builtins.int green: builtins.int blue: builtins.int alpha: builtins.int - + class RgbColor(typing.TypedDict): red: builtins.int green: builtins.int blue: builtins.int - + ... @typing.final @@ -445,36 +488,35 @@ class RgbaColor: def alpha(self, value: builtins.int) -> None: ... @typing.final -class SlintToPyValue: - ... +class SlintToPyValue: ... @typing.final class Timer: r""" Timer is a handle to the timer system that triggers a callback after a specified period of time. - + Use `Timer.start()` to create a timer that that repeatedly triggers a callback, or `Timer.single_shot()` to trigger a callback only once. - + The timer will automatically stop when garbage collected. You must keep the Timer object around for as long as you want the timer to keep firing. - + ```python class AppWindow(...) def __init__(self): super().__init__() self.my_timer = None - + @slint.callback def button_clicked(self): self.my_timer = slint.Timer() self.my_timer.start(timedelta(seconds=1), self.do_something) - + def do_something(self): pass ``` - + Timers can only be used in the thread that runs the Slint event loop. They don't fire if used in another thread. """ @@ -489,17 +531,19 @@ class Timer: def interval(self, value: datetime.timedelta) -> None: r""" The duration of timer. - + When setting this property and the timer is running (see `Timer.running`), then the time when the callback will be next invoked is re-calculated to be in the specified duration relative to when this property is set. """ def __new__(cls) -> Self: ... - def start(self, mode: TimerMode, interval: datetime.timedelta, callback: typing.Any) -> None: + def start( + self, mode: TimerMode, interval: datetime.timedelta, callback: typing.Any + ) -> None: r""" Starts the timer with the given mode and interval, in order for the callback to called when the timer fires. If the timer has been started previously and not fired yet, then it will be restarted. - + Arguments: * `mode`: The timer mode to apply, i.e. whether to repeatedly fire the timer or just once. * `interval`: The duration from now until when the timer should firethe first time, and subsequently @@ -511,7 +555,7 @@ class Timer: r""" Starts the timer with the duration and the callback to called when the timer fires. It is fired only once and then deleted. - + Arguments: * `duration`: The duration from now until when the timer should fire. * `callback`: The function to call when the time has been reached or exceeded. @@ -525,7 +569,7 @@ class Timer: Restarts the timer. If the timer was previously started by calling `Timer.start()` with a duration and callback, then the time when the callback will be next invoked is re-calculated to be in the specified duration relative to when this function is called. - + Does nothing if the timer was never started. """ @@ -538,9 +582,10 @@ class DiagnosticLevel(enum.Enum): class TimerMode(enum.Enum): r""" The TimerMode specifies what should happen after the timer fired. - + Used by the `Timer.start()` function. """ + SingleShot = ... r""" A SingleShot timer is fired only once. @@ -563,12 +608,7 @@ class ValueType(enum.Enum): Enumeration = ... def init_translations(translations: typing.Any) -> None: ... - def invoke_from_event_loop(callable: typing.Any) -> None: ... - def quit_event_loop() -> None: ... - def run_event_loop() -> None: ... - def set_xdg_app_id(app_id: builtins.str) -> None: ... - diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index 411a9bd03e5..b2188a48991 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -333,4 +333,4 @@ def _write_to_self(self) -> None: if isinstance(selector, _SlintSelector): selector._wakeup() else: - super()._write_to_self() # type: ignore[attr-defined] + super()._write_to_self() # type: ignore[attr-defined] diff --git a/api/python/slint/stubgen.sh b/api/python/slint/stubgen.sh index e8b2c08c906..4d4e814eb31 100755 --- a/api/python/slint/stubgen.sh +++ b/api/python/slint/stubgen.sh @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + export PATH="$HOME/.pyenv/bin:$PATH"; eval "$(pyenv init -)"; pyenv local 3.13.1; diff --git a/api/python/slint/tests/codegen/examples/counter/counter.py b/api/python/slint/tests/codegen/examples/counter/counter.py index 0614ac6d045..e98f7e62fb2 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.py +++ b/api/python/slint/tests/codegen/examples/counter/counter.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # Generated by slint.codegen from counter.slint from __future__ import annotations @@ -10,11 +13,12 @@ import slint -__all__ = ['CounterWindow'] +__all__ = ["CounterWindow"] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = 'counter.slint' +_SLINT_RESOURCE = "counter.slint" + def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -35,6 +39,7 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) + _module = _load() CounterWindow = _module.CounterWindow diff --git a/api/python/slint/tests/codegen/examples/counter/counter.pyi b/api/python/slint/tests/codegen/examples/counter/counter.pyi index 89f2fb8b43b..eb8b6b75ba3 100644 --- a/api/python/slint/tests/codegen/examples/counter/counter.pyi +++ b/api/python/slint/tests/codegen/examples/counter/counter.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations from typing import ( @@ -6,11 +9,10 @@ from typing import ( ) import slint -__all__ = ['CounterWindow'] +__all__ = ["CounterWindow"] class CounterWindow(slint.Component): def __init__(self, **kwargs: Any) -> None: ... alignment: Any counter: int request_increase: Callable[[], None] - diff --git a/api/python/slint/tests/codegen/examples/counter/test.py b/api/python/slint/tests/codegen/examples/counter/test.py index a629bea7f90..55fa293831d 100644 --- a/api/python/slint/tests/codegen/examples/counter/test.py +++ b/api/python/slint/tests/codegen/examples/counter/test.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # Generated by slint.codegen from test.slint from __future__ import annotations @@ -10,11 +13,19 @@ import slint -__all__ = ['OptionalDemo', 'OptionalFloat', 'OptionalBool', 'OptionalInt', 'OptionalString', 'OptionalEnum'] +__all__ = [ + "OptionalDemo", + "OptionalFloat", + "OptionalBool", + "OptionalInt", + "OptionalString", + "OptionalEnum", +] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = 'test.slint' +_SLINT_RESOURCE = "test.slint" + def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -35,6 +46,7 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) + _module = _load() OptionalDemo = _module.OptionalDemo diff --git a/api/python/slint/tests/codegen/examples/counter/test.pyi b/api/python/slint/tests/codegen/examples/counter/test.pyi index db00f2ff07c..5aa34b97dba 100644 --- a/api/python/slint/tests/codegen/examples/counter/test.pyi +++ b/api/python/slint/tests/codegen/examples/counter/test.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import enum @@ -9,7 +12,14 @@ from typing import ( import slint -__all__ = ['OptionalDemo', 'OptionalFloat', 'OptionalBool', 'OptionalInt', 'OptionalString', 'OptionalEnum'] +__all__ = [ + "OptionalDemo", + "OptionalFloat", + "OptionalBool", + "OptionalInt", + "OptionalString", + "OptionalEnum", +] class OptionalFloat: def __init__(self, *, maybe_value: float = ...) -> None: ... @@ -28,13 +38,12 @@ class OptionalString: maybe_value: str class OptionalEnum(enum.Enum): - OptionA = 'OptionA' - OptionB = 'OptionB' - OptionC = 'OptionC' + OptionA = "OptionA" + OptionB = "OptionB" + OptionC = "OptionC" class OptionalDemo(slint.Component): def __init__(self, **kwargs: Any) -> None: ... maybe_count: Optional[int] on_action: Callable[[Optional[float]], Optional[int]] compute: Callable[[Optional[str]], Optional[bool]] - diff --git a/api/python/slint/tests/codegen/examples/counter/test.slint b/api/python/slint/tests/codegen/examples/counter/test.slint index 96c0718b53c..f2df7baa304 100644 --- a/api/python/slint/tests/codegen/examples/counter/test.slint +++ b/api/python/slint/tests/codegen/examples/counter/test.slint @@ -1,3 +1,6 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + export struct OptionalInt { maybe_value: int, } diff --git a/api/python/slint/tests/codegen/test_emitters_package.py b/api/python/slint/tests/codegen/test_emitters_package.py index a76a1772d8d..be23f26303b 100644 --- a/api/python/slint/tests/codegen/test_emitters_package.py +++ b/api/python/slint/tests/codegen/test_emitters_package.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + import ast import importlib import keyword @@ -138,17 +141,13 @@ def test_write_package_emitters(tmp_path: Path, emitters_modules) -> None: source_relative="ui/app.slint", artifacts=artifacts, ) - emitters.write_package_init_stub( - package_dir / "__init__.pyi", artifacts=artifacts - ) + emitters.write_package_init_stub(package_dir / "__init__.pyi", artifacts=artifacts) emitters.write_package_enums( package_dir / "enums.py", source_relative="ui/app.slint", artifacts=artifacts, ) - emitters.write_package_enums_stub( - package_dir / "enums.pyi", artifacts=artifacts - ) + emitters.write_package_enums_stub(package_dir / "enums.pyi", artifacts=artifacts) emitters.write_package_structs( package_dir / "structs.py", source_relative="ui/app.slint", @@ -233,9 +232,7 @@ def test_write_package_emitters(tmp_path: Path, emitters_modules) -> None: structs_py = (package_dir / "structs.py").read_text(encoding="utf-8") assert structs_py.startswith("# Generated by slint.codegen from ui/app.slint") assert "ConfigAlias = ConfigStruct" in structs_py - assert _collect_generated_imports(package_dir / "structs.py") == [ - "ConfigStruct" - ] + assert _collect_generated_imports(package_dir / "structs.py") == ["ConfigStruct"] assert _read_all_symbols(package_dir / "structs.py") == [ "ConfigStruct", "ConfigAlias", @@ -244,9 +241,7 @@ def test_write_package_emitters(tmp_path: Path, emitters_modules) -> None: structs_pyi = (package_dir / "structs.pyi").read_text(encoding="utf-8") assert structs_pyi.startswith("from __future__ import annotations") assert "# Generated by" not in structs_pyi - assert _collect_generated_imports(package_dir / "structs.pyi") == [ - "ConfigStruct" - ] + assert _collect_generated_imports(package_dir / "structs.pyi") == ["ConfigStruct"] assert _read_all_symbols(package_dir / "structs.pyi") == [ "ConfigStruct", "ConfigAlias", diff --git a/api/python/slint/tests/test_compiler.py b/api/python/slint/tests/test_compiler.py index 561c5a607fb..d6f32700f84 100644 --- a/api/python/slint/tests/test_compiler.py +++ b/api/python/slint/tests/test_compiler.py @@ -66,9 +66,9 @@ def test_basic_compiler() -> None: assert compdef.global_properties("Garbage") is None test_global_prop = compdef.global_properties("TestGlobal") assert test_global_prop is not None - assert [ - (name, type) for name, type in test_global_prop.items() - ] == [("theglobalprop", ValueType.String)] + assert [(name, type) for name, type in test_global_prop.items()] == [ + ("theglobalprop", ValueType.String) + ] assert compdef.global_callbacks("Garbage") is None assert compdef.global_callbacks("TestGlobal") == ["globallogic"] diff --git a/api/python/slint/tests/test_load_file_source.py b/api/python/slint/tests/test_load_file_source.py index a5a50a25b7c..76c8ee9e44e 100644 --- a/api/python/slint/tests/test_load_file_source.py +++ b/api/python/slint/tests/test_load_file_source.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + # Generated by slint.codegen from test-load-file-source.slint from __future__ import annotations @@ -10,11 +13,20 @@ import slint -__all__ = ['Diag', 'App', 'MyData', 'Secret_Struct', 'TestEnum', 'MyDiag', 'Public_Struct'] +__all__ = [ + "Diag", + "App", + "MyData", + "Secret_Struct", + "TestEnum", + "MyDiag", + "Public_Struct", +] _MODULE_DIR = Path(__file__).parent -_SLINT_RESOURCE = 'test-load-file-source.slint' +_SLINT_RESOURCE = "test-load-file-source.slint" + def _load() -> types.SimpleNamespace: """Load the compiled Slint module for this package.""" @@ -35,6 +47,7 @@ def _load() -> types.SimpleNamespace: translation_domain=None, ) + _module = _load() Diag = _module.Diag diff --git a/api/python/slint/tests/test_load_file_source.pyi b/api/python/slint/tests/test_load_file_source.pyi index b2e988097d8..3b8efc73e45 100644 --- a/api/python/slint/tests/test_load_file_source.pyi +++ b/api/python/slint/tests/test_load_file_source.pyi @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + from __future__ import annotations import enum @@ -7,7 +10,15 @@ from typing import ( ) import slint -__all__ = ['Diag', 'App', 'MyData', 'Secret_Struct', 'TestEnum', 'MyDiag', 'Public_Struct'] +__all__ = [ + "Diag", + "App", + "MyData", + "Secret_Struct", + "TestEnum", + "MyDiag", + "Public_Struct", +] class MyData: def __init__(self, *, age: float = ..., name: str = ...) -> None: ... @@ -19,8 +30,8 @@ class Secret_Struct: balance: float class TestEnum(enum.Enum): - Variant1 = 'Variant1' - Variant2 = 'Variant2' + Variant1 = "Variant1" + Variant2 = "Variant2" class Diag(slint.Component): def __init__(self, **kwargs: Any) -> None: ... @@ -28,6 +39,7 @@ class Diag(slint.Component): global_prop: str global_callback: Callable[[str], str] minus_one: Callable[[int], None] + class SecondGlobal: second: str @@ -50,10 +62,10 @@ class App(slint.Component): global_prop: str global_callback: Callable[[str], str] minus_one: Callable[[int], None] + class SecondGlobal: second: str MyDiag = Diag Public_Struct = Secret_Struct - diff --git a/api/python/slint/tests/test_sigint.py b/api/python/slint/tests/test_sigint.py index 41900ee2f5d..1233f8d4750 100644 --- a/api/python/slint/tests/test_sigint.py +++ b/api/python/slint/tests/test_sigint.py @@ -1,3 +1,6 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + import signal import threading import time diff --git a/api/python/slint/value.rs b/api/python/slint/value.rs index f23985c469d..279cb941336 100644 --- a/api/python/slint/value.rs +++ b/api/python/slint/value.rs @@ -344,9 +344,9 @@ impl TypeCollection { } pub fn enums(&self) -> impl Iterator)> + '_ { - self.enum_classes.iter().filter_map(|(name, info)| { - (!info.is_builtin).then_some((name, &info.class)) - }) + self.enum_classes + .iter() + .filter_map(|(name, info)| (!info.is_builtin).then_some((name, &info.class))) } pub fn slint_value_from_py_value( diff --git a/internal/compiler/build.rs b/internal/compiler/build.rs index 8c480de374e..a1c09697348 100644 --- a/internal/compiler/build.rs +++ b/internal/compiler/build.rs @@ -41,8 +41,7 @@ fn widget_library() -> &'static [(&'static str, &'static BuiltinDirectory<'stati writeln!(file, "]\n}}")?; file.flush()?; - let stable_library_path = - copy_to_stable_location(&cargo_manifest_dir, &output_file_path)?; + let stable_library_path = copy_to_stable_location(&cargo_manifest_dir, &output_file_path)?; println!("cargo:rerun-if-env-changed=CARGO_TARGET_DIR"); println!("cargo:rustc-env=SLINT_WIDGETS_LIBRARY={}", stable_library_path.display()); @@ -54,12 +53,10 @@ fn copy_to_stable_location( cargo_manifest_dir: &Path, generated_file: &Path, ) -> std::io::Result { - let out_dir = - generated_file.parent().expect("generated file to have a parent directory"); + let out_dir = generated_file.parent().expect("generated file to have a parent directory"); - let workspace_dir = std::env::var_os("CARGO_WORKSPACE_DIR") - .map(PathBuf::from) - .unwrap_or_else(|| { + let workspace_dir = + std::env::var_os("CARGO_WORKSPACE_DIR").map(PathBuf::from).unwrap_or_else(|| { cargo_manifest_dir .ancestors() .nth(1) @@ -71,18 +68,17 @@ fn copy_to_stable_location( .map(PathBuf::from) .or_else(|| { out_dir.ancestors().find_map(|ancestor| { - (ancestor.file_name() == Some(OsStr::new("target"))) - .then(|| ancestor.to_path_buf()) + (ancestor.file_name() == Some(OsStr::new("target"))).then(|| ancestor.to_path_buf()) }) }) .unwrap_or_else(|| workspace_dir.join("target")); - let target_triple = - std::env::var("TARGET").or_else(|_| std::env::var("HOST")).unwrap_or_else(|_| "host".into()); + let target_triple = std::env::var("TARGET") + .or_else(|_| std::env::var("HOST")) + .unwrap_or_else(|_| "host".into()); let profile = std::env::var("PROFILE").unwrap_or_else(|_| "debug".into()); - let stable_dir = - target_dir.join("slint-widget-cache").join(target_triple).join(profile); + let stable_dir = target_dir.join("slint-widget-cache").join(target_triple).join(profile); std::fs::create_dir_all(&stable_dir)?; let stable_path = stable_dir.join("included_library.rs"); From 9864217ff465e6028b170302b1abe1e5d0a67949 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 20:31:32 +0800 Subject: [PATCH 47/52] fix(python): remove unused imports from interpreter.rs --- api/python/slint/interpreter.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/python/slint/interpreter.rs b/api/python/slint/interpreter.rs index 836d59e4297..8412b04fc04 100644 --- a/api/python/slint/interpreter.rs +++ b/api/python/slint/interpreter.rs @@ -7,9 +7,6 @@ use std::path::PathBuf; use std::rc::Rc; use std::sync::OnceLock; -use i_slint_core::api::CloseRequestResponse; -use i_slint_core::platform::WindowEvent; - use pyo3::IntoPyObjectExt; use pyo3_stub_gen::derive::*; use slint_interpreter::{ComponentHandle, Value}; From ae40bf69aa812cb376c9d37ff30e1cb85532f745 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 20:49:02 +0800 Subject: [PATCH 48/52] fix(python): mis-replaced repr(Path) --- api/python/slint/slint/codegen/emitters.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/python/slint/slint/codegen/emitters.py b/api/python/slint/slint/codegen/emitters.py index fb13667235a..c7243d27f5b 100644 --- a/api/python/slint/slint/codegen/emitters.py +++ b/api/python/slint/slint/codegen/emitters.py @@ -18,7 +18,7 @@ def module_relative_path_expr(module_dir: Path, target: Path) -> str: try: rel = os.path.relpath(target, module_dir) except ValueError: - return repr(target) + return f"Path({repr(str(target))})" if rel in (".", ""): return "_MODULE_DIR" @@ -289,9 +289,11 @@ def _stmt(code: str) -> cst.BaseStatement: ] library_expr_code = f"{{{', '.join(library_items)}}}" if library_items else "None" - style_expr = repr(Path(config.style)) if config.style else "None" + style_expr = repr(config.style) if config.style is not None else "None" domain_expr = ( - repr(Path(config.translation_domain)) if config.translation_domain else "None" + repr(config.translation_domain) + if config.translation_domain is not None + else "None" ) export_bindings = _collect_export_bindings(artifacts, include_builtin_enums=False) From 142c58ac358f179370b95a57e2539a4a3a49319f Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 21:27:53 +0800 Subject: [PATCH 49/52] fix(python): make mypy happy --- api/python/slint/slint/codegen/generator.py | 11 ++++----- api/python/slint/slint/core.pyi | 10 ++++---- api/python/slint/slint/loop.py | 8 +++---- api/python/slint/slint/models.py | 16 ++++++------- .../tests/codegen/test_emitters_package.py | 8 +++++-- api/python/slint/tests/test_enums.py | 13 ++++++---- api/python/slint/tests/test_gc.py | 4 ++-- .../slint/tests/test_load_file_module.py | 24 +++++++++---------- api/python/slint/tests/test_models.py | 4 ++-- api/python/slint/tests/test_sigint.py | 2 +- api/python/slint/tests/test_structs.py | 18 ++++++++++---- 11 files changed, 65 insertions(+), 53 deletions(-) diff --git a/api/python/slint/slint/codegen/generator.py b/api/python/slint/slint/codegen/generator.py index 990123469a6..45f87acf0fb 100644 --- a/api/python/slint/slint/codegen/generator.py +++ b/api/python/slint/slint/codegen/generator.py @@ -61,9 +61,9 @@ def copy_slint_file(source: Path, destination: Path) -> None: if config.style: compiler.style = config.style if config.include_paths: - compiler.include_paths = config.include_paths.copy() # type: ignore[assignment] + compiler.include_paths = config.include_paths.copy() if config.library_paths: - compiler.library_paths = config.library_paths.copy() # type: ignore[assignment] + compiler.library_paths = config.library_paths.copy() if config.translation_domain: compiler.set_translation_domain(config.translation_domain) @@ -208,10 +208,10 @@ def is_error(diag: PyDiagnostic) -> bool: print(f" warning: {warn}") if fatal_errors: - print(f"error: Compilation of {source_relative} failed & skiped with errors:") + print(f"error: Compilation of {source_relative} failed & skipped with errors:") for fatal in fatal_errors: print(f" error: {fatal}") - return + return None return result @@ -220,7 +220,6 @@ def _collect_metadata( result: CompilationResult, source_descriptor: str ) -> ModuleArtifacts: components: list[ComponentMeta] = [] - used_enum_class_names: set[str] = set() component_enum_props: dict[str, dict[str, str]] = defaultdict(dict) global_enum_props: dict[tuple[str, str], dict[str, str]] = defaultdict(dict) struct_enum_fields: dict[str, dict[str, str]] = defaultdict(dict) @@ -367,7 +366,7 @@ def _collect_metadata( for enum_name, enum_cls in enums.items(): values: list[EnumValueMeta] = [] - for member, enum_member in enum_cls.__members__.items(): # type: ignore + for member, enum_member in enum_cls.__members__.items(): values.append( EnumValueMeta( name=member, diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index b27dee1e744..37e22ad674b 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -68,7 +68,7 @@ class Brush: Returns a new version of this brush with the related color's opacities set to `alpha`. """ - def __eq__(self, other: Brush) -> builtins.bool: ... + def __eq__(self, other: object) -> builtins.bool: ... @typing.final class CallbackInfo: @@ -145,7 +145,7 @@ class Color: Returns a new version of this color with the opacity set to `alpha`. """ def __str__(self) -> builtins.str: ... - def __eq__(self, other: Color) -> builtins.bool: ... + def __eq__(self, other: object) -> builtins.bool: ... @typing.final class CompilationResult: @@ -184,10 +184,10 @@ class Compiler: def __new__(cls) -> Self: ... def set_translation_domain(self, domain: builtins.str) -> None: ... def build_from_path( - self, path: builtins.str | os.PathLike | pathlib.Path + self, path: builtins.str | os.PathLike[builtins.str] | pathlib.Path ) -> CompilationResult: ... def build_from_source( - self, source_code: builtins.str, path: builtins.str | os.PathLike | pathlib.Path + self, source_code: builtins.str, path: builtins.str | os.PathLike[builtins.str] | pathlib.Path ) -> CompilationResult: ... @typing.final @@ -293,7 +293,7 @@ class Image: """ def __new__(cls) -> Self: ... @staticmethod - def load_from_path(path: builtins.str | os.PathLike | pathlib.Path) -> Image: + def load_from_path(path: builtins.str | os.PathLike[builtins.str] | pathlib.Path) -> Image: r""" Loads the image from the specified path. Returns None if the image can't be loaded. """ diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index b2188a48991..69b28f84979 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -198,11 +198,11 @@ async def loop_stopper(event: asyncio.Event) -> None: self._stopping = False asyncio.events._set_running_loop(None) - def run_until_complete[T](self, future: typing.Awaitable[T]) -> T | None: # type: ignore[override] + def run_until_complete[T](self, future: typing.Awaitable[T]) -> T | None: if self._core_loop_started and not self._core_loop_running: return asyncio.selector_events.BaseSelectorEventLoop.run_until_complete( self, future - ) # type: ignore[misc] + ) def stop_loop(future: typing.Any) -> None: self.stop() @@ -300,7 +300,7 @@ def call_soon(self, callback, *args, context=None) -> asyncio.TimerHandle: # ty when=self.time(), callback=callback, args=args, loop=self, context=context ) self._soon_tasks.append(handle) - self.call_later(0, self._flush_soon_tasks) # type: ignore + self.call_later(0, self._flush_soon_tasks) # type: ignore return handle def _flush_soon_tasks(self) -> None: @@ -333,4 +333,4 @@ def _write_to_self(self) -> None: if isinstance(selector, _SlintSelector): selector._wakeup() else: - super()._write_to_self() # type: ignore[attr-defined] + asyncio.SelectorEventLoop._write_to_self(self) # type: ignore diff --git a/api/python/slint/slint/models.py b/api/python/slint/slint/models.py index ff18bb90057..c9a8d04ca5a 100644 --- a/api/python/slint/slint/models.py +++ b/api/python/slint/slint/models.py @@ -16,7 +16,7 @@ class Model[T](core.PyModelBase, ABC, Iterable[T]): Models are iterable and can be used in for loops.""" - def __new__(cls, *args: Any): + def __new__(cls, *args: Any) -> "Model[T]": return super().__new__(cls) def __init__(self) -> None: @@ -46,20 +46,18 @@ class ListModel[T](Model[T]): in UI they're used with. """ - def __init__(self, iterable: typing.Optional[Iterable[T]] = None): + def __init__(self, iterable: typing.Optional[Iterable[T]] = None) -> None: """Constructs a new ListModel from the give iterable. All the values the iterable produces are stored in a list.""" super().__init__() - if iterable is not None: - self.list = list(iterable) - else: - self.list = [] + items = list(iterable) if iterable is not None else [] + self.list = typing.cast(list[T], items) def row_count(self) -> int: return len(self.list) - def row_data(self, row: int) -> typing.Optional[T]: + def row_data(self, row: int) -> T: return self.list[row] def set_row_data(self, row: int, data: T) -> None: @@ -84,7 +82,7 @@ def append(self, value: T) -> None: class ModelIterator[T](Iterator[T]): - def __init__(self, model: Model[T]): + def __init__(self, model: Model[T]) -> None: self.model = model self.index = 0 @@ -98,4 +96,4 @@ def __next__(self) -> T: self.index += 1 data = self.model.row_data(index) assert data is not None - return data + return typing.cast(T, data) diff --git a/api/python/slint/tests/codegen/test_emitters_package.py b/api/python/slint/tests/codegen/test_emitters_package.py index be23f26303b..5980e1dd570 100644 --- a/api/python/slint/tests/codegen/test_emitters_package.py +++ b/api/python/slint/tests/codegen/test_emitters_package.py @@ -6,6 +6,8 @@ import keyword import sys import types +import typing +from collections.abc import Iterator from pathlib import Path import pytest @@ -17,7 +19,7 @@ def _read_all_symbols(path: Path) -> list[str]: if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name) and target.id == "__all__": - return ast.literal_eval(node.value) + return typing.cast(list[str], ast.literal_eval(node.value)) raise AssertionError(f"Missing __all__ in {path}") @@ -34,7 +36,9 @@ def _collect_generated_imports(path: Path) -> list[str]: @pytest.fixture() -def emitters_modules(monkeypatch: pytest.MonkeyPatch): +def emitters_modules( + monkeypatch: pytest.MonkeyPatch, +) -> Iterator[tuple[types.ModuleType, types.ModuleType]]: root = Path(__file__).resolve().parents[2] slint_pkg = types.ModuleType("slint") diff --git a/api/python/slint/tests/test_enums.py b/api/python/slint/tests/test_enums.py index 29664041d49..7288ddf1c23 100644 --- a/api/python/slint/tests/test_enums.py +++ b/api/python/slint/tests/test_enums.py @@ -3,11 +3,13 @@ from __future__ import annotations +import importlib.abc import importlib.util import sys import inspect +import typing from pathlib import Path -from typing import Any +from types import ModuleType import pytest from slint import ListModel, core @@ -20,7 +22,7 @@ def _slint_source() -> Path: @pytest.fixture -def generated_module(tmp_path: Path) -> Any: +def generated_module(tmp_path: Path) -> ModuleType: slint_file = _slint_source() output_dir = tmp_path / "generated" config = GenerationConfig( @@ -43,11 +45,12 @@ def generated_module(tmp_path: Path) -> Any: sys.modules.pop(spec.name, None) module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module - spec.loader.exec_module(module) # type: ignore[arg-type] - return module + loader = typing.cast(importlib.abc.Loader, spec.loader) + loader.exec_module(module) + return typing.cast(ModuleType, module) -def test_enums(generated_module: Any) -> None: +def test_enums(generated_module: ModuleType) -> None: module = generated_module TestEnum = module.TestEnum diff --git a/api/python/slint/tests/test_gc.py b/api/python/slint/tests/test_gc.py index a333c527b6e..9579b210d08 100644 --- a/api/python/slint/tests/test_gc.py +++ b/api/python/slint/tests/test_gc.py @@ -66,7 +66,7 @@ def test_struct_gc() -> None: instance: core.ComponentInstance | None = compdef.create() assert instance is not None - model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) + model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) # type: ignore assert model assert model.row_count() == 3 @@ -94,7 +94,7 @@ def test_properties_gc() -> None: instance: core.ComponentInstance | None = compdef.create() assert instance is not None - model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) + model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) # type: ignore assert model assert model.row_count() == 3 diff --git a/api/python/slint/tests/test_load_file_module.py b/api/python/slint/tests/test_load_file_module.py index 744b145db39..43920ca92e4 100644 --- a/api/python/slint/tests/test_load_file_module.py +++ b/api/python/slint/tests/test_load_file_module.py @@ -3,23 +3,23 @@ from __future__ import annotations -import importlib +from importlib import import_module, reload from types import ModuleType -from typing import TYPE_CHECKING -import test_load_file_source as generated_module +def _module() -> ModuleType: + """ + Return a fresh instance of the generated module for each call. -def _module(): - if TYPE_CHECKING: - return generated_module - - # Reload to ensure a fresh module for each call - return importlib.reload(generated_module) + Using a dynamic import keeps mypy from requiring type information for the + generated module, while runtime callers still get the reloaded module. + """ + module = import_module("test_load_file_source") + return reload(module) def test_codegen_module_exports() -> None: - module = _module() + module: ModuleType = _module() expected_exports = { "App", @@ -50,7 +50,7 @@ def test_codegen_module_exports() -> None: def test_generated_module_wrapper() -> None: - module = _module() + module: ModuleType = _module() instance = module.App() @@ -71,7 +71,7 @@ def test_generated_module_wrapper() -> None: def test_constructor_kwargs() -> None: - module = _module() + module: ModuleType = _module() def early_say_hello(arg: str) -> str: return "early:" + arg diff --git a/api/python/slint/tests/test_models.py b/api/python/slint/tests/test_models.py index 651d0bf5c7c..8a7b1e62dc2 100644 --- a/api/python/slint/tests/test_models.py +++ b/api/python/slint/tests/test_models.py @@ -46,9 +46,9 @@ def test_model_notify() -> None: assert instance.get_property("layout-height") == 100 model.set_row_data(1, 50) assert instance.get_property("layout-height") == 150 - model.append(75) + model.append(75) # type: ignore assert instance.get_property("layout-height") == 225 - del model[1:] + del model[1:] # type: ignore assert instance.get_property("layout-height") == 100 assert isinstance(instance.get_property("fixed-height-model"), models.ListModel) diff --git a/api/python/slint/tests/test_sigint.py b/api/python/slint/tests/test_sigint.py index 1233f8d4750..dc0fc1ef386 100644 --- a/api/python/slint/tests/test_sigint.py +++ b/api/python/slint/tests/test_sigint.py @@ -10,7 +10,7 @@ import slint -def test_run_event_loop_handles_sigint(): +def test_run_event_loop_handles_sigint() -> None: def trigger_sigint() -> None: # Allow the event loop to start before raising the signal. time.sleep(0.1) diff --git a/api/python/slint/tests/test_structs.py b/api/python/slint/tests/test_structs.py index 5eea3d8b0eb..65eb5c6d088 100644 --- a/api/python/slint/tests/test_structs.py +++ b/api/python/slint/tests/test_structs.py @@ -4,8 +4,11 @@ from __future__ import annotations import importlib.util +import importlib.abc import inspect import sys +import typing +from types import ModuleType from pathlib import Path import pytest @@ -81,7 +84,7 @@ def test_user_structs_exported_and_builtin_hidden() -> None: @pytest.fixture -def generated_struct_module(tmp_path: Path): +def generated_struct_module(tmp_path: Path) -> ModuleType: slint_file = Path(__file__).with_name("test-load-file-source.slint") output_dir = tmp_path / "generated" config = GenerationConfig( @@ -100,11 +103,14 @@ def generated_struct_module(tmp_path: Path): module = importlib.util.module_from_spec(spec) sys.modules.pop(spec.name, None) sys.modules[spec.name] = module - spec.loader.exec_module(module) # type: ignore[arg-type] - return module + loader = typing.cast(importlib.abc.Loader, spec.loader) + loader.exec_module(module) + return typing.cast(ModuleType, module) -def test_struct_accepts_keywords_only(generated_struct_module) -> None: +def test_struct_accepts_keywords_only( + generated_struct_module: ModuleType, +) -> None: MyData = generated_struct_module.MyData with pytest.raises(TypeError, match="keyword arguments only"): @@ -115,7 +121,9 @@ def test_struct_accepts_keywords_only(generated_struct_module) -> None: assert instance.age == 42 -def test_struct_rejects_unknown_keywords(generated_struct_module) -> None: +def test_struct_rejects_unknown_keywords( + generated_struct_module: ModuleType, +) -> None: MyData = generated_struct_module.MyData with pytest.raises(TypeError, match="unexpected keyword"): # noqa: PT012 From b4671f6754f5c46d8698cbc0ffe69ea7b9d41727 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 21:28:18 +0800 Subject: [PATCH 50/52] fix(python): ruff --- api/python/slint/slint/core.pyi | 8 ++++++-- api/python/slint/slint/loop.py | 4 ++-- api/python/slint/tests/test_gc.py | 4 ++-- api/python/slint/tests/test_models.py | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/api/python/slint/slint/core.pyi b/api/python/slint/slint/core.pyi index 37e22ad674b..bf9e92e515f 100644 --- a/api/python/slint/slint/core.pyi +++ b/api/python/slint/slint/core.pyi @@ -187,7 +187,9 @@ class Compiler: self, path: builtins.str | os.PathLike[builtins.str] | pathlib.Path ) -> CompilationResult: ... def build_from_source( - self, source_code: builtins.str, path: builtins.str | os.PathLike[builtins.str] | pathlib.Path + self, + source_code: builtins.str, + path: builtins.str | os.PathLike[builtins.str] | pathlib.Path, ) -> CompilationResult: ... @typing.final @@ -293,7 +295,9 @@ class Image: """ def __new__(cls) -> Self: ... @staticmethod - def load_from_path(path: builtins.str | os.PathLike[builtins.str] | pathlib.Path) -> Image: + def load_from_path( + path: builtins.str | os.PathLike[builtins.str] | pathlib.Path, + ) -> Image: r""" Loads the image from the specified path. Returns None if the image can't be loaded. """ diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index 69b28f84979..29300ef569d 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -300,7 +300,7 @@ def call_soon(self, callback, *args, context=None) -> asyncio.TimerHandle: # ty when=self.time(), callback=callback, args=args, loop=self, context=context ) self._soon_tasks.append(handle) - self.call_later(0, self._flush_soon_tasks) # type: ignore + self.call_later(0, self._flush_soon_tasks) # type: ignore return handle def _flush_soon_tasks(self) -> None: @@ -333,4 +333,4 @@ def _write_to_self(self) -> None: if isinstance(selector, _SlintSelector): selector._wakeup() else: - asyncio.SelectorEventLoop._write_to_self(self) # type: ignore + asyncio.SelectorEventLoop._write_to_self(self) # type: ignore diff --git a/api/python/slint/tests/test_gc.py b/api/python/slint/tests/test_gc.py index 9579b210d08..cc934f02b19 100644 --- a/api/python/slint/tests/test_gc.py +++ b/api/python/slint/tests/test_gc.py @@ -66,7 +66,7 @@ def test_struct_gc() -> None: instance: core.ComponentInstance | None = compdef.create() assert instance is not None - model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) # type: ignore + model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) # type: ignore assert model assert model.row_count() == 3 @@ -94,7 +94,7 @@ def test_properties_gc() -> None: instance: core.ComponentInstance | None = compdef.create() assert instance is not None - model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) # type: ignore + model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) # type: ignore assert model assert model.row_count() == 3 diff --git a/api/python/slint/tests/test_models.py b/api/python/slint/tests/test_models.py index 8a7b1e62dc2..487fbb2036b 100644 --- a/api/python/slint/tests/test_models.py +++ b/api/python/slint/tests/test_models.py @@ -46,9 +46,9 @@ def test_model_notify() -> None: assert instance.get_property("layout-height") == 100 model.set_row_data(1, 50) assert instance.get_property("layout-height") == 150 - model.append(75) # type: ignore + model.append(75) # type: ignore assert instance.get_property("layout-height") == 225 - del model[1:] # type: ignore + del model[1:] # type: ignore assert instance.get_property("layout-height") == 100 assert isinstance(instance.get_property("fixed-height-model"), models.ListModel) From e903396e7e9095c1f50828ca97ed92fbdfd14eb7 Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 21:48:54 +0800 Subject: [PATCH 51/52] fix(python): mypy, ruff --- api/python/slint/slint/loop.py | 23 ++++++++----------- api/python/slint/slint/models.py | 6 ++--- .../tests/codegen/test_emitters_package.py | 4 +++- api/python/slint/tests/test_enums.py | 6 ++--- api/python/slint/tests/test_gc.py | 4 ++-- api/python/slint/tests/test_models.py | 4 ++-- api/python/slint/tests/test_structs.py | 6 ++--- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index 29300ef569d..d878464bdaa 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -198,7 +198,7 @@ async def loop_stopper(event: asyncio.Event) -> None: self._stopping = False asyncio.events._set_running_loop(None) - def run_until_complete[T](self, future: typing.Awaitable[T]) -> T | None: + def run_until_complete[T](self, future: typing.Awaitable[T]) -> T: if self._core_loop_started and not self._core_loop_running: return asyncio.selector_events.BaseSelectorEventLoop.run_until_complete( self, future @@ -217,16 +217,13 @@ def stop_loop(future: typing.Any) -> None: if future.done(): return future.result() - else: - if self.stop_run_forever_event.is_set(): - raise RuntimeError("run_until_complete's future isn't done", future) - else: - # If the loop was quit for example because the user closed the last window, then - # don't thrown an error but return a None sentinel. The return value of asyncio.run() - # isn't used by slint.run_event_loop() anyway - # TODO: see if we can properly cancel the future by calling cancel() and throwing - # the task cancellation exception. - return None + if self.stop_run_forever_event.is_set(): + raise RuntimeError("run_until_complete's future isn't done", future) + + future.cancel() + raise RuntimeError( + "run_until_complete future was cancelled before completion", future + ) def _run_forever_setup(self) -> None: pass @@ -300,7 +297,7 @@ def call_soon(self, callback, *args, context=None) -> asyncio.TimerHandle: # ty when=self.time(), callback=callback, args=args, loop=self, context=context ) self._soon_tasks.append(handle) - self.call_later(0, self._flush_soon_tasks) # type: ignore + self.call_later(0, self._flush_soon_tasks) return handle def _flush_soon_tasks(self) -> None: @@ -333,4 +330,4 @@ def _write_to_self(self) -> None: if isinstance(selector, _SlintSelector): selector._wakeup() else: - asyncio.SelectorEventLoop._write_to_self(self) # type: ignore + asyncio.SelectorEventLoop._write_to_self(self) # type: ignore[attr-defined] diff --git a/api/python/slint/slint/models.py b/api/python/slint/slint/models.py index c9a8d04ca5a..18f89bd5ab9 100644 --- a/api/python/slint/slint/models.py +++ b/api/python/slint/slint/models.py @@ -52,7 +52,7 @@ def __init__(self, iterable: typing.Optional[Iterable[T]] = None) -> None: super().__init__() items = list(iterable) if iterable is not None else [] - self.list = typing.cast(list[T], items) + self.list: list[T] = items def row_count(self) -> int: return len(self.list) @@ -94,6 +94,4 @@ def __next__(self) -> T: raise StopIteration() index = self.index self.index += 1 - data = self.model.row_data(index) - assert data is not None - return typing.cast(T, data) + return self.model.row_data(index) # type: ignore diff --git a/api/python/slint/tests/codegen/test_emitters_package.py b/api/python/slint/tests/codegen/test_emitters_package.py index 5980e1dd570..4a5e3150bf5 100644 --- a/api/python/slint/tests/codegen/test_emitters_package.py +++ b/api/python/slint/tests/codegen/test_emitters_package.py @@ -74,7 +74,9 @@ def _normalize_prop(name: str) -> str: sys.modules.pop(name, None) -def test_write_package_emitters(tmp_path: Path, emitters_modules) -> None: +def test_write_package_emitters( + tmp_path: Path, emitters_modules: tuple[types.ModuleType, types.ModuleType] +) -> None: emitters, models = emitters_modules artifacts = models.ModuleArtifacts( diff --git a/api/python/slint/tests/test_enums.py b/api/python/slint/tests/test_enums.py index 7288ddf1c23..6d21bbaadb1 100644 --- a/api/python/slint/tests/test_enums.py +++ b/api/python/slint/tests/test_enums.py @@ -7,7 +7,6 @@ import importlib.util import sys import inspect -import typing from pathlib import Path from types import ModuleType @@ -45,9 +44,10 @@ def generated_module(tmp_path: Path) -> ModuleType: sys.modules.pop(spec.name, None) module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module - loader = typing.cast(importlib.abc.Loader, spec.loader) + loader = spec.loader + assert isinstance(loader, importlib.abc.Loader) loader.exec_module(module) - return typing.cast(ModuleType, module) + return module def test_enums(generated_module: ModuleType) -> None: diff --git a/api/python/slint/tests/test_gc.py b/api/python/slint/tests/test_gc.py index cc934f02b19..a333c527b6e 100644 --- a/api/python/slint/tests/test_gc.py +++ b/api/python/slint/tests/test_gc.py @@ -66,7 +66,7 @@ def test_struct_gc() -> None: instance: core.ComponentInstance | None = compdef.create() assert instance is not None - model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) # type: ignore + model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) assert model assert model.row_count() == 3 @@ -94,7 +94,7 @@ def test_properties_gc() -> None: instance: core.ComponentInstance | None = compdef.create() assert instance is not None - model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) # type: ignore + model: typing.Optional[slint.ListModel[int]] = slint.ListModel([1, 2, 3]) assert model assert model.row_count() == 3 diff --git a/api/python/slint/tests/test_models.py b/api/python/slint/tests/test_models.py index 487fbb2036b..651d0bf5c7c 100644 --- a/api/python/slint/tests/test_models.py +++ b/api/python/slint/tests/test_models.py @@ -46,9 +46,9 @@ def test_model_notify() -> None: assert instance.get_property("layout-height") == 100 model.set_row_data(1, 50) assert instance.get_property("layout-height") == 150 - model.append(75) # type: ignore + model.append(75) assert instance.get_property("layout-height") == 225 - del model[1:] # type: ignore + del model[1:] assert instance.get_property("layout-height") == 100 assert isinstance(instance.get_property("fixed-height-model"), models.ListModel) diff --git a/api/python/slint/tests/test_structs.py b/api/python/slint/tests/test_structs.py index 65eb5c6d088..2110299a797 100644 --- a/api/python/slint/tests/test_structs.py +++ b/api/python/slint/tests/test_structs.py @@ -7,7 +7,6 @@ import importlib.abc import inspect import sys -import typing from types import ModuleType from pathlib import Path @@ -103,9 +102,10 @@ def generated_struct_module(tmp_path: Path) -> ModuleType: module = importlib.util.module_from_spec(spec) sys.modules.pop(spec.name, None) sys.modules[spec.name] = module - loader = typing.cast(importlib.abc.Loader, spec.loader) + loader = spec.loader + assert isinstance(loader, importlib.abc.Loader) loader.exec_module(module) - return typing.cast(ModuleType, module) + return module def test_struct_accepts_keywords_only( From 7a077a4f822ba0c2e474546f49e699801fe288db Mon Sep 17 00:00:00 2001 From: GreyElaina Date: Wed, 29 Oct 2025 23:21:41 +0800 Subject: [PATCH 52/52] fix(python): alright then. run_until_complete() -> T | None --- api/python/slint/slint/loop.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/api/python/slint/slint/loop.py b/api/python/slint/slint/loop.py index d878464bdaa..af5e7f249a7 100644 --- a/api/python/slint/slint/loop.py +++ b/api/python/slint/slint/loop.py @@ -198,7 +198,7 @@ async def loop_stopper(event: asyncio.Event) -> None: self._stopping = False asyncio.events._set_running_loop(None) - def run_until_complete[T](self, future: typing.Awaitable[T]) -> T: + def run_until_complete[T](self, future: typing.Awaitable[T]) -> T | None: # type: ignore[override] if self._core_loop_started and not self._core_loop_running: return asyncio.selector_events.BaseSelectorEventLoop.run_until_complete( self, future @@ -220,10 +220,15 @@ def stop_loop(future: typing.Any) -> None: if self.stop_run_forever_event.is_set(): raise RuntimeError("run_until_complete's future isn't done", future) - future.cancel() - raise RuntimeError( - "run_until_complete future was cancelled before completion", future - ) + # The Slint core event loop can terminate even though the awaiting coroutine + # is still running (for example when the user closes the last window). Python's + # BaseEventLoop would raise a RuntimeError in that case, but Slint's API expects + # a graceful shutdown without surfacing an exception to the caller. Returning + # None here mirrors the historical behaviour and avoids breaking applications. + # Attempts at cancelling the Task at this point still leave it pending because + # the underlying loop has already stopped, so we cannot currently satisfy the + # TODO of propagating a proper CancelledError. + return None def _run_forever_setup(self) -> None: pass