diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7fa8f8ab..7af4a6f3d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### ⚠️ Breaking Changes ⚠️ +- Python: Added the `type_name` field for custom type configs. This is required when the name of + the custom type differs from the Python type name. - The Rust side of the custom type system has changed and users will need to update their code. The `UniffiCustomTypeConverter` trait is no longer used, use the `custom_type!` macro instead. We did this to help fix some edge-cases with custom types wrapping types from other crates (eg, Url). diff --git a/docs/manual/src/udl/custom_types.md b/docs/manual/src/udl/custom_types.md index fd3310ace8..5c20f476cd 100644 --- a/docs/manual/src/udl/custom_types.md +++ b/docs/manual/src/udl/custom_types.md @@ -219,10 +219,7 @@ lower = "{}.toString()" Here's how the configuration works in `uniffi.toml`. * Create a `[bindings.{language}.custom_types.{CustomTypeName}]` table to enable a custom type on a bindings side. This has several subkeys: - * `type_name` (Optional, Typed languages only): Type/class name for the - custom type. Defaults to the type name used in the UDL. Note: The UDL - type name will still be used in generated function signatures, however it - will be defined as a typealias to this type. + * `type_name` (Optional): Type/class name for the custom type. Defaults to the type name used in the proc macro. * `lift`: Expression to convert the UDL type to the custom type. `{}` will be replaced with the value of the UDL type. * `lower`: Expression to convert the custom type to the UDL type. `{}` will be replaced with the value of the custom type. * `imports` (Optional) list of modules to import for your `lift`/`lower` functions. diff --git a/examples/custom-types/uniffi.toml b/examples/custom-types/uniffi.toml index 3af45bf952..d8ba782808 100644 --- a/examples/custom-types/uniffi.toml +++ b/examples/custom-types/uniffi.toml @@ -48,8 +48,8 @@ from_custom = "{}.toString()" [bindings.python.custom_types.Url] # We're going to be the urllib.parse.ParseResult class, which is the closest -# thing Python has to a Url class. No need to specify `type_name` though, -# since Python is loosely typed. +# thing Python has to a Url class. +type_name = "urllib.parse.ParseResult" # modules to import imports = ["urllib.parse"] # Functions to convert between strings and the ParsedUrl class diff --git a/fixtures/futures/tests/bindings/test_futures.py b/fixtures/futures/tests/bindings/test_futures.py index 6a3ba92cd1..a25794b5b6 100644 --- a/fixtures/futures/tests/bindings/test_futures.py +++ b/fixtures/futures/tests/bindings/test_futures.py @@ -77,13 +77,13 @@ async def test(): asyncio.run(test()) def test_async_constructors(self): - # Check the default constructor has been disabled. + # Async constructor's can't be called directly with self.assertRaises(ValueError) as e: Megaphone() - self.assertTrue(str(e.exception).startswith("async constructors not supported")) async def test(): - megaphone = await Megaphone.secondary() + # Instead, users should use the `new` classmethod. + megaphone = await Megaphone.new() result_alice = await megaphone.say_after(0, 'Alice') self.assertEqual(result_alice, 'HELLO, ALICE!') diff --git a/uniffi_bindgen/src/bindings/python/gen_python.rs b/uniffi_bindgen/src/bindings/python/gen_python.rs new file mode 100644 index 0000000000..ae3f6ffc03 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/gen_python.rs @@ -0,0 +1,282 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::collections::HashMap; +use std::fmt::Debug; + +use anyhow::{Context, Result}; +use heck::ToSnakeCase; +use rinja::Template; +use serde::{Deserialize, Serialize}; + +use crate::{ + backend::{filters::to_rinja_error, TemplateExpression}, + interface::ir::*, + interface::ComponentInterface, +}; + +mod visit_mut; + +use visit_mut::{BindingsIrVisitor, Protocol, Runtimes}; + +// Config options to customize the generated python. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + pub(super) cdylib_name: Option, + #[serde(default)] + custom_types: HashMap, + #[serde(default)] + external_packages: HashMap, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CustomTypeConfig { + type_name: Option, + imports: Option>, + into_custom: TemplateExpression, + from_custom: TemplateExpression, +} + +impl Config { + pub fn cdylib_name(&self) -> String { + if let Some(cdylib_name) = &self.cdylib_name { + cdylib_name.clone() + } else { + "uniffi".into() + } + } + + /// Get the package name for a given external namespace. + pub fn module_for_namespace(&self, ns: &str) -> String { + let ns = ns.to_string().to_snake_case(); + match self.external_packages.get(&ns) { + None => format!(".{ns}"), + Some(value) if value.is_empty() => ns, + Some(value) => format!("{value}.{ns}"), + } + } +} + +// Generate python bindings for the given ComponentInterface, as a string. +pub fn generate_python_bindings(config: &Config, ci: &ComponentInterface) -> Result { + PythonBindingsIr::new(config.clone(), ci.clone())? + .render() + .context("failed to render python bindings") +} + +/// Specializes the BindingsIr for Python +#[derive(Template)] +#[template(syntax = "py", escape = "none", path = "wrapper.py")] +struct PythonBindingsIr { + imports: Vec, + ffi_definitions: Vec, + checksum_checks: Vec, + type_definitions: Vec, + functions: Vec, + module_docstring: Option, + globals: GlobalDefinitions, + protocols: Vec, + cdylib_name: String, + runtimes: Runtimes, + /// names to export via the __all__ object + exports: Vec, +} + +impl PythonBindingsIr { + fn new(config: Config, ci: ComponentInterface) -> Result { + let mut ir: BindingsIr = ci.clone().try_into()?; + let cdylib_name = config.cdylib_name(); + let mut visitor = BindingsIrVisitor::new(config); + ir.visit_mut(&mut visitor)?; + Ok(Self { + imports: visitor.imports.into_iter().collect(), + ffi_definitions: ir.ffi_definitions, + checksum_checks: ir.checksum_checks, + type_definitions: ir.type_definitions, + functions: ir.functions, + module_docstring: ir.crate_docstring, + globals: ir.globals, + protocols: visitor.protocols, + cdylib_name, + runtimes: visitor.runtimes, + exports: visitor.exports, + }) + } +} + +#[derive(Clone, Default)] +pub struct PythonCodeOracle; + +pub mod filters { + + use super::*; + + /// Fetch the `type_name` value set in `visit_mut.rs` + pub fn type_name(node: impl AsType) -> rinja::Result { + node.as_type() + .lang_data + .get("type_name") + .ok_or_else(|| to_rinja_error(&format!("Error fetching `type_name` {node:?}"))) + } + + /// Fetch the `ffi_converter_name` value set in `visit_mut.rs` + pub fn ffi_converter_name(node: impl AsType) -> rinja::Result { + node.as_type() + .lang_data + .get("ffi_converter_name") + .ok_or_else(|| to_rinja_error(&format!("Error fetching `ffi_converter_name` {node:?}"))) + } + + /// Fetch the `ffi_type_name` value set in `visit_mut.rs` + pub fn ffi_type_name(node: impl AsFfiType) -> rinja::Result { + node.as_ffi_type() + .lang_data + .get("ffi_type_name") + .ok_or_else(|| to_rinja_error(&format!("Error fetching `ffi_type_name` {node:?}"))) + } + + /// Fetch the `protocol_name` value set in `visit_mut.rs` + pub fn protocol_name(node: &Interface) -> rinja::Result { + node.lang_data + .get("protocol_name") + .ok_or_else(|| to_rinja_error(&format!("Error fetching `protocol_name` {node:?}"))) + } + + /// Fetch the `literal` value set in `visit_mut.rs` + pub fn literal(node: &Literal) -> rinja::Result { + node.lang_data + .get("rendered_literal") + .ok_or_else(|| to_rinja_error(&format!("Error fetching `rendered_literal` {node:?}"))) + } + + /// Fetch the `ffi_default_return` value set in `visit_mut.rs` + pub fn ffi_default_return(node: &ReturnType) -> rinja::Result { + node.lang_data + .get("ffi_default") + .ok_or_else(|| to_rinja_error(&format!("Error fetching `ffi_default` {node:?}"))) + } + + /// Fetch the `base_classes` value set in `visit_mut.rs` + pub fn base_classes(node: impl Node) -> rinja::Result { + node.lang_data() + .get("base_classes") + .ok_or_else(|| to_rinja_error(&format!("Error fetching `base_classes` {node:?}"))) + } + + /// Fetch the `custom_type_config` value set in `visit_mut.rs` + pub fn custom_type_config(node: &CustomType) -> rinja::Result> { + Ok(node.lang_data.get("custom_type_config")) + } + + /// Fetch the `had_async_constructor` value set in `visit_mut.rs` + pub fn had_async_constructor(node: &Interface) -> rinja::Result { + Ok(node.lang_data.get("had_async_constructor").unwrap_or(false)) + } + + pub fn lower_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.lower", ffi_converter_name(node)?)) + } + + pub fn check_lower_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.check_lower", ffi_converter_name(node)?)) + } + + pub fn lift_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.lift", ffi_converter_name(node)?)) + } + + pub fn write_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.write", ffi_converter_name(node)?)) + } + + pub fn read_fn(node: impl AsType) -> rinja::Result { + Ok(format!("{}.read", ffi_converter_name(node)?)) + } + + pub fn return_type(callable: impl Callable) -> rinja::Result { + match &callable.return_type().ty { + Some(ty) => type_name(ty), + None => Ok("None".to_string()), + } + } + + /// Generate `def` or `async def` for a callable + pub fn def(callable: impl Callable) -> rinja::Result { + if callable.is_async() { + Ok("async def".to_string()) + } else { + Ok("def".to_string()) + } + } + + /// Generate a comma-separated list argument names and types + pub fn arg_list(callable: impl Callable) -> rinja::Result { + let args = callable.arguments().iter().map(|a| { + let ty = type_name(a)?; + let name = &a.name; + Ok(match &a.default { + Some(_) => format!("{name}: typing.Union[object, {ty}] = _DEFAULT"), + None => format!("{name}: {ty}"), + }) + }); + let self_arg = (callable.is_method() || callable.is_primary_constructor()) + .then(|| Ok("self".to_string())); + + Ok(self_arg + .into_iter() + .chain(args) + .collect::>>()? + .join(", ")) + } + + /// Get the FFI converter for a throws type + /// + /// Returns "None" if there isn't a throws type. + pub fn error_ffi_converter(callable: impl Callable) -> rinja::Result { + Ok(match &callable.throws_type().ty { + Some(error_type) => { + let ffi_converter_name = ffi_converter_name(error_type)?; + match &error_type.kind { + TypeKind::Interface { .. } => format!("{ffi_converter_name}__as_error"), + _ => ffi_converter_name, + } + } + None => "None".to_string(), + }) + } + + pub fn ffi_return_type(node: impl FfiCallable) -> rinja::Result { + match &node.return_type().ty { + Some(ty) => ffi_type_name(ty), + None => Ok("None".to_string()), + } + } + + /// Indent a docstring + /// + /// For Some values will indent each line, except the first by `spaces`. + /// For None, it will return the empty string. + /// + /// This gets both cases right with template code that looks like this: + /// + /// ```python + /// {{ meth.docstring|docindent(4) }} + /// fn {{ meth.name }}(...) + /// ``` + pub fn docindent(docstring: &Option, spaces: usize) -> rinja::Result { + Ok(match docstring { + None => "".to_string(), + Some(docstring) => { + let mut output = String::new(); + let leading_space = " ".repeat(spaces); + for line in docstring.split('\n') { + output.push_str(line); + output.push('\n'); + output.push_str(&leading_space); + } + output + } + }) + } +} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs b/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs deleted file mode 100644 index 7793fde625..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/callback_interface.rs +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; -use crate::{backend::Literal, bail, Result}; - -#[derive(Debug)] -pub struct CallbackInterfaceCodeType { - id: String, -} - -impl CallbackInterfaceCodeType { - pub fn new(id: String) -> Self { - Self { id } - } -} - -impl CodeType for CallbackInterfaceCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.id) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } - - fn literal(&self, _literal: &Literal) -> Result { - bail!("literals not supported here") - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs b/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs deleted file mode 100644 index b06bfc9974..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/compounds.rs +++ /dev/null @@ -1,118 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; -use crate::{ - backend::{Literal, Type}, - bail, - bindings::python::gen_python::AsCodeType, - Result, -}; - -#[derive(Debug)] -pub struct OptionalCodeType { - inner: Type, -} - -impl OptionalCodeType { - pub fn new(inner: Type) -> Self { - Self { inner } - } -} - -impl CodeType for OptionalCodeType { - fn type_label(&self) -> String { - format!( - "typing.Optional[{}]", - super::PythonCodeOracle.find(&self.inner).type_label() - ) - } - - fn canonical_name(&self) -> String { - format!( - "Optional{}", - super::PythonCodeOracle.find(&self.inner).canonical_name(), - ) - } - - fn literal(&self, literal: &Literal) -> Result { - match literal { - Literal::None => Ok("None".into()), - Literal::Some { inner } => super::PythonCodeOracle.find(&self.inner).literal(inner), - _ => bail!("Invalid literal for Optional type: {literal:?}"), - } - } -} - -#[derive(Debug)] -pub struct SequenceCodeType { - inner: Type, -} - -impl SequenceCodeType { - pub fn new(inner: Type) -> Self { - Self { inner } - } -} - -impl CodeType for SequenceCodeType { - fn type_label(&self) -> String { - // Python 3.8 and below do not support `list[T]` - format!( - "typing.List[{}]", - super::PythonCodeOracle.find(&self.inner).type_label() - ) - } - - fn canonical_name(&self) -> String { - format!( - "Sequence{}", - super::PythonCodeOracle.find(&self.inner).canonical_name(), - ) - } - - fn literal(&self, literal: &Literal) -> Result { - match literal { - Literal::EmptySequence => Ok("[]".into()), - _ => bail!("Invalid literal for sequence type: {literal:?}"), - } - } -} - -#[derive(Debug)] -pub struct MapCodeType { - key: Type, - value: Type, -} - -impl MapCodeType { - pub fn new(key: Type, value: Type) -> Self { - Self { key, value } - } -} - -impl CodeType for MapCodeType { - fn type_label(&self) -> String { - format!( - "dict[{}, {}]", - self.key.as_codetype().type_label(), - self.value.as_codetype().type_label() - ) - } - - fn canonical_name(&self) -> String { - format!( - "Map{}{}", - super::PythonCodeOracle.find(&self.key).canonical_name(), - super::PythonCodeOracle.find(&self.value).canonical_name(), - ) - } - - fn literal(&self, literal: &Literal) -> Result { - match literal { - Literal::EmptyMap => Ok("{}".into()), - _ => bail!("Invalid literal for map type: {literal:?}"), - } - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/custom.rs b/uniffi_bindgen/src/bindings/python/gen_python/custom.rs deleted file mode 100644 index 8782339170..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/custom.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -#[derive(Debug)] -pub struct CustomCodeType { - name: String, -} - -impl CustomCodeType { - pub fn new(name: String) -> Self { - Self { name } - } -} - -impl CodeType for CustomCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.name) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs b/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs deleted file mode 100644 index 30419c546e..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/enum_.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; -use crate::{backend::Literal, bail, Result}; - -#[derive(Debug)] -pub struct EnumCodeType { - id: String, -} - -impl EnumCodeType { - pub fn new(id: String) -> Self { - Self { id } - } -} - -impl CodeType for EnumCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.id) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } - - fn literal(&self, literal: &Literal) -> Result { - if let Literal::Enum(v, _) = literal { - Ok(format!( - "{}.{}", - self.type_label(), - super::PythonCodeOracle.enum_variant_name(v) - )) - } else { - bail!("Invalid literal for enum type: {literal:?}") - } - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/external.rs b/uniffi_bindgen/src/bindings/python/gen_python/external.rs deleted file mode 100644 index c98738eba4..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/external.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -#[derive(Debug)] -pub struct ExternalCodeType { - name: String, -} - -impl ExternalCodeType { - pub fn new(name: String) -> Self { - Self { name } - } -} - -impl CodeType for ExternalCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.name) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs b/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs deleted file mode 100644 index 1ee22b95a4..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/miscellany.rs +++ /dev/null @@ -1,30 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -use paste::paste; - -macro_rules! impl_code_type_for_miscellany { - ($T:ty, $canonical_name:literal) => { - paste! { - #[derive(Debug)] - pub struct $T; - - impl CodeType for $T { - fn type_label(&self) -> String { - format!("{}", $canonical_name) - } - - fn canonical_name(&self) -> String { - format!("{}", $canonical_name) - } - } - } - }; -} - -impl_code_type_for_miscellany!(TimestampCodeType, "Timestamp"); - -impl_code_type_for_miscellany!(DurationCodeType, "Duration"); diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs deleted file mode 100644 index 32f9674722..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ /dev/null @@ -1,678 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use anyhow::{bail, Context, Result}; -use rinja::Template; - -use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; -use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; -use std::cell::RefCell; -use std::collections::{BTreeSet, HashMap, HashSet}; -use std::fmt::Debug; - -use crate::backend::TemplateExpression; - -use crate::interface::*; -use crate::VisitMut; - -mod callback_interface; -mod compounds; -mod custom; -mod enum_; -mod external; -mod miscellany; -mod object; -mod primitives; -mod record; - -/// A trait tor the implementation. -trait CodeType: Debug { - /// The language specific label used to reference this type. This will be used in - /// method signatures and property declarations. - fn type_label(&self) -> String; - - /// A representation of this type label that can be used as part of another - /// identifier. e.g. `read_foo()`, or `FooInternals`. - /// - /// This is especially useful when creating specialized objects or methods to deal - /// with this type only. - fn canonical_name(&self) -> String { - self.type_label() - } - - fn literal(&self, _literal: &Literal) -> Result { - bail!("Unimplemented for {}", self.type_label()) - } - - /// Name of the FfiConverter - /// - /// This is the object that contains the lower, write, lift, and read methods for this type. - fn ffi_converter_name(&self) -> String { - format!("FfiConverter{}", self.canonical_name()) - } - - /// A list of imports that are needed if this type is in use. - /// Classes are imported exactly once. - fn imports(&self) -> Option> { - None - } - - /// Function to run at startup - fn initialization_fn(&self) -> Option { - None - } -} - -// Taken from Python's `keyword.py` module. -static KEYWORDS: Lazy> = Lazy::new(|| { - let kwlist = vec![ - "False", - "None", - "True", - "__peg_parser__", - "and", - "as", - "assert", - "async", - "await", - "break", - "class", - "continue", - "def", - "del", - "elif", - "else", - "except", - "finally", - "for", - "from", - "global", - "if", - "import", - "in", - "is", - "lambda", - "nonlocal", - "not", - "or", - "pass", - "raise", - "return", - "try", - "while", - "with", - "yield", - ]; - HashSet::from_iter(kwlist.into_iter().map(|s| s.to_string())) -}); - -// Config options to customize the generated python. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct Config { - pub(super) cdylib_name: Option, - #[serde(default)] - custom_types: HashMap, - #[serde(default)] - external_packages: HashMap, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct CustomTypeConfig { - // This `CustomTypeConfig` doesn't have a `type_name` like the others -- which is why we have - // separate structs rather than a shared one. - imports: Option>, - into_custom: TemplateExpression, - from_custom: TemplateExpression, -} - -impl Config { - pub fn cdylib_name(&self) -> String { - if let Some(cdylib_name) = &self.cdylib_name { - cdylib_name.clone() - } else { - "uniffi".into() - } - } - - /// Get the package name for a given external namespace. - pub fn module_for_namespace(&self, ns: &str) -> String { - let ns = ns.to_string().to_snake_case(); - match self.external_packages.get(&ns) { - None => format!(".{ns}"), - Some(value) if value.is_empty() => ns, - Some(value) => format!("{value}.{ns}"), - } - } -} - -// Generate python bindings for the given ComponentInterface, as a string. -pub fn generate_python_bindings(config: &Config, ci: &mut ComponentInterface) -> Result { - PythonWrapper::new(config.clone(), ci) - .render() - .context("failed to render python bindings") -} - -/// A struct to record a Python import statement. -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub enum ImportRequirement { - /// A simple module import. - Module { mod_name: String }, - /// A single symbol from a module. - Symbol { - mod_name: String, - symbol_name: String, - }, - /// A single symbol from a module with the specified local name. - SymbolAs { - mod_name: String, - symbol_name: String, - as_name: String, - }, -} - -impl ImportRequirement { - /// Render the Python import statement. - fn render(&self) -> String { - match &self { - ImportRequirement::Module { mod_name } => format!("import {mod_name}"), - ImportRequirement::Symbol { - mod_name, - symbol_name, - } => format!("from {mod_name} import {symbol_name}"), - ImportRequirement::SymbolAs { - mod_name, - symbol_name, - as_name, - } => format!("from {mod_name} import {symbol_name} as {as_name}"), - } - } -} - -/// Renders Python helper code for all types -/// -/// This template is a bit different than others in that it stores internal state from the render -/// process. Make sure to only call `render()` once. -#[derive(Template)] -#[template(syntax = "py", escape = "none", path = "Types.py")] -pub struct TypeRenderer<'a> { - python_config: &'a Config, - ci: &'a ComponentInterface, - // Track included modules for the `include_once()` macro - include_once_names: RefCell>, - // Track imports added with the `add_import()` macro - imports: RefCell>, -} - -impl<'a> TypeRenderer<'a> { - fn new(python_config: &'a Config, ci: &'a ComponentInterface) -> Self { - Self { - python_config, - ci, - include_once_names: RefCell::new(HashSet::new()), - imports: RefCell::new(BTreeSet::new()), - } - } - - // The following methods are used by the `Types.py` macros. - - // Helper for the including a template, but only once. - // - // The first time this is called with a name it will return true, indicating that we should - // include the template. Subsequent calls will return false. - fn include_once_check(&self, name: &str) -> bool { - self.include_once_names - .borrow_mut() - .insert(name.to_string()) - } - - // Helper to add an import statement - // - // Call this inside your template to cause an import statement to be added at the top of the - // file. Imports will be sorted and de-deuped. - // - // Returns an empty string so that it can be used inside an rinja `{{ }}` block. - fn add_import(&self, name: &str) -> &str { - self.imports.borrow_mut().insert(ImportRequirement::Module { - mod_name: name.to_owned(), - }); - "" - } - - // Like add_import, but arranges for `from module import name`. - fn add_import_of(&self, mod_name: &str, name: &str) -> &str { - self.imports.borrow_mut().insert(ImportRequirement::Symbol { - mod_name: mod_name.to_owned(), - symbol_name: name.to_owned(), - }); - "" - } - - // Like add_import, but arranges for `from module import name as other`. - fn add_import_of_as(&self, mod_name: &str, symbol_name: &str, as_name: &str) -> &str { - self.imports - .borrow_mut() - .insert(ImportRequirement::SymbolAs { - mod_name: mod_name.to_owned(), - symbol_name: symbol_name.to_owned(), - as_name: as_name.to_owned(), - }); - "" - } - - // An inefficient algo to return type aliases needed for custom types - // in an order such that dependencies are in the correct order. - // Eg, if there's a custom type `Guid` -> `str` and another `GuidWrapper` -> `Guid`, - // it's important the type alias for `Guid` appears first. Fails to handle - // another level of indirection (eg, `A { builtin: C}, B { }, C { builtin: B })`) - // but that's pathological :) - fn get_custom_type_aliases(&self) -> Vec<(String, &Type)> { - let mut ordered = vec![]; - for type_ in self.ci.iter_types() { - if let Type::Custom { name, builtin, .. } = type_ { - match ordered.iter().position(|x: &(&str, &Type)| { - x.1.iter_types() - .any(|nested_type| *name == nested_type.as_codetype().type_label()) - }) { - // This 'name' appears as a builtin, so we must insert our type first. - Some(pos) => ordered.insert(pos, (name, builtin)), - // Otherwise at the end. - None => ordered.push((name, builtin)), - } - } - } - ordered - .into_iter() - .map(|(n, t)| (PythonCodeOracle.class_name(n), t)) - .collect() - } - - // Sort object types to avoid forward references; traits before everything else. - fn iter_sorted_object_types(&self) -> impl Iterator { - let mut obs: Vec<&Type> = self - .ci - .iter_types() - .filter(|t| matches!(t, Type::Object { .. })) - .collect(); - obs.sort_by_key(|t| !matches!(t, Type::Object { imp, .. } if imp.is_trait_interface())); - obs.into_iter() - } -} - -#[derive(Template)] -#[template(syntax = "py", escape = "none", path = "wrapper.py")] -pub struct PythonWrapper<'a> { - ci: &'a ComponentInterface, - config: Config, - type_helper_code: String, - type_imports: BTreeSet, -} -impl<'a> PythonWrapper<'a> { - pub fn new(config: Config, ci: &'a mut ComponentInterface) -> Self { - ci.visit_mut(&PythonCodeOracle); - - let type_renderer = TypeRenderer::new(&config, ci); - let type_helper_code = type_renderer.render().unwrap(); - let type_imports = type_renderer.imports.into_inner(); - - Self { - config, - ci, - type_helper_code, - type_imports, - } - } - - pub fn imports(&self) -> Vec { - self.type_imports.iter().cloned().collect() - } -} - -fn fixup_keyword(name: String) -> String { - if KEYWORDS.contains(&name) { - format!("_{name}") - } else { - name - } -} - -#[derive(Clone, Default)] -pub struct PythonCodeOracle; - -impl PythonCodeOracle { - fn find(&self, type_: &Type) -> Box { - type_.clone().as_type().as_codetype() - } - - /// Get the idiomatic Python rendering of a class name (for enums, records, errors, etc). - fn class_name(&self, nm: &str) -> String { - fixup_keyword(nm.to_string().to_upper_camel_case()) - } - - /// Get the idiomatic Python rendering of a function name. - fn fn_name(&self, nm: &str) -> String { - fixup_keyword(nm.to_string().to_snake_case()) - } - - /// Get the idiomatic Python rendering of a variable name. - fn var_name(&self, nm: &str) -> String { - fixup_keyword(nm.to_string().to_snake_case()) - } - - /// Get the idiomatic Python rendering of an individual enum variant. - fn enum_variant_name(&self, nm: &str) -> String { - fixup_keyword(nm.to_string().to_shouty_snake_case()) - } - - /// Get the idiomatic Python rendering of an FFI callback function name - fn ffi_callback_name(&self, nm: &str) -> String { - format!("_UNIFFI_{}", nm.to_shouty_snake_case()) - } - - /// Get the idiomatic Python rendering of an FFI struct name - fn ffi_struct_name(&self, nm: &str) -> String { - // The ctypes docs use both SHOUTY_SNAKE_CASE AND UpperCamelCase for structs. Let's use - // UpperCamelCase and reserve shouting for global variables - format!("_Uniffi{}", nm.to_upper_camel_case()) - } - - fn ffi_type_label(&self, ffi_type: &FfiType) -> String { - match ffi_type { - FfiType::Int8 => "ctypes.c_int8".to_string(), - FfiType::UInt8 => "ctypes.c_uint8".to_string(), - FfiType::Int16 => "ctypes.c_int16".to_string(), - FfiType::UInt16 => "ctypes.c_uint16".to_string(), - FfiType::Int32 => "ctypes.c_int32".to_string(), - FfiType::UInt32 => "ctypes.c_uint32".to_string(), - FfiType::Int64 => "ctypes.c_int64".to_string(), - FfiType::UInt64 => "ctypes.c_uint64".to_string(), - FfiType::Float32 => "ctypes.c_float".to_string(), - FfiType::Float64 => "ctypes.c_double".to_string(), - FfiType::Handle => "ctypes.c_uint64".to_string(), - FfiType::RustArcPtr(_) => "ctypes.c_void_p".to_string(), - FfiType::RustBuffer(maybe_external) => match maybe_external { - Some(external_meta) => format!("_UniffiRustBuffer{}", external_meta.name), - None => "_UniffiRustBuffer".to_string(), - }, - FfiType::RustCallStatus => "_UniffiRustCallStatus".to_string(), - FfiType::ForeignBytes => "_UniffiForeignBytes".to_string(), - FfiType::Callback(name) => self.ffi_callback_name(name), - FfiType::Struct(name) => self.ffi_struct_name(name), - // Pointer to an `asyncio.EventLoop` instance - FfiType::Reference(inner) | FfiType::MutReference(inner) => { - format!("ctypes.POINTER({})", self.ffi_type_label(inner)) - } - FfiType::VoidPointer => "ctypes.c_void_p".to_string(), - } - } - - /// Default values for FFI types - /// - /// Used to set a default return value when returning an error - fn ffi_default_value(&self, return_type: Option<&FfiType>) -> String { - match return_type { - Some(t) => match t { - FfiType::UInt8 - | FfiType::Int8 - | FfiType::UInt16 - | FfiType::Int16 - | FfiType::UInt32 - | FfiType::Int32 - | FfiType::UInt64 - | FfiType::Int64 => "0".to_owned(), - FfiType::Float32 | FfiType::Float64 => "0.0".to_owned(), - FfiType::RustArcPtr(_) => "ctypes.c_void_p()".to_owned(), - FfiType::RustBuffer(maybe_external) => match maybe_external { - Some(external_meta) => { - format!("_UniffiRustBuffer{}.default()", external_meta.name) - } - None => "_UniffiRustBuffer.default()".to_owned(), - }, - _ => unimplemented!("FFI return type: {t:?}"), - }, - // When we need to use a value for void returns, we use a `u8` placeholder - None => "0".to_owned(), - } - } -} - -impl VisitMut for PythonCodeOracle { - fn visit_record(&self, record: &mut Record) { - record.rename(self.class_name(record.name())); - } - - fn visit_object(&self, object: &mut Object) { - object.rename(self.class_name(object.name())); - for i in object.trait_impls_mut() { - i.trait_name = self.class_name(&i.trait_name); - // should i.tr_module_path be fixed? - } - } - - fn visit_field(&self, field: &mut Field) { - field.rename(self.var_name(field.name())); - } - - fn visit_ffi_field(&self, ffi_field: &mut FfiField) { - ffi_field.rename(self.var_name(ffi_field.name())); - } - - fn visit_ffi_argument(&self, ffi_argument: &mut FfiArgument) { - ffi_argument.rename(self.class_name(ffi_argument.name())); - } - - fn visit_enum(&self, _is_error: bool, enum_: &mut Enum) { - enum_.rename(self.class_name(enum_.name())); - } - - fn visit_enum_key(&self, key: &mut String) -> String { - self.class_name(key) - } - - fn visit_variant(&self, is_error: bool, variant: &mut Variant) { - if is_error { - variant.rename(self.class_name(variant.name())); - } else { - variant.rename(self.enum_variant_name(variant.name())); - } - } - - fn visit_type(&self, type_: &mut Type) { - // Renaming Types is a special case. We have simple types with names like - // an Object, but we also have types which have inner_types and builtin types. - // Which in turn have a different name. Therefore we pass the patterns as a - // function down to the renaming operation of the type itself, which can apply it - // to all its nested names if needed. - let name_transformer = |name: &str| self.class_name(name); - type_.rename_recursive(&name_transformer); - } - - fn visit_method(&self, method: &mut Method) { - method.rename(self.fn_name(method.name())); - } - - fn visit_argument(&self, argument: &mut Argument) { - argument.rename(self.var_name(argument.name())); - } - - fn visit_constructor(&self, constructor: &mut Constructor) { - if !constructor.is_primary_constructor() { - constructor.rename(self.fn_name(constructor.name())); - } - } - - fn visit_function(&self, function: &mut Function) { - // Conversions for wrapper.py - //TODO: Renaming the function name in wrapper.py is not currently tested - function.rename(self.fn_name(function.name())); - } - - fn visit_error_name(&self, name: &mut String) { - *name = self.class_name(name); - } -} - -trait AsCodeType { - fn as_codetype(&self) -> Box; -} - -impl AsCodeType for T { - fn as_codetype(&self) -> Box { - // Map `Type` instances to a `Box` for that type. - // - // There is a companion match in `templates/Types.py` which performs a similar function for the - // template code. - // - // - When adding additional types here, make sure to also add a match arm to the `Types.py` template. - // - To keep things manageable, let's try to limit ourselves to these 2 mega-matches - match self.as_type() { - Type::UInt8 => Box::new(primitives::UInt8CodeType), - Type::Int8 => Box::new(primitives::Int8CodeType), - Type::UInt16 => Box::new(primitives::UInt16CodeType), - Type::Int16 => Box::new(primitives::Int16CodeType), - Type::UInt32 => Box::new(primitives::UInt32CodeType), - Type::Int32 => Box::new(primitives::Int32CodeType), - Type::UInt64 => Box::new(primitives::UInt64CodeType), - Type::Int64 => Box::new(primitives::Int64CodeType), - Type::Float32 => Box::new(primitives::Float32CodeType), - Type::Float64 => Box::new(primitives::Float64CodeType), - Type::Boolean => Box::new(primitives::BooleanCodeType), - Type::String => Box::new(primitives::StringCodeType), - Type::Bytes => Box::new(primitives::BytesCodeType), - - Type::Timestamp => Box::new(miscellany::TimestampCodeType), - Type::Duration => Box::new(miscellany::DurationCodeType), - - Type::Enum { name, .. } => Box::new(enum_::EnumCodeType::new(name)), - Type::Object { name, .. } => Box::new(object::ObjectCodeType::new(name)), - Type::Record { name, .. } => Box::new(record::RecordCodeType::new(name)), - Type::CallbackInterface { name, .. } => { - Box::new(callback_interface::CallbackInterfaceCodeType::new(name)) - } - Type::Optional { inner_type } => { - Box::new(compounds::OptionalCodeType::new(*inner_type)) - } - Type::Sequence { inner_type } => { - Box::new(compounds::SequenceCodeType::new(*inner_type)) - } - Type::Map { - key_type, - value_type, - } => Box::new(compounds::MapCodeType::new(*key_type, *value_type)), - Type::External { name, .. } => Box::new(external::ExternalCodeType::new(name)), - Type::Custom { name, .. } => Box::new(custom::CustomCodeType::new(name)), - } - } -} - -pub mod filters { - use crate::backend::filters::to_rinja_error; - - use super::*; - - pub(super) fn type_name(as_ct: &impl AsCodeType) -> Result { - Ok(as_ct.as_codetype().type_label()) - } - - pub(super) fn ffi_converter_name(as_ct: &impl AsCodeType) -> Result { - Ok(String::from("_Uniffi") + &as_ct.as_codetype().ffi_converter_name()[3..]) - } - - pub(super) fn canonical_name(as_ct: &impl AsCodeType) -> Result { - Ok(as_ct.as_codetype().canonical_name()) - } - - pub(super) fn lift_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.lift", ffi_converter_name(as_ct)?)) - } - - pub(super) fn check_lower_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.check_lower", ffi_converter_name(as_ct)?)) - } - - pub(super) fn lower_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.lower", ffi_converter_name(as_ct)?)) - } - - pub(super) fn read_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.read", ffi_converter_name(as_ct)?)) - } - - pub(super) fn write_fn(as_ct: &impl AsCodeType) -> Result { - Ok(format!("{}.write", ffi_converter_name(as_ct)?)) - } - - pub(super) fn literal_py( - literal: &Literal, - as_ct: &impl AsCodeType, - ) -> Result { - as_ct - .as_codetype() - .literal(literal) - .map_err(|e| to_rinja_error(&e)) - } - - // Get the idiomatic Python rendering of an individual enum variant's discriminant - pub fn variant_discr_literal(e: &Enum, index: &usize) -> Result { - let literal = e - .variant_discr(*index) - .context("invalid index") - .map_err(|e| to_rinja_error(&e))?; - Type::UInt64 - .as_codetype() - .literal(&literal) - .map_err(|e| to_rinja_error(&e)) - } - - pub fn ffi_type_name(type_: &FfiType) -> Result { - Ok(PythonCodeOracle.ffi_type_label(type_)) - } - - pub fn ffi_default_value(return_type: Option) -> Result { - Ok(PythonCodeOracle.ffi_default_value(return_type.as_ref())) - } - - /// Get the idiomatic Python rendering of an FFI callback function name - pub fn ffi_callback_name(nm: &str) -> Result { - Ok(PythonCodeOracle.ffi_callback_name(nm)) - } - - /// Get the idiomatic Python rendering of an FFI struct name - pub fn ffi_struct_name(nm: &str) -> Result { - Ok(PythonCodeOracle.ffi_struct_name(nm)) - } - - /// Get the idiomatic Python rendering of docstring - pub fn docstring(docstring: &str, spaces: &i32) -> Result { - let docstring = textwrap::dedent(docstring); - // Escape triple quotes to avoid syntax error - let escaped = docstring.replace(r#"""""#, r#"\"\"\""#); - - let wrapped = format!("\"\"\"\n{escaped}\n\"\"\""); - - let spaces = usize::try_from(*spaces).unwrap_or_default(); - Ok(textwrap::indent(&wrapped, &" ".repeat(spaces))) - } -} - -#[cfg(test)] -mod tests { - #[test] - fn test_docstring_escape() { - let docstring = r#""""This is a docstring beginning with triple quotes. -Contains "quotes" in it. -It also has a triple quote: """ -And a even longer quote: """"""#; - - let expected = r#"""" -\"\"\"This is a docstring beginning with triple quotes. -Contains "quotes" in it. -It also has a triple quote: \"\"\" -And a even longer quote: \"\"\""" -""""#; - - assert_eq!(super::filters::docstring(docstring, &0).unwrap(), expected); - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/object.rs b/uniffi_bindgen/src/bindings/python/gen_python/object.rs deleted file mode 100644 index 092d09b61a..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/object.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -#[derive(Debug)] -pub struct ObjectCodeType { - id: String, -} - -impl ObjectCodeType { - pub fn new(id: String) -> Self { - Self { id } - } -} - -impl CodeType for ObjectCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.id) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs b/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs deleted file mode 100644 index 768f5fea7a..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/primitives.rs +++ /dev/null @@ -1,70 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; -use crate::{backend::Literal, bail, interface::Radix, Result}; -use paste::paste; - -fn render_literal(literal: &Literal) -> Result { - Ok(match literal { - Literal::Boolean(v) => { - if *v { - "True".into() - } else { - "False".into() - } - } - Literal::String(s) => format!("\"{s}\""), - // https://docs.python.org/3/reference/lexical_analysis.html#integer-literals - Literal::Int(i, radix, _) => match radix { - Radix::Octal => format!("int(0o{i:o})"), - Radix::Decimal => format!("{i}"), - Radix::Hexadecimal => format!("{i:#x}"), - }, - Literal::UInt(i, radix, _) => match radix { - Radix::Octal => format!("0o{i:o}"), - Radix::Decimal => format!("{i}"), - Radix::Hexadecimal => format!("{i:#x}"), - }, - Literal::Float(string, _type_) => string.clone(), - - _ => bail!("Invalid literal {literal:?}"), - }) -} - -macro_rules! impl_code_type_for_primitive { - ($T:ty, $python_name:literal, $canonical_name:literal) => { - paste! { - #[derive(Debug)] - pub struct $T; - impl CodeType for $T { - fn type_label(&self) -> String { - $python_name.into() - } - - fn canonical_name(&self) -> String { - $canonical_name.into() - } - - fn literal(&self, literal: &Literal) -> Result { - render_literal(&literal) - } - } - } - }; -} - -impl_code_type_for_primitive!(BooleanCodeType, "bool", "Bool"); -impl_code_type_for_primitive!(StringCodeType, "str", "String"); -impl_code_type_for_primitive!(BytesCodeType, "bytes", "Bytes"); -impl_code_type_for_primitive!(Int8CodeType, "int", "Int8"); -impl_code_type_for_primitive!(Int16CodeType, "int", "Int16"); -impl_code_type_for_primitive!(Int32CodeType, "int", "Int32"); -impl_code_type_for_primitive!(Int64CodeType, "int", "Int64"); -impl_code_type_for_primitive!(UInt8CodeType, "int", "UInt8"); -impl_code_type_for_primitive!(UInt16CodeType, "int", "UInt16"); -impl_code_type_for_primitive!(UInt32CodeType, "int", "UInt32"); -impl_code_type_for_primitive!(UInt64CodeType, "int", "UInt64"); -impl_code_type_for_primitive!(Float32CodeType, "float", "Float"); -impl_code_type_for_primitive!(Float64CodeType, "float", "Double"); diff --git a/uniffi_bindgen/src/bindings/python/gen_python/record.rs b/uniffi_bindgen/src/bindings/python/gen_python/record.rs deleted file mode 100644 index ef14343650..0000000000 --- a/uniffi_bindgen/src/bindings/python/gen_python/record.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::CodeType; - -#[derive(Debug)] -pub struct RecordCodeType { - id: String, -} - -impl RecordCodeType { - pub fn new(id: String) -> Self { - Self { id } - } -} - -impl CodeType for RecordCodeType { - fn type_label(&self) -> String { - super::PythonCodeOracle.class_name(&self.id) - } - - fn canonical_name(&self) -> String { - format!("Type{}", self.type_label()) - } -} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/visit_mut.rs b/uniffi_bindgen/src/bindings/python/gen_python/visit_mut.rs new file mode 100644 index 0000000000..ecf0b328d9 --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/gen_python/visit_mut.rs @@ -0,0 +1,569 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use anyhow::{bail, Result}; + +use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use once_cell::sync::Lazy; + +use std::collections::{BTreeSet, HashSet}; +use std::fmt::Debug; + +use super::Config; +use crate::interface::ir::*; + +// Taken from Python's `keyword.py` module. +static KEYWORDS: Lazy> = Lazy::new(|| { + let kwlist = vec![ + "False", + "None", + "True", + "__peg_parser__", + "and", + "as", + "assert", + "async", + "await", + "break", + "class", + "continue", + "def", + "del", + "elif", + "else", + "except", + "finally", + "for", + "from", + "global", + "if", + "import", + "in", + "is", + "lambda", + "nonlocal", + "not", + "or", + "pass", + "raise", + "return", + "try", + "while", + "with", + "yield", + ]; + HashSet::from_iter(kwlist.into_iter().map(|s| s.to_string())) +}); + +/// Implements `VisitMut` to specialize the BindingsIr for Python +pub struct BindingsIrVisitor { + pub config: Config, + // The following fields are populated as we walk the tree for VisitMut + pub imports: BTreeSet, + pub protocols: Vec, + pub runtimes: Runtimes, + pub exports: Vec, +} + +// Runtimes to generate +// +// These are sections of helper code that we load once +#[derive(Default, Debug)] +pub struct Runtimes { + pub async_: bool, + pub async_callback: bool, + pub callback_interface: bool, +} + +/// Protocol to define +pub struct Protocol { + pub name: String, + pub base_class: String, + pub docstring: Option, + pub methods: Vec, +} + +impl BindingsIrVisitor { + pub fn new(config: Config) -> Self { + Self { + config, + imports: BTreeSet::new(), + runtimes: Runtimes::default(), + protocols: vec![], + exports: vec![], + } + } + + /// Idiomatic Python class name (for enums, records, errors, etc). + fn class_name(&self, nm: &str) -> String { + self.fixup_keyword(nm.to_string().to_upper_camel_case()) + } + + /// Idiomatic Python function name. + fn fn_name(&self, nm: &str) -> String { + self.fixup_keyword(nm.to_string().to_snake_case()) + } + + /// Idiomatic Python a variable name. + fn var_name(&self, nm: &str) -> String { + self.fixup_keyword(nm.to_string().to_snake_case()) + } + + /// Idiomatic Python enum variant name. + /// + /// These use SHOUTY_SNAKE_CASE style. User's access them through `EnumName.VARIANT_NAME`. + /// This naming style is used for both flat enums and fielded ones. + /// + /// The exception is error enums. In that case, there's no enum in the generated python. + /// Instead there's a base class and subclasses -- all with UpperCamelCase names. + /// That case is not handled by this method. + fn enum_variant_name(&self, nm: &str) -> String { + self.fixup_keyword(nm.to_string().to_shouty_snake_case()) + } + + /// Python type name + fn type_name(&self, ty: &Type) -> String { + match &ty.kind { + TypeKind::Boolean => "bool".to_string(), + TypeKind::String => "str".to_string(), + TypeKind::Bytes => "bytes".to_string(), + TypeKind::Int8 => "int".to_string(), + TypeKind::Int16 + | TypeKind::Int32 + | TypeKind::Int64 + | TypeKind::UInt8 + | TypeKind::UInt16 + | TypeKind::UInt32 + | TypeKind::UInt64 => "int".to_string(), + TypeKind::Duration => "Duration".to_string(), + TypeKind::Timestamp => "Timestamp".to_string(), + TypeKind::Float32 | TypeKind::Float64 => "float".to_string(), + TypeKind::Interface { name, .. } + | TypeKind::Record { name, .. } + | TypeKind::Enum { name, .. } + | TypeKind::CallbackInterface { name, .. } + | TypeKind::Custom { name, .. } + | TypeKind::External { name, .. } => self.class_name(name), + TypeKind::Optional { inner_type } => { + format!("typing.Optional[{}]", self.type_name(inner_type)) + } + TypeKind::Sequence { inner_type } => { + format!("typing.List[{}]", self.type_name(inner_type)) + } + TypeKind::Map { + key_type, + value_type, + } => format!( + "dict[{}, {}]", + self.type_name(key_type), + self.type_name(value_type) + ), + } + } + + /// Python rendering of a literal value + fn literal(&self, lit: &Literal) -> Result { + Ok(match &lit.kind { + LiteralKind::Boolean(true) => "True".to_string(), + LiteralKind::Boolean(false) => "False".to_string(), + LiteralKind::String(s) => format!("\"{s}\""), + // https://docs.python.org/3/reference/lexical_analysis.html#integer-literals + LiteralKind::Int(i, radix, _) => match radix { + Radix::Octal => format!("int(0o{i:o})"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + LiteralKind::UInt(i, radix, _) => match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + LiteralKind::Float(value, _) => value.clone(), + LiteralKind::EmptySequence => "[]".to_string(), + LiteralKind::EmptyMap => "{}".to_string(), + LiteralKind::None => "None".to_string(), + LiteralKind::Some { inner } => self.literal(inner)?, + LiteralKind::Enum(variant, ty) => match &ty.kind { + TypeKind::Enum { name, .. } => { + format!( + "{}.{}", + self.class_name(name), + self.enum_variant_name(variant) + ) + } + _ => { + bail!("Invalid type for enum literal: {ty:?} ({lit:?})") + } + }, + }) + } + + /// Python type name for an FfiType + fn ffi_type_name(&self, ffi_type: &FfiType) -> String { + match &ffi_type.kind { + FfiTypeKind::Int8 => "ctypes.c_int8".to_string(), + FfiTypeKind::UInt8 => "ctypes.c_uint8".to_string(), + FfiTypeKind::Int16 => "ctypes.c_int16".to_string(), + FfiTypeKind::UInt16 => "ctypes.c_uint16".to_string(), + FfiTypeKind::Int32 => "ctypes.c_int32".to_string(), + FfiTypeKind::UInt32 => "ctypes.c_uint32".to_string(), + FfiTypeKind::Int64 => "ctypes.c_int64".to_string(), + FfiTypeKind::UInt64 => "ctypes.c_uint64".to_string(), + FfiTypeKind::Float32 => "ctypes.c_float".to_string(), + FfiTypeKind::Float64 => "ctypes.c_double".to_string(), + FfiTypeKind::Handle => "ctypes.c_uint64".to_string(), + FfiTypeKind::RustArcPtr(_) => "ctypes.c_void_p".to_string(), + FfiTypeKind::RustBuffer(meta) => match meta { + None => "_UniffiRustBuffer".to_string(), + Some(meta) => { + let module_name = self.config.module_for_namespace(&meta.namespace); + format!("{module_name}._UniffiRustBuffer") + } + }, + FfiTypeKind::RustCallStatus => "_UniffiRustCallStatus".to_string(), + FfiTypeKind::ForeignBytes => "_UniffiForeignBytes".to_string(), + FfiTypeKind::FunctionPointer(name) => self.ffi_function_type_name(name), + FfiTypeKind::Struct(name) => self.ffi_struct_name(name), + // Pointer to an `asyncio.EventLoop` instance + FfiTypeKind::Reference(inner) | FfiTypeKind::MutReference(inner) => { + format!("ctypes.POINTER({})", self.ffi_type_name(inner)) + } + FfiTypeKind::VoidPointer => "ctypes.c_void_p".to_string(), + } + } + + /// Idiomatic name for an FFI function type. + /// + /// These follow the ctypes convention of SHOUTY_SNAKE_CASE. Prefix with `_UNIFFI` so that + /// it can't conflict with user-defined items. + fn ffi_function_type_name(&self, nm: &str) -> String { + format!("_UNIFFI_{}", nm.to_shouty_snake_case()) + } + + /// Idiomatic name for an FFI struct. + /// + /// These follow the ctypes convention of UpperCamelCase (although the ctypes docs also uses + /// SHOUTY_SNAKE_CASE in some places). Prefix with `_Uniffi` so that it can't conflict with + /// user-defined items. + fn ffi_struct_name(&self, nm: &str) -> String { + format!("_Uniffi{}", nm.to_upper_camel_case()) + } + + /// Push to `self.imports` based on a type we've observed in the IR. + fn add_imports_for_type(&mut self, ty: &Type) { + match &ty.kind { + TypeKind::Custom { name, .. } => { + if let Some(config) = self.config.custom_types.get(name.as_str()) { + for mod_name in config.imports.iter().flatten() { + self.imports.insert(format!("import {mod_name}")); + } + } + } + TypeKind::External { + name, namespace, .. + } => { + let mod_name = self.config.module_for_namespace(namespace); + let name = self.class_name(name); + self.imports.insert(format!("import {mod_name}")); + self.imports + .insert(format!("from {mod_name} import {name}")); + } + _ => (), + } + } + + fn add_module_import(&mut self, module_name: &str) { + self.imports.insert(format!("import {module_name}")); + } + + /// Fixup a name by ensuring it's not a keyword + fn fixup_keyword(&self, name: String) -> String { + if KEYWORDS.contains(&name) { + format!("_{name}") + } else { + name + } + } + + pub fn format_docstring(&self, docstring: Option) -> Option { + docstring.map(|docstring| { + // Escape triple quotes to avoid syntax errors in docstrings + let docstring = docstring.replace(r#"""""#, r#"\"\"\""#); + // Remove indentation and surround with quotes + format!("\"\"\"\n{}\n\"\"\"", &textwrap::dedent(&docstring)) + }) + } +} + +impl VisitMut for BindingsIrVisitor { + fn visit_bindings_ir(&mut self, bindings_ir: &mut BindingsIr) -> Result<()> { + bindings_ir.crate_docstring = self.format_docstring(bindings_ir.crate_docstring.take()); + Ok(()) + } + + fn visit_record(&mut self, rec: &mut Record) -> Result<()> { + rec.name = self.class_name(&rec.name); + rec.docstring = self.format_docstring(rec.docstring.take()); + self.exports.push(rec.name.clone()); + Ok(()) + } + + fn visit_interface(&mut self, interface: &mut Interface) -> Result<()> { + // Make sure to setup docstring before we clone it for our Protocol. + interface.docstring = self.format_docstring(interface.docstring.take()); + if interface.has_callback_interface() { + // This is a trait interface that can be implemented in Python, so it is treated like a + // callback interface where the primary use-case is the trait being implemented + // locally. It is a base-class local implementations might subclass. + // We reuse "Protocol.py" for this, even though here we are not generating a protocol + self.runtimes.callback_interface = true; + if interface.has_async_method() { + self.runtimes.async_callback = true; + } + let protocol = Protocol { + name: self.class_name(&interface.name), + base_class: "".to_string(), + docstring: interface.docstring.clone(), + methods: interface.methods.clone(), + }; + interface.name = format!("{}Impl", self.class_name(&interface.name)); + interface + .lang_data + .insert("protocol_name", protocol.name.clone()); + self.exports.push(protocol.name.clone()); + self.protocols.push(protocol); + } else { + let protocol = Protocol { + name: format!("{}Protocol", self.class_name(&interface.name)), + base_class: "typing.Protocol".to_string(), + docstring: interface.docstring.clone(), + methods: interface.methods.clone(), + }; + interface.name = self.class_name(&interface.name); + interface + .lang_data + .insert("protocol_name", protocol.name.clone()); + self.exports.push(interface.name.clone()); + self.exports.push(protocol.name.clone()); + self.protocols.push(protocol); + } + for i in interface.trait_impls.iter_mut() { + i.trait_name = self.class_name(&i.trait_name); + } + // Python constructors can't be async. If the primary constructor from Rust is async, then + // treat it like a secondary constructor which generates a factory method. + if let Some(cons) = interface + .constructors + .iter_mut() + .find(|c| c.primary && c.is_async()) + { + cons.name = "new".to_string(); + cons.primary = false; + interface.lang_data.insert("had_async_constructor", true); + } + + interface.lang_data.insert( + "base_classes", + interface + .trait_impls + .iter() + .map(|trait_impl| trait_impl.trait_name.as_str()) + .chain(interface.is_used_as_error().then_some("Exception")) + .collect::>() + .join(", "), + ); + Ok(()) + } + + fn visit_callback_interface(&mut self, cbi: &mut CallbackInterface) -> Result<()> { + self.runtimes.callback_interface = true; + cbi.name = self.class_name(&cbi.name); + cbi.docstring = self.format_docstring(cbi.docstring.take()); + if cbi.has_async_method() { + self.runtimes.async_callback = true; + } + self.protocols.push(Protocol { + name: self.class_name(&cbi.name), + base_class: "typing.Protocol".to_string(), + docstring: cbi.docstring.clone(), + methods: cbi.methods.clone(), + }); + self.exports.push(cbi.name.clone()); + Ok(()) + } + + fn visit_vtable(&mut self, vtable: &mut VTable) -> Result<()> { + vtable.name = format!("_UniffiVTable{}", self.class_name(&vtable.name)); + Ok(()) + } + + fn visit_field(&mut self, field: &mut Field) -> Result<()> { + field.name = self.var_name(&field.name); + field.docstring = self.format_docstring(field.docstring.take()); + Ok(()) + } + + fn visit_enum(&mut self, enum_: &mut Enum) -> Result<()> { + enum_.name = self.class_name(&enum_.name); + enum_.docstring = self.format_docstring(enum_.docstring.take()); + self.exports.push(enum_.name.clone()); + Ok(()) + } + + fn visit_custom_type(&mut self, custom: &mut CustomType) -> Result<()> { + if let Some(config) = self.config.custom_types.get(&custom.name) { + custom.lang_data.insert("custom_type_config", config); + } + custom.name = self.class_name(&custom.name); + self.exports.push(custom.name.clone()); + Ok(()) + } + + fn visit_external_type(&mut self, ext: &mut ExternalType) -> Result<()> { + ext.name = self.class_name(&ext.name); + Ok(()) + } + + fn visit_variant(&mut self, variant: &mut Variant) -> Result<()> { + if variant.enum_shape.is_error() { + variant.name = self.class_name(&variant.name); + } else { + variant.name = self.enum_variant_name(&variant.name); + } + variant.docstring = self.format_docstring(variant.docstring.take()); + Ok(()) + } + + fn visit_method(&mut self, meth: &mut Method) -> Result<()> { + meth.name = self.fn_name(&meth.name); + meth.docstring = self.format_docstring(meth.docstring.take()); + if meth.is_async() { + self.add_module_import("asyncio"); + self.runtimes.async_ = true; + } + Ok(()) + } + + fn visit_uniffi_trait(&mut self, ut: &mut UniffiTrait) -> Result<()> { + match ut { + UniffiTrait::Debug { fmt } => { + fmt.name = "__repr__".to_string(); + } + UniffiTrait::Display { fmt } => { + fmt.name = "__str__".to_string(); + } + UniffiTrait::Eq { eq, ne } => { + eq.name = "__eq__".to_string(); + ne.name = "__ne__".to_string(); + } + UniffiTrait::Hash { hash } => { + hash.name = "__hash__".to_string(); + } + } + Ok(()) + } + + fn visit_argument(&mut self, arg: &mut Argument) -> Result<()> { + arg.name = self.var_name(&arg.name); + Ok(()) + } + + fn visit_constructor(&mut self, cons: &mut Constructor) -> Result<()> { + cons.name = if cons.is_primary_constructor() && !cons.is_async() { + "__init__".to_string() + } else { + self.fn_name(&cons.name) + }; + cons.docstring = self.format_docstring(cons.docstring.take()); + if cons.is_async() { + self.add_module_import("asyncio"); + self.runtimes.async_ = true; + } + Ok(()) + } + + fn visit_function(&mut self, func: &mut Function) -> Result<()> { + func.name = self.fn_name(&func.name); + func.docstring = self.format_docstring(func.docstring.take()); + if func.is_async() { + self.add_module_import("asyncio"); + self.runtimes.async_ = true; + } + self.exports.push(func.name.clone()); + Ok(()) + } + + fn visit_type(&mut self, ty: &mut Type) -> Result<()> { + self.add_imports_for_type(ty); + let ffi_converter_name = format!("_UniffiConverter{}", ty.canonical_name()); + match &ty.kind { + TypeKind::External { namespace, .. } => { + let mod_name = self.config.module_for_namespace(namespace); + ty.lang_data.insert( + "ffi_converter_name", + format!("{mod_name}.{ffi_converter_name}"), + ); + } + _ => ty + .lang_data + .insert("ffi_converter_name", ffi_converter_name), + } + ty.lang_data.insert("type_name", self.type_name(ty)); + Ok(()) + } + + fn visit_return_type(&mut self, return_type: &mut ReturnType) -> Result<()> { + let ffi_default = match &return_type.ty { + Some(t) => match &t.ffi_type.kind { + FfiTypeKind::UInt8 + | FfiTypeKind::Int8 + | FfiTypeKind::UInt16 + | FfiTypeKind::Int16 + | FfiTypeKind::UInt32 + | FfiTypeKind::Int32 + | FfiTypeKind::UInt64 + | FfiTypeKind::Int64 + | FfiTypeKind::Handle => "0".to_string(), + FfiTypeKind::Float32 | FfiTypeKind::Float64 => "0.0".to_string(), + FfiTypeKind::RustArcPtr(_) => "ctypes.c_void_p()".to_string(), + FfiTypeKind::RustBuffer(meta) => match meta { + None => "_UniffiRustBuffer.default()".to_string(), + Some(meta) => { + let module_name = self.config.module_for_namespace(&meta.namespace); + format!("{module_name}._UniffiRustBuffer.default()") + } + }, + _ => unimplemented!("FFI default for: {t:?}"), + }, + // When we need to use a value for void returns, we use a `u8` placeholder and `0` as + // the default. + None => "0".to_string(), + }; + return_type.lang_data.insert("ffi_default", ffi_default); + Ok(()) + } + + fn visit_literal(&mut self, literal: &mut Literal) -> Result<()> { + literal + .lang_data + .insert("rendered_literal", self.literal(literal)?); + Ok(()) + } + + fn visit_ffi_function_type(&mut self, func_type: &mut FfiFunctionType) -> Result<()> { + func_type.name = self.ffi_function_type_name(&func_type.name); + Ok(()) + } + + fn visit_ffi_struct(&mut self, struct_: &mut FfiStruct) -> Result<()> { + struct_.name = self.ffi_struct_name(&struct_.name); + Ok(()) + } + + fn visit_ffi_type(&mut self, node: &mut FfiType) -> Result<()> { + node.lang_data + .insert("ffi_type_name", self.ffi_type_name(node)); + Ok(()) + } +} diff --git a/uniffi_bindgen/src/bindings/python/mod.rs b/uniffi_bindgen/src/bindings/python/mod.rs index ca598baf56..428fa2fdfa 100644 --- a/uniffi_bindgen/src/bindings/python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/mod.rs @@ -10,6 +10,7 @@ use fs_err as fs; mod gen_python; #[cfg(feature = "bindgen-tests")] pub mod test; + use crate::{BindingGenerator, Component, GenerationSettings}; use gen_python::{generate_python_bindings, Config}; @@ -51,7 +52,7 @@ impl BindingGenerator for PythonBindingGenerator { ) -> Result<()> { for Component { ci, config, .. } in components { let py_file = settings.out_dir.join(format!("{}.py", ci.namespace())); - fs::write(&py_file, generate_python_bindings(config, &mut ci.clone())?)?; + fs::write(&py_file, generate_python_bindings(config, ci)?)?; if settings.try_format_code { if let Err(e) = Command::new("yapf").arg(&py_file).output() { diff --git a/uniffi_bindgen/src/bindings/python/templates/Async.py b/uniffi_bindgen/src/bindings/python/templates/Async.py index c80ac0c3f9..2ebdf2998a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Async.py +++ b/uniffi_bindgen/src/bindings/python/templates/Async.py @@ -40,7 +40,7 @@ def _uniffi_set_future_result(future, poll_code): if not future.cancelled(): future.set_result(poll_code) -async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, lift_func, error_ffi_converter): +async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, error_ffi_converter): try: eventloop = _uniffi_get_event_loop() @@ -56,13 +56,11 @@ async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, if poll_code == _UNIFFI_RUST_FUTURE_POLL_READY: break - return lift_func( - _uniffi_rust_call_with_error(error_ffi_converter, ffi_complete, rust_future) - ) + return _uniffi_rust_call_with_error(error_ffi_converter, ffi_complete, rust_future) finally: ffi_free(rust_future) -{%- if ci.has_async_callback_interface_definition() %} +{%- if runtimes.async_callback %} def _uniffi_trait_interface_call_async(make_call, handle_success, handle_error): async def make_call_and_call_callback(): try: @@ -72,7 +70,7 @@ async def make_call_and_call_callback(): traceback.print_exc(file=sys.stderr) handle_error( _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR, - {{ Type::String.borrow()|lower_fn }}(repr(e)), + {{ globals.string_type|lower_fn }}(repr(e)), ) eventloop = _uniffi_get_event_loop() task = asyncio.run_coroutine_threadsafe(make_call_and_call_callback(), eventloop) @@ -94,7 +92,7 @@ async def make_call_and_call_callback(): traceback.print_exc(file=sys.stderr) handle_error( _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR, - {{ Type::String.borrow()|lower_fn }}(repr(e)), + {{ globals.string_type|lower_fn }}(repr(e)), ) eventloop = _uniffi_get_event_loop() task = asyncio.run_coroutine_threadsafe(make_call_and_call_callback(), eventloop) diff --git a/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py b/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py index 3f8c5d1d4d..6bcceb8e7f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/BooleanHelper.py @@ -1,4 +1,4 @@ -class _UniffiConverterBool: +class {{ ffi_converter_name }}: @classmethod def check_lower(cls, value): return not not value diff --git a/uniffi_bindgen/src/bindings/python/templates/BytesHelper.py b/uniffi_bindgen/src/bindings/python/templates/BytesHelper.py index 4d09531322..7051f86fb0 100644 --- a/uniffi_bindgen/src/bindings/python/templates/BytesHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/BytesHelper.py @@ -1,4 +1,4 @@ -class _UniffiConverterBytes(_UniffiConverterRustBuffer): +class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): size = buf.read_i32() diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py index 21e13d9e0e..855f6d90bc 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceTemplate.py @@ -1,14 +1,5 @@ -{%- let cbi = ci.get_callback_interface_definition(name).unwrap() %} -{%- let ffi_init_callback = cbi.ffi_init_callback() %} -{%- let protocol_name = type_name.clone() %} -{%- let protocol_base_class = "typing.Protocol" %} -{%- let protocol_docstring = cbi.docstring() %} -{%- let vtable = cbi.vtable() %} -{%- let methods = cbi.methods() %} -{%- let vtable_methods = cbi.vtable_methods() %} - -{% include "Protocol.py" %} -{% include "CallbackInterfaceImpl.py" %} +{%- let vtable = cbi.vtable %} +{% include "VTable.py" %} # The _UniffiConverter which transforms the Callbacks in to Handles to pass to Rust. {{ ffi_converter_name }} = _UniffiCallbackInterfaceFfiConverter() diff --git a/uniffi_bindgen/src/bindings/python/templates/CustomType.py b/uniffi_bindgen/src/bindings/python/templates/CustomType.py index 3f7c88fcc4..6a5a91f124 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CustomType.py +++ b/uniffi_bindgen/src/bindings/python/templates/CustomType.py @@ -1,59 +1,57 @@ -{%- match python_config.custom_types.get(name.as_str()) %} +{%- match custom_type|custom_type_config %} {% when None %} {#- No custom type config, just forward all methods to our builtin type #} -class _UniffiConverterType{{ name }}: +{{ custom_type.name }} = {{ custom_type.builtin|type_name }} + +class {{ ffi_converter_name }}: @staticmethod def write(value, buf): - {{ builtin|ffi_converter_name }}.write(value, buf) + {{ custom_type.builtin|ffi_converter_name }}.write(value, buf) @staticmethod def read(buf): - return {{ builtin|ffi_converter_name }}.read(buf) + return {{ custom_type.builtin|ffi_converter_name }}.read(buf) @staticmethod def lift(value): - return {{ builtin|ffi_converter_name }}.lift(value) + return {{ custom_type.builtin|ffi_converter_name }}.lift(value) @staticmethod def check_lower(value): - return {{ builtin|ffi_converter_name }}.check_lower(value) + return {{ custom_type.builtin|ffi_converter_name }}.check_lower(value) @staticmethod def lower(value): - return {{ builtin|ffi_converter_name }}.lower(value) + return {{ custom_type.builtin|ffi_converter_name }}.lower(value) {%- when Some(config) %} - -{%- if let Some(imports) = config.imports %} -{%- for import_name in imports %} -{{ self.add_import(import_name) }} -{%- endfor %} +{%- if let Some(type_name) = config.type_name %} +{{ custom_type.name }} = {{ type_name }} {%- endif %} - {#- Custom type config supplied, use it to convert the builtin type #} -class _UniffiConverterType{{ name }}: +class {{ ffi_converter_name }}: @staticmethod def write(value, buf): builtin_value = {{ config.from_custom.render("value") }} - {{ builtin|write_fn }}(builtin_value, buf) + {{ custom_type.builtin|write_fn }}(builtin_value, buf) @staticmethod def read(buf): - builtin_value = {{ builtin|read_fn }}(buf) + builtin_value = {{ custom_type.builtin|read_fn }}(buf) return {{ config.into_custom.render("builtin_value") }} @staticmethod def lift(value): - builtin_value = {{ builtin|lift_fn }}(value) + builtin_value = {{ custom_type.builtin|lift_fn }}(value) return {{ config.into_custom.render("builtin_value") }} @staticmethod def check_lower(value): builtin_value = {{ config.from_custom.render("value") }} - return {{ builtin|check_lower_fn }}(builtin_value) + return {{ custom_type.builtin|check_lower_fn }}(builtin_value) @staticmethod def lower(value): builtin_value = {{ config.from_custom.render("value") }} - return {{ builtin|lower_fn }}(builtin_value) + return {{ custom_type.builtin|lower_fn }}(builtin_value) {%- endmatch %} diff --git a/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py b/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py index ecb035b7f4..bbecc887e7 100644 --- a/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/DurationHelper.py @@ -4,7 +4,7 @@ # There is a loss of precision when converting from Rust durations, # which are accurate to the nanosecond, # to Python durations, which are only accurate to the microsecond. -class _UniffiConverterDuration(_UniffiConverterRustBuffer): +class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): seconds = buf.read_u64() diff --git a/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py b/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py index f007313353..720dfc66b0 100644 --- a/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/EnumTemplate.py @@ -6,29 +6,28 @@ #} {% if e.is_flat() %} -class {{ type_name }}(enum.Enum): - {%- call py::docstring(e, 4) %} - {%- for variant in e.variants() %} - {{ variant.name() }} = {{ e|variant_discr_literal(loop.index0) }} - {%- call py::docstring(variant, 4) %} +class {{ e.name }}(enum.Enum): + {{ e.docstring|docindent(4) -}} + {%- for variant in e.variants %} + {{ variant.name }} = {{ variant.discr|literal }} + {{ variant.docstring|docindent(4) -}} {% endfor %} {% else %} -class {{ type_name }}: - {%- call py::docstring(e, 4) %} +class {{ e.name }}: + {{ e.docstring|docindent(4) -}} def __init__(self): - raise RuntimeError("{{ type_name }} cannot be instantiated directly") + raise RuntimeError("{{ e.name }} cannot be instantiated directly") # Each enum variant is a nested class of the enum itself. - {% for variant in e.variants() -%} - class {{ variant.name() }}: - {%- call py::docstring(variant, 8) %} - + {% for variant in e.variants -%} + class {{ variant.name }}: + {{ variant.docstring|docindent(8) -}} {%- if variant.has_nameless_fields() %} def __init__(self, *values): - if len(values) != {{ variant.fields().len() }}: - raise TypeError(f"Expected {{ variant.fields().len() }} arguments, found {len(values)}") - {%- for field in variant.fields() %} + if len(values) != {{ variant.fields.len() }}: + raise TypeError(f"Expected {{ variant.fields.len() }} arguments, found {len(values)}") + {%- for field in variant.fields %} if not isinstance(values[{{ loop.index0 }}], {{ field|type_name }}): raise TypeError(f"unexpected type for tuple element {{ loop.index0 }} - expected '{{ field|type_name }}', got '{type(values[{{ loop.index0 }}])}'") {%- endfor %} @@ -38,36 +37,36 @@ def __getitem__(self, index): return self._values[index] def __str__(self): - return f"{{ type_name }}.{{ variant.name() }}{self._values!r}" + return f"{{ e.name }}.{{ variant.name }}{self._values!r}" def __eq__(self, other): - if not other.is_{{ variant.name() }}(): + if not other.is_{{ variant.name }}(): return False return self._values == other._values {%- else -%} - {%- for field in variant.fields() %} - {{ field.name() }}: "{{ field|type_name }}" - {%- call py::docstring(field, 8) %} + {%- for field in variant.fields %} + {{ field.name }}: "{{ field|type_name }}" + {{ field.docstring|docindent(8) -}} {%- endfor %} - def __init__(self,{% for field in variant.fields() %}{{ field.name() }}: "{{- field|type_name }}"{% if loop.last %}{% else %}, {% endif %}{% endfor %}): + def __init__(self,{% for field in variant.fields %}{{ field.name }}: "{{- field|type_name }}"{% if loop.last %}{% else %}, {% endif %}{% endfor %}): {%- if variant.has_fields() %} - {%- for field in variant.fields() %} - self.{{ field.name() }} = {{ field.name() }} + {%- for field in variant.fields %} + self.{{ field.name }} = {{ field.name }} {%- endfor %} {%- else %} pass {%- endif %} def __str__(self): - return "{{ type_name }}.{{ variant.name() }}({% for field in variant.fields() %}{{ field.name() }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in variant.fields() %}self.{{ field.name() }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + return "{{ e.name }}.{{ variant.name }}({% for field in variant.fields %}{{ field.name }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in variant.fields %}self.{{ field.name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) def __eq__(self, other): - if not other.is_{{ variant.name() }}(): + if not other.is_{{ variant.name }}(): return False - {%- for field in variant.fields() %} - if self.{{ field.name() }} != other.{{ field.name() }}: + {%- for field in variant.fields %} + if self.{{ field.name }} != other.{{ field.name }}: return False {%- endfor %} return True @@ -76,22 +75,22 @@ def __eq__(self, other): # For each variant, we have `is_NAME` and `is_name` methods for easily checking # whether an instance is that variant. - {% for variant in e.variants() -%} - def is_{{ variant.name() }}(self) -> bool: - return isinstance(self, {{ type_name }}.{{ variant.name() }}) + {% for variant in e.variants -%} + def is_{{ variant.name }}(self) -> bool: + return isinstance(self, {{ e.name }}.{{ variant.name }}) {#- We used to think we used `is_NAME` but did `is_name` instead. In #2270 we decided to do both. #} - {%- if variant.name() != variant.name().to_snake_case() %} - def is_{{ variant.name().to_snake_case() }}(self) -> bool: - return isinstance(self, {{ type_name }}.{{ variant.name() }}) + {%- if variant.name != variant.name.to_snake_case() %} + def is_{{ variant.name.to_snake_case() }}(self) -> bool: + return isinstance(self, {{ e.name }}.{{ variant.name }}) {%- endif %} {% endfor %} # Now, a little trick - we make each nested variant class be a subclass of the main # enum class, so that method calls and instance checks etc will work intuitively. # We might be able to do this a little more neatly with a metaclass, but this'll do. -{% for variant in e.variants() -%} -{{ type_name }}.{{ variant.name() }} = type("{{ type_name }}.{{ variant.name() }}", ({{ type_name }}.{{variant.name()}}, {{ type_name }},), {}) # type: ignore +{% for variant in e.variants -%} +{{ e.name }}.{{ variant.name }} = type("{{ e.name }}.{{ variant.name }}", ({{ e.name }}.{{variant.name}}, {{ e.name }},), {}) # type: ignore {% endfor %} {% endif %} @@ -101,13 +100,13 @@ class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): def read(buf): variant = buf.read_i32() - {%- for variant in e.variants() %} + {%- for variant in e.variants %} if variant == {{ loop.index }}: {%- if e.is_flat() %} - return {{ type_name }}.{{variant.name()}} + return {{ e.name }}.{{variant.name}} {%- else %} - return {{ type_name }}.{{variant.name()}}( - {%- for field in variant.fields() %} + return {{ e.name }}.{{variant.name}}( + {%- for field in variant.fields %} {{ field|read_fn }}(buf), {%- endfor %} ) @@ -117,20 +116,20 @@ def read(buf): @staticmethod def check_lower(value): - {%- if e.variants().is_empty() %} + {%- if e.variants.is_empty() %} pass {%- else %} - {%- for variant in e.variants() %} + {%- for variant in e.variants %} {%- if e.is_flat() %} - if value == {{ type_name }}.{{ variant.name() }}: + if value == {{ e.name }}.{{ variant.name }}: {%- else %} - if value.is_{{ variant.name() }}(): + if value.is_{{ variant.name }}(): {%- endif %} - {%- for field in variant.fields() %} + {%- for field in variant.fields %} {%- if variant.has_nameless_fields() %} {{ field|check_lower_fn }}(value._values[{{ loop.index0 }}]) {%- else %} - {{ field|check_lower_fn }}(value.{{ field.name() }}) + {{ field|check_lower_fn }}(value.{{ field.name }}) {%- endif %} {%- endfor %} return @@ -140,18 +139,18 @@ def check_lower(value): @staticmethod def write(value, buf): - {%- for variant in e.variants() %} + {%- for variant in e.variants %} {%- if e.is_flat() %} - if value == {{ type_name }}.{{ variant.name() }}: + if value == {{ e.name }}.{{ variant.name }}: buf.write_i32({{ loop.index }}) {%- else %} - if value.is_{{ variant.name() }}(): + if value.is_{{ variant.name }}(): buf.write_i32({{ loop.index }}) - {%- for field in variant.fields() %} + {%- for field in variant.fields %} {%- if variant.has_nameless_fields() %} {{ field|write_fn }}(value._values[{{ loop.index0 }}], buf) {%- else %} - {{ field|write_fn }}(value.{{ field.name() }}, buf) + {{ field|write_fn }}(value.{{ field.name }}, buf) {%- endif %} {%- endfor %} {%- endif %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py index 9d3b30831d..e98d935744 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py @@ -1,34 +1,30 @@ -# {{ type_name }} +# {{ e.name }} # We want to define each variant as a nested class that's also a subclass, # which is tricky in Python. To accomplish this we're going to create each # class separately, then manually add the child classes to the base class's # __dict__. All of this happens in dummy class to avoid polluting the module # namespace. -class {{ type_name }}(Exception): - {%- call py::docstring(e, 4) %} +class {{ e.name }}(Exception): + {{ e.docstring|docindent(4) }} pass -_UniffiTemp{{ type_name }} = {{ type_name }} +_UniffiTemp{{ e.name }} = {{ e.name }} -class {{ type_name }}: # type: ignore - {%- call py::docstring(e, 4) %} - {%- for variant in e.variants() -%} - {%- let variant_type_name = variant.name() -%} +class {{ e.name }}: # type: ignore + {%- for variant in e.variants -%} {%- if e.is_flat() %} - class {{ variant_type_name }}(_UniffiTemp{{ type_name }}): - {%- call py::docstring(variant, 8) %} - + class {{ variant.name }}(_UniffiTemp{{ e.name }}): + {{ variant.docstring|docindent(8) -}} def __repr__(self): - return "{{ type_name }}.{{ variant_type_name }}({})".format(repr(str(self))) + return "{{ e.name }}.{{ variant.name }}({})".format(repr(str(self))) {%- else %} - class {{ variant_type_name }}(_UniffiTemp{{ type_name }}): - {%- call py::docstring(variant, 8) %} - + class {{ variant.name }}(_UniffiTemp{{ e.name }}): + {{ variant.docstring|docindent(8) -}} {%- if variant.has_nameless_fields() %} def __init__(self, *values): - if len(values) != {{ variant.fields().len() }}: - raise TypeError(f"Expected {{ variant.fields().len() }} arguments, found {len(values)}") - {%- for field in variant.fields() %} + if len(values) != {{ variant.fields.len() }}: + raise TypeError(f"Expected {{ variant.fields.len() }} arguments, found {len(values)}") + {%- for field in variant.fields %} if not isinstance(values[{{ loop.index0 }}], {{ field|type_name }}): raise TypeError(f"unexpected type for tuple element {{ loop.index0 }} - expected '{{ field|type_name }}', got '{type(values[{{ loop.index0 }}])}'") {%- endfor %} @@ -39,15 +35,15 @@ def __getitem__(self, index): return self._values[index] {%- else %} - def __init__(self{% for field in variant.fields() %}, {{ field.name() }}{% endfor %}): + def __init__(self{% for field in variant.fields %}, {{ field.name }}{% endfor %}): {%- if variant.has_fields() %} super().__init__(", ".join([ - {%- for field in variant.fields() %} - "{{ field.name() }}={!r}".format({{ field.name() }}), + {%- for field in variant.fields %} + "{{ field.name }}={!r}".format({{ field.name }}), {%- endfor %} ])) - {%- for field in variant.fields() %} - self.{{ field.name() }} = {{ field.name() }} + {%- for field in variant.fields %} + self.{{ field.name }} = {{ field.name }} {%- endfor %} {%- else %} pass @@ -55,45 +51,47 @@ def __init__(self{% for field in variant.fields() %}, {{ field.name() }}{% endfo {%- endif %} def __repr__(self): - return "{{ type_name }}.{{ variant_type_name }}({})".format(str(self)) + return "{{ e.name }}.{{ variant.name }}({})".format(str(self)) {%- endif %} - _UniffiTemp{{ type_name }}.{{ variant_type_name }} = {{ variant_type_name }} # type: ignore + _UniffiTemp{{ e.name }}.{{ variant.name }} = {{ variant.name }} # type: ignore {%- endfor %} -{{ type_name }} = _UniffiTemp{{ type_name }} # type: ignore -del _UniffiTemp{{ type_name }} +{{ e.name }} = _UniffiTemp{{ e.name }} # type: ignore +del _UniffiTemp{{ e.name }} class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): variant = buf.read_i32() - {%- for variant in e.variants() %} + {%- for variant in e.variants %} if variant == {{ loop.index }}: - return {{ type_name }}.{{ variant.name() }}( - {%- if e.is_flat() %} - {{ Type::String.borrow()|read_fn }}(buf), - {%- else %} - {%- for field in variant.fields() %} + {%- if e.is_flat() %} + return {{ e.name }}.{{ variant.name }}( + {{ globals.string_type|read_fn }}(buf) + ) + {%- else %} + return {{ e.name }}.{{ variant.name }}( + {%- for field in variant.fields %} {{ field|read_fn }}(buf), {%- endfor %} - {%- endif %} ) + {%- endif %} {%- endfor %} raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): - {%- if e.variants().is_empty() %} + {%- if e.variants.is_empty() %} pass {%- else %} - {%- for variant in e.variants() %} - if isinstance(value, {{ type_name }}.{{ variant.name() }}): - {%- for field in variant.fields() %} + {%- for variant in e.variants %} + if isinstance(value, {{ e.name }}.{{ variant.name }}): + {%- for field in variant.fields %} {%- if variant.has_nameless_fields() %} {{ field|check_lower_fn }}(value._values[{{ loop.index0 }}]) {%- else %} - {{ field|check_lower_fn }}(value.{{ field.name() }}) + {{ field|check_lower_fn }}(value.{{ field.name }}) {%- endif %} {%- endfor %} return @@ -102,14 +100,14 @@ def check_lower(value): @staticmethod def write(value, buf): - {%- for variant in e.variants() %} - if isinstance(value, {{ type_name }}.{{ variant.name() }}): + {%- for variant in e.variants %} + if isinstance(value, {{ e.name }}.{{ variant.name }}): buf.write_i32({{ loop.index }}) - {%- for field in variant.fields() %} - {%- if variant.has_nameless_fields() %} + {%- for field in variant.fields %} + {%- if variant.has_nameless_fields() %} {{ field|write_fn }}(value._values[{{ loop.index0 }}], buf) - {%- else %} - {{ field|write_fn }}(value.{{ field.name() }}, buf) - {%- endif %} + {%- else %} + {{ field|write_fn }}(value.{{ field.name }}, buf) + {%- endif %} {%- endfor %} {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py deleted file mode 100644 index 26e4639e19..0000000000 --- a/uniffi_bindgen/src/bindings/python/templates/ExternalTemplate.py +++ /dev/null @@ -1,9 +0,0 @@ -{%- let module = python_config.module_for_namespace(namespace) -%} - -# External type {{name}} is in namespace "{{namespace}}", crate {{module_path}} -{%- let ffi_converter_name = "_UniffiConverterType{}"|format(name) %} -{{ self.add_import_of(module, ffi_converter_name) }} -{{ self.add_import_of(module, name) }} {#- import the type alias itself -#} - -{%- let rustbuffer_local_name = "_UniffiRustBuffer{}"|format(name) %} -{{ self.add_import_of_as(module, "_UniffiRustBuffer", rustbuffer_local_name) }} diff --git a/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py b/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py index 49a1a7286e..6bcae689d5 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Float32Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterFloat(_UniffiConverterPrimitiveFloat): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveFloat): @staticmethod def read(buf): return buf.read_float() diff --git a/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py b/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py index e2084c7b13..b658822c7c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Float64Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterDouble(_UniffiConverterPrimitiveFloat): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveFloat): @staticmethod def read(buf): return buf.read_double() diff --git a/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/uniffi_bindgen/src/bindings/python/templates/Helpers.py index 1b1d27840d..eff2f2b0fb 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Helpers.py +++ b/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -75,7 +75,7 @@ def _uniffi_trait_interface_call(call_status, make_call, write_return_value): return write_return_value(make_call()) except Exception as e: call_status.code = _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR - call_status.error_buf = {{ Type::String.borrow()|lower_fn }}(repr(e)) + call_status.error_buf = {{ globals.string_type|lower_fn }}(repr(e)) def _uniffi_trait_interface_call_with_error(call_status, make_call, write_return_value, error_type, lower_error): try: @@ -86,4 +86,4 @@ def _uniffi_trait_interface_call_with_error(call_status, make_call, write_return call_status.error_buf = lower_error(e) except Exception as e: call_status.code = _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR - call_status.error_buf = {{ Type::String.borrow()|lower_fn }}(repr(e)) + call_status.error_buf = {{ globals.string_type|lower_fn }}(repr(e)) diff --git a/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py b/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py index befa563384..f68e99cddd 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Int16Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterInt16(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "i16" VALUE_MIN = -2**15 VALUE_MAX = 2**15 diff --git a/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py b/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py index 70644f6717..f320e3904f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Int32Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterInt32(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "i32" VALUE_MIN = -2**31 VALUE_MAX = 2**31 diff --git a/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py b/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py index 232f127bd6..c99b0013aa 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Int64Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterInt64(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "i64" VALUE_MIN = -2**63 VALUE_MAX = 2**63 diff --git a/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py b/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py index c1de1625e7..d2f0a50a6e 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/Int8Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterInt8(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "i8" VALUE_MIN = -2**7 VALUE_MAX = 2**7 diff --git a/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py b/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py index 2d9c5f514d..d5b2433eab 100644 --- a/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/NamespaceLibraryTemplate.py @@ -31,22 +31,22 @@ def _uniffi_load_indirect(): # Anything else must be an ELF platform - Linux, *BSD, Solaris/illumos libname = "lib{}.so" - libname = libname.format("{{ config.cdylib_name() }}") + libname = libname.format("{{ cdylib_name }}") path = os.path.join(os.path.dirname(__file__), libname) lib = ctypes.cdll.LoadLibrary(path) return lib def _uniffi_check_contract_api_version(lib): # Get the bindings contract version from our ComponentInterface - bindings_contract_version = {{ ci.uniffi_contract_version() }} + bindings_contract_version = {{ globals.contract_version }} # Get the scaffolding contract version by calling the into the dylib - scaffolding_contract_version = lib.{{ ci.ffi_uniffi_contract_version().name() }}() + scaffolding_contract_version = lib.{{ globals.ffi_uniffi_contract_version.name }}() if bindings_contract_version != scaffolding_contract_version: raise InternalError("UniFFI contract version mismatch: try cleaning and rebuilding your project") def _uniffi_check_api_checksums(lib): - {%- for (name, expected_checksum) in ci.iter_checksums() %} - if lib.{{ name }}() != {{ expected_checksum }}: + {%- for check in checksum_checks %} + if lib.{{ check.func.name }}() != {{ check.checksum }}: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") {%- else %} pass @@ -57,33 +57,35 @@ def _uniffi_check_api_checksums(lib): _UniffiLib = _uniffi_load_indirect() -{%- for def in ci.ffi_definitions() %} +{%- for def in ffi_definitions %} {%- match def %} -{%- when FfiDefinition::CallbackFunction(callback) %} -{{ callback.name()|ffi_callback_name }} = ctypes.CFUNCTYPE( - {%- match callback.return_type() %} - {%- when Some(return_type) %}{{ return_type|ffi_type_name }}, - {%- when None %}None, - {%- endmatch %} - {%- for arg in callback.arguments() -%} - {{ arg.type_().borrow()|ffi_type_name }}, +{%- when FfiDefinition::FunctionType(func_type) %} +{{ func_type.name }} = ctypes.CFUNCTYPE( + {{ func_type|ffi_return_type }}, + {%- for arg in func_type.arguments -%} + {{ arg|ffi_type_name }}, {%- endfor -%} - {%- if callback.has_rust_call_status_arg() %} + {%- if func_type.has_rust_call_status_arg %} ctypes.POINTER(_UniffiRustCallStatus), {%- endif %} ) {%- when FfiDefinition::Struct(ffi_struct) %} -class {{ ffi_struct.name()|ffi_struct_name }}(ctypes.Structure): +class {{ ffi_struct.name }}(ctypes.Structure): _fields_ = [ - {%- for field in ffi_struct.fields() %} - ("{{ field.name() }}", {{ field.type_().borrow()|ffi_type_name }}), + {%- for field in ffi_struct.fields %} + ("{{ field.name }}", {{ field|ffi_type_name }}), {%- endfor %} ] {%- when FfiDefinition::Function(func) %} -_UniffiLib.{{ func.name() }}.argtypes = ( - {%- call py::arg_list_ffi_decl(func) -%} +_UniffiLib.{{ func.name }}.argtypes = ( + {%- for arg in func.arguments %} + {{ arg|ffi_type_name }}, + {%- endfor %} + {%- if func.has_rust_call_status_arg %} + ctypes.POINTER(_UniffiRustCallStatus), + {%- endif %} ) -_UniffiLib.{{ func.name() }}.restype = {% match func.return_type() %}{% when Some(type_) %}{{ type_|ffi_type_name }}{% when None %}None{% endmatch %} +_UniffiLib.{{ func.name }}.restype = {{ func|ffi_return_type }} {%- endmatch %} {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py index 33a6e0f917..103a38e5a2 100644 --- a/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py @@ -1,62 +1,34 @@ -{%- let obj = ci.get_object_definition(name).unwrap() %} -{%- let methods = obj.methods() %} -{%- let protocol_name = format!("{type_name}Protocol") %} -{%- let protocol_docstring = obj.docstring() %} -{%- let protocol_base_class = "typing.Protocol" %} -{%- include "Protocol.py" %} - -{%- let impl_name %} -{%- if obj.has_callback_interface() %} -# {{ type_name }} is a foreign trait so treated like a callback interface, where the -# primary use-case is the trait being implemented locally. -# It is a base-class local implementations might subclass. -{# We reuse "Protocol.py" for this, even though here we are not generating a protocol #} -{%- let protocol_name = format!("{type_name}") %} -{%- let protocol_base_class = "" %} -{% include "Protocol.py" %} - -{%- let impl_name = format!("{type_name}Impl") %} -# `{{ impl_name }}` is the implementation for a Rust implemented version. -{%- else %} -# {{ type_name }} is a Rust-only trait - it's a wrapper around a Rust implementation. -{%- let impl_name = type_name.clone() %} +{%- if let Some(vtable) = interface.vtable %} +{% include "VTable.py" %} {%- endif %} -{%- if ci.is_name_used_as_error(name) %} -class {{ impl_name }}(Exception): -{%- else %} -class {{ impl_name }}({% for t in obj.trait_impls() %}{{ t.trait_name }},{% endfor %}): -{%- endif %} - {%- call py::docstring(obj, 4) %} +class {{ interface.name }}({{ interface|base_classes }}): + {{ interface.docstring|docindent(4) -}} _pointer: ctypes.c_void_p -{%- match obj.primary_constructor() %} -{%- when Some(cons) %} -{%- if cons.is_async() %} - def __init__(self, *args, **kw): - raise ValueError("async constructors not supported.") -{%- else %} - def __init__(self, {% call py::arg_list_decl(cons) -%}): - {%- call py::docstring(cons, 8) %} - {%- call py::setup_args_extra_indent(cons) %} - self._pointer = {% call py::to_ffi_call(cons) %} -{%- endif %} -{%- when None %} - {# no __init__ means simple construction without a pointer works, which can confuse #} - def __init__(self, *args, **kwargs): + {%- match interface.primary_constructor() %} + {%- when Some(cons) %} + {% filter indent(4) %}{% call py::define_callable(cons) %}{% endfilter %} + {%- when None %} + {# Define __init__ to prevent construction without a pointer, which can confuse #} + def __init__(self): + {%- if interface|had_async_constructor %} + raise ValueError("Async constructors are not supported in Python, call the `new` classmethod instead.") + {%- else %} raise ValueError("This class has no default constructor") -{%- endmatch %} + {%- endif %} + {%- endmatch %} def __del__(self): # In case of partial initialization of instances. pointer = getattr(self, "_pointer", None) if pointer is not None: - _uniffi_rust_call(_UniffiLib.{{ obj.ffi_object_free().name() }}, pointer) + _uniffi_rust_call(_UniffiLib.{{ interface.ffi_free.name }}, pointer) def _uniffi_clone_pointer(self): - return _uniffi_rust_call(_UniffiLib.{{ obj.ffi_object_clone().name() }}, self._pointer) + return _uniffi_rust_call(_UniffiLib.{{ interface.ffi_clone.name }}, self._pointer) - # Used by alternative constructors or any methods which return this type. + # Used by the lift function to construct a new instance @classmethod def _make_instance_(cls, pointer): # Lightly yucky way to bypass the usual __init__ logic @@ -65,66 +37,53 @@ def _make_instance_(cls, pointer): inst._pointer = pointer return inst -{%- for cons in obj.alternate_constructors() %} - @classmethod -{%- if cons.is_async() %} - async def {{ cons.name() }}(cls, {% call py::arg_list_decl(cons) %}): - {%- call py::docstring(cons, 8) %} - {%- call py::setup_args_extra_indent(cons) %} - - return await _uniffi_rust_call_async( - _UniffiLib.{{ cons.ffi_func().name() }}({% call py::arg_list_lowered(cons) %}), - _UniffiLib.{{ cons.ffi_rust_future_poll(ci) }}, - _UniffiLib.{{ cons.ffi_rust_future_complete(ci) }}, - _UniffiLib.{{ cons.ffi_rust_future_free(ci) }}, - {{ ffi_converter_name }}.lift, - {% call py::error_ffi_converter(cons) %} - ) -{%- else %} - def {{ cons.name() }}(cls, {% call py::arg_list_decl(cons) %}): - {%- call py::docstring(cons, 8) %} - {%- call py::setup_args_extra_indent(cons) %} - # Call the (fallible) function before creating any half-baked object instances. - pointer = {% call py::to_ffi_call(cons) %} - return cls._make_instance_(pointer) -{%- endif %} +{%- for cons in interface.alternate_constructors() %} + @staticmethod + {% filter indent(4) %}{% call py::define_callable(cons) %}{% endfilter %} {% endfor %} -{%- for meth in obj.methods() -%} - {%- call py::method_decl(meth.name(), meth) %} +{%- for meth in interface.methods %} + {% filter indent(4) %}{% call py::define_callable(meth) %}{% endfilter %} {%- endfor %} -{%- for tm in obj.uniffi_traits() -%} -{%- match tm %} -{%- when UniffiTrait::Debug { fmt } %} - {%- call py::method_decl("__repr__", fmt) %} -{%- when UniffiTrait::Display { fmt } %} - {%- call py::method_decl("__str__", fmt) %} -{%- when UniffiTrait::Eq { eq, ne } %} - def __eq__(self, other: object) -> {{ eq.return_type().unwrap()|type_name }}: - if not isinstance(other, {{ type_name }}): + +{%- for uniffi_trait in interface.uniffi_traits %} +{%- match uniffi_trait %} +{%- when UniffiTrait::Eq { eq, ne } %} +{# Special-case these to return NotImplemented when the wrong type is passed in #} + def __eq__(self, other: object) -> bool: + if not isinstance(other, {{ interface.name }}): return NotImplemented - return {{ eq.return_type().unwrap()|lift_fn }}({% call py::to_ffi_call_with_prefix("self._uniffi_clone_pointer()", eq) %}) + return {{ eq.return_type.ty.as_ref().unwrap()|lift_fn }}( + _uniffi_rust_call_with_error( + None, + _UniffiLib.{{ eq.ffi_func().name }}, + self._uniffi_clone_pointer(), + {{ interface|lower_fn }}(other), + ) + ) - def __ne__(self, other: object) -> {{ ne.return_type().unwrap()|type_name }}: - if not isinstance(other, {{ type_name }}): + def __ne__(self, other: object) -> bool: + if not isinstance(other, {{ interface.name }}): return NotImplemented - return {{ ne.return_type().unwrap()|lift_fn }}({% call py::to_ffi_call_with_prefix("self._uniffi_clone_pointer()", ne) %}) -{%- when UniffiTrait::Hash { hash } %} - {%- call py::method_decl("__hash__", hash) %} -{%- endmatch %} + return {{ ne.return_type.ty.as_ref().unwrap()|lift_fn }}( + _uniffi_rust_call_with_error( + None, + _UniffiLib.{{ ne.ffi_func().name }}, + self._uniffi_clone_pointer(), + {{ interface|lower_fn }}(other), + ) + ) +{%- else %} +{%- for trait_method in uniffi_trait.methods() %} + {% filter indent(4) %}{% call py::define_callable(trait_method) %}{% endfilter %} +{%- endfor %} +{%- endmatch %} {%- endfor %} -{%- if obj.has_callback_interface() %} -{%- let ffi_init_callback = obj.ffi_init_callback() %} -{%- let vtable = obj.vtable().expect("trait interface should have a vtable") %} -{%- let vtable_methods = obj.vtable_methods() %} -{% include "CallbackInterfaceImpl.py" %} -{%- endif %} - -{# Objects as error #} -{%- if ci.is_name_used_as_error(name) %} +{# Interface as error #} +{%- if interface.is_used_as_error() %} {# Due to some mismatches in the ffi converter mechanisms, errors are forced to be a RustBuffer #} class {{ ffi_converter_name }}__as_error(_UniffiConverterRustBuffer): @classmethod @@ -148,30 +107,30 @@ def lower(value): {%- endif %} class {{ ffi_converter_name }}: - {%- if obj.has_callback_interface() %} + {%- if interface.has_callback_interface() %} _handle_map = _UniffiHandleMap() {%- endif %} @staticmethod def lift(value: int): - return {{ impl_name }}._make_instance_(value) + return {{ interface.name }}._make_instance_(value) @staticmethod - def check_lower(value: {{ type_name }}): - {%- if obj.has_callback_interface() %} + def check_lower(value: {{ interface|protocol_name }}): + {%- if interface.has_callback_interface() %} pass {%- else %} - if not isinstance(value, {{ impl_name }}): - raise TypeError("Expected {{ impl_name }} instance, {} found".format(type(value).__name__)) + if not isinstance(value, {{ interface.name }}): + raise TypeError("Expected {{ interface.name }} instance, {} found".format(type(value).__name__)) {%- endif %} @staticmethod - def lower(value: {{ protocol_name }}): - {%- if obj.has_callback_interface() %} + def lower(value: {{ interface|protocol_name }}): + {%- if interface.has_callback_interface() %} return {{ ffi_converter_name }}._handle_map.insert(value) {%- else %} - if not isinstance(value, {{ impl_name }}): - raise TypeError("Expected {{ impl_name }} instance, {} found".format(type(value).__name__)) + if not isinstance(value, {{ interface.name }}): + raise TypeError("Expected {{ interface.name }} instance, {} found".format(type(value).__name__)) return value._uniffi_clone_pointer() {%- endif %} @@ -183,5 +142,5 @@ def read(cls, buf: _UniffiRustBuffer): return cls.lift(ptr) @classmethod - def write(cls, value: {{ protocol_name }}, buf: _UniffiRustBuffer): + def write(cls, value: {{ interface|protocol_name }}, buf: _UniffiRustBuffer): buf.write_u64(cls.lower(value)) diff --git a/uniffi_bindgen/src/bindings/python/templates/Protocol.py b/uniffi_bindgen/src/bindings/python/templates/Protocol.py index 0e48bda575..2caf6b864a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Protocol.py +++ b/uniffi_bindgen/src/bindings/python/templates/Protocol.py @@ -1,9 +1,9 @@ {# misnamed - a generic "abstract base class". Used as both a protocol and an ABC for traits. #} -class {{ protocol_name }}({{ protocol_base_class }}): - {%- call py::docstring_value(protocol_docstring, 4) %} - {%- for meth in methods.iter() %} - def {{ meth.name() }}(self, {% call py::arg_list_decl(meth) %}): - {%- call py::docstring(meth, 8) %} +class {{ protocol.name }}({{ protocol.base_class }}): + {{ protocol.docstring|docindent(4) -}} + {%- for meth in protocol.methods %} + {{ meth|def }} {{ meth.name }}({{ meth|arg_list }}): + {{ meth.docstring|docindent(8) -}} raise NotImplementedError {%- else %} pass diff --git a/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py b/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py index d5a3838662..70d6ce4332 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/RecordTemplate.py @@ -1,25 +1,24 @@ -{%- let rec = ci.get_record_definition(name).unwrap() %} -class {{ type_name }}: - {%- call py::docstring(rec, 4) %} - {%- for field in rec.fields() %} - {{ field.name() }}: "{{ field|type_name }}" - {%- call py::docstring(field, 4) %} +class {{ rec.name }}: + {{ rec.docstring|docindent(4) -}} + {%- for field in rec.fields %} + {{ field.name }}: "{{ field|type_name }}" + {{ field.docstring|docindent(4) -}} {%- endfor %} {%- if rec.has_fields() %} - def __init__(self, *, {% for field in rec.fields() %} - {{- field.name() }}: "{{- field|type_name }}" - {%- if field.default_value().is_some() %} = _DEFAULT{% endif %} + def __init__(self, *, {% for field in rec.fields %} + {{- field.name }}: "{{- field|type_name }}" + {%- if field.has_default() %} = _DEFAULT{% endif %} {%- if !loop.last %}, {% endif %} {%- endfor %}): - {%- for field in rec.fields() %} - {%- let field_name = field.name() %} - {%- match field.default_value() %} + {%- for field in rec.fields %} + {%- let field_name = field.name %} + {%- match field.default %} {%- when None %} self.{{ field_name }} = {{ field_name }} {%- when Some(literal) %} if {{ field_name }} is _DEFAULT: - self.{{ field_name }} = {{ literal|literal_py(field) }} + self.{{ field_name }} = {{ literal|literal }} else: self.{{ field_name }} = {{ field_name }} {%- endmatch %} @@ -27,11 +26,11 @@ def __init__(self, *, {% for field in rec.fields() %} {%- endif %} def __str__(self): - return "{{ type_name }}({% for field in rec.fields() %}{{ field.name() }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in rec.fields() %}self.{{ field.name() }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + return "{{ rec.name }}({% for field in rec.fields %}{{ field.name }}={}{% if loop.last %}{% else %}, {% endif %}{% endfor %})".format({% for field in rec.fields %}self.{{ field.name }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) def __eq__(self, other): - {%- for field in rec.fields() %} - if self.{{ field.name() }} != other.{{ field.name() }}: + {%- for field in rec.fields %} + if self.{{ field.name }} != other.{{ field.name }}: return False {%- endfor %} return True @@ -39,28 +38,24 @@ def __eq__(self, other): class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return {{ type_name }}( - {%- for field in rec.fields() %} - {{ field.name() }}={{ field|read_fn }}(buf), + return {{ rec.name }}( + {%- for field in rec.fields %} + {{ field.name }}={{ field|read_fn }}(buf), {%- endfor %} ) @staticmethod def check_lower(value): - {%- if rec.fields().is_empty() %} - pass + {%- for field in rec.fields %} + {{ field|check_lower_fn }}(value.{{ field.name }}) {%- else %} - {%- for field in rec.fields() %} - {{ field|check_lower_fn }}(value.{{ field.name() }}) + pass {%- endfor %} - {%- endif %} @staticmethod def write(value, buf): - {%- if rec.has_fields() %} - {%- for field in rec.fields() %} - {{ field|write_fn }}(value.{{ field.name() }}, buf) - {%- endfor %} + {%- for field in rec.fields %} + {{ field|write_fn }}(value.{{ field.name }}, buf) {%- else %} pass - {%- endif %} + {%- endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py index 5005bd1147..fb4b5cd193 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py @@ -12,14 +12,14 @@ def default(): @staticmethod def alloc(size): - return _uniffi_rust_call(_UniffiLib.{{ ci.ffi_rustbuffer_alloc().name() }}, size) + return _uniffi_rust_call(_UniffiLib.{{ globals.ffi_rustbuffer_alloc.name }}, size) @staticmethod def reserve(rbuf, additional): - return _uniffi_rust_call(_UniffiLib.{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) + return _uniffi_rust_call(_UniffiLib.{{ globals.ffi_rustbuffer_reserve.name }}, rbuf, additional) def free(self): - return _uniffi_rust_call(_UniffiLib.{{ ci.ffi_rustbuffer_free().name() }}, self) + return _uniffi_rust_call(_UniffiLib.{{ globals.ffi_rustbuffer_free.name }}, self) def __str__(self): return "_UniffiRustBuffer(capacity={}, len={}, data={})".format( diff --git a/uniffi_bindgen/src/bindings/python/templates/StringHelper.py b/uniffi_bindgen/src/bindings/python/templates/StringHelper.py index d574edd09f..b74a079610 100644 --- a/uniffi_bindgen/src/bindings/python/templates/StringHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/StringHelper.py @@ -1,4 +1,4 @@ -class _UniffiConverterString: +class {{ ffi_converter_name }}: @staticmethod def check_lower(value): if not isinstance(value, str): diff --git a/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py b/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py index 76d7f8bcdb..dec65c389a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py +++ b/uniffi_bindgen/src/bindings/python/templates/TimestampHelper.py @@ -4,7 +4,7 @@ # There is a loss of precision when converting from Rust timestamps, # which are accurate to the nanosecond, # to Python datetimes, which have a variable precision due to the use of float as representation. -class _UniffiConverterTimestamp(_UniffiConverterRustBuffer): +class {{ ffi_converter_name }}(_UniffiConverterRustBuffer): @staticmethod def read(buf): seconds = buf.read_i64() diff --git a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py index ae012a7a06..bce810b098 100644 --- a/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/TopLevelFunctionTemplate.py @@ -1,42 +1,11 @@ -{%- if func.is_async() %} +{{ func|def }} {{ func.name }}({{ func|arg_list }}) -> {{ func|return_type }}: + {{ func.docstring|docindent(4) -}} -{%- match func.return_type() -%} -{%- when Some(return_type) %} -async def {{ func.name() }}({%- call py::arg_list_decl(func) -%}) -> "{{ return_type|type_name }}": -{% when None %} -async def {{ func.name() }}({%- call py::arg_list_decl(func) -%}) -> None: -{% endmatch %} - - {%- call py::docstring(func, 4) %} - {%- call py::setup_args(func) %} - return await _uniffi_rust_call_async( - _UniffiLib.{{ func.ffi_func().name() }}({% call py::arg_list_lowered(func) %}), - _UniffiLib.{{func.ffi_rust_future_poll(ci) }}, - _UniffiLib.{{func.ffi_rust_future_complete(ci) }}, - _UniffiLib.{{func.ffi_rust_future_free(ci) }}, - # lift function - {%- match func.return_type() %} - {%- when Some(return_type) %} - {{ return_type|lift_fn }}, - {%- when None %} - lambda val: None, - {% endmatch %} - {% call py::error_ffi_converter(func) %} + {%- match func.return_type().ty -%} + {%- when Some(return_type) %} + return {{ return_type|lift_fn }}( + {% if func.is_async() %}await {% endif %}{{ func|ffi_caller_name }}({{ func|arg_names }}) ) - -{%- else %} -{%- match func.return_type() -%} -{%- when Some(return_type) %} - -def {{ func.name() }}({%- call py::arg_list_decl(func) -%}) -> "{{ return_type|type_name }}": - {%- call py::docstring(func, 4) %} - {%- call py::setup_args(func) %} - return {{ return_type|lift_fn }}({% call py::to_ffi_call(func) %}) -{% when None %} - -def {{ func.name() }}({%- call py::arg_list_decl(func) -%}) -> None: - {%- call py::docstring(func, 4) %} - {%- call py::setup_args(func) %} - {% call py::to_ffi_call(func) %} -{% endmatch %} -{%- endif %} + {% when None %} + {% if func.is_async() %}await {% endif %}{{ func|ffi_caller_name }}({{ func|arg_names }}) + {%- endmatch %} diff --git a/uniffi_bindgen/src/bindings/python/templates/Types.py b/uniffi_bindgen/src/bindings/python/templates/Types.py index 2c83757dda..add63dce2f 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Types.py +++ b/uniffi_bindgen/src/bindings/python/templates/Types.py @@ -1,116 +1,92 @@ -{%- import "macros.py" as py %} - -{%- for type_ in ci.iter_types() %} -{%- let type_name = type_|type_name %} -{%- let ffi_converter_name = type_|ffi_converter_name %} -{%- let canonical_type_name = type_|canonical_name %} - -{# - # Map `Type` instances to an include statement for that type. - # - # There is a companion match in `PythonCodeOracle::create_code_type()` which performs a similar function for the - # Rust code. - # - # - When adding additional types here, make sure to also add a match arm to that function. - # - To keep things manageable, let's try to limit ourselves to these 2 mega-matches - #} -{%- match type_ %} - -{%- when Type::Boolean %} +{%- for type_def in type_definitions %} + +{%- let ffi_converter_name = type_def|ffi_converter_name %} + +{% match type_def %} + +{%- when TypeDefinition::Builtin(type_node) %} +{%- match type_node.kind %} +{%- when TypeKind::Boolean %} {%- include "BooleanHelper.py" %} -{%- when Type::Int8 %} +{%- when TypeKind::Int8 %} {%- include "Int8Helper.py" %} -{%- when Type::Int16 %} +{%- when TypeKind::Int16 %} {%- include "Int16Helper.py" %} -{%- when Type::Int32 %} +{%- when TypeKind::Int32 %} {%- include "Int32Helper.py" %} -{%- when Type::Int64 %} +{%- when TypeKind::Int64 %} {%- include "Int64Helper.py" %} -{%- when Type::UInt8 %} +{%- when TypeKind::UInt8 %} {%- include "UInt8Helper.py" %} -{%- when Type::UInt16 %} +{%- when TypeKind::UInt16 %} {%- include "UInt16Helper.py" %} -{%- when Type::UInt32 %} +{%- when TypeKind::UInt32 %} {%- include "UInt32Helper.py" %} -{%- when Type::UInt64 %} +{%- when TypeKind::UInt64 %} {%- include "UInt64Helper.py" %} -{%- when Type::Float32 %} +{%- when TypeKind::Float32 %} {%- include "Float32Helper.py" %} -{%- when Type::Float64 %} +{%- when TypeKind::Float64 %} {%- include "Float64Helper.py" %} -{%- when Type::String %} +{%- when TypeKind::String %} {%- include "StringHelper.py" %} -{%- when Type::Bytes %} +{%- when TypeKind::Bytes %} {%- include "BytesHelper.py" %} -{%- when Type::Enum { name, module_path } %} -{%- let e = ci.get_enum_definition(name).unwrap() %} -{# For enums, there are either an error *or* an enum, they can't be both. #} -{%- if ci.is_name_used_as_error(name) %} -{%- include "ErrorTemplate.py" %} -{%- else %} -{%- include "EnumTemplate.py" %} -{% endif %} - -{%- when Type::Record { name, module_path } %} -{%- include "RecordTemplate.py" %} - -{%- when Type::Timestamp %} +{%- when TypeKind::Timestamp %} {%- include "TimestampHelper.py" %} -{%- when Type::Duration %} +{%- when TypeKind::Duration %} {%- include "DurationHelper.py" %} -{%- when Type::Optional { inner_type } %} + +{%- when TypeKind::Optional { inner_type } %} {%- include "OptionalTemplate.py" %} -{%- when Type::Sequence { inner_type } %} +{%- when TypeKind::Sequence { inner_type } %} {%- include "SequenceTemplate.py" %} -{%- when Type::Map { key_type, value_type } %} +{%- when TypeKind::Map { key_type, value_type } %} {%- include "MapTemplate.py" %} -{%- when Type::CallbackInterface { name, module_path } %} +{%- else %} +# Invalid Builtin type: {type_def:?}") +{%- endmatch %} + +{%- when TypeDefinition::Enum(e) %} +{# For enums, there are either an error *or* an enum, they can't be both. #} + +{%- if e.self_type.is_used_as_error %} +{%- include "ErrorTemplate.py" %} +{%- else %} +{%- include "EnumTemplate.py" %} +{% endif %} + +{%- when TypeDefinition::Record(rec) %} +{%- include "RecordTemplate.py" %} + +{%- when TypeDefinition::Interface(interface) %} +{%- include "ObjectTemplate.py" %} + +{%- when TypeDefinition::CallbackInterface(cbi) %} {%- include "CallbackInterfaceTemplate.py" %} -{%- when Type::Custom { name, module_path, builtin } %} +{%- when TypeDefinition::Custom(custom_type) %} {%- include "CustomType.py" %} -{%- when Type::External { name, module_path, namespace, .. } %} -{%- include "ExternalTemplate.py" %} - -{%- else %} +{%- when TypeDefinition::External(_) %} {%- endmatch %} -{%- endfor %} - -# objects. -{%- for type_ in self.iter_sorted_object_types() %} -{%- match type_ %} -{%- when Type::Object { name, .. } %} -{%- let type_name = type_|type_name %} -{%- let ffi_converter_name = type_|ffi_converter_name %} -{%- let canonical_type_name = type_|canonical_name %} -{%- include "ObjectTemplate.py" %} -{%- else %} -{%- endmatch %} -{%- endfor %} - -{#- -Setup type aliases for our custom types, has complications due to -forward type references, #2067 --#} -{%- for (name, ty) in self.get_custom_type_aliases() %} -{{ name }} = {{ ty|type_name }} -{%- endfor %} + +{% endfor %} diff --git a/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py b/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py index 039bf76162..930cef3b68 100644 --- a/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/UInt16Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterUInt16(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "u16" VALUE_MIN = 0 VALUE_MAX = 2**16 diff --git a/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py b/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py index 1650bf9b60..1bb69afb6e 100644 --- a/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/UInt32Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterUInt32(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "u32" VALUE_MIN = 0 VALUE_MAX = 2**32 diff --git a/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py b/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py index f354545e26..4c98b60731 100644 --- a/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/UInt64Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterUInt64(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "u64" VALUE_MIN = 0 VALUE_MAX = 2**64 diff --git a/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py b/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py index cee130b4d9..92b46ee52c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py +++ b/uniffi_bindgen/src/bindings/python/templates/UInt8Helper.py @@ -1,4 +1,4 @@ -class _UniffiConverterUInt8(_UniffiConverterPrimitiveInt): +class {{ ffi_converter_name }}(_UniffiConverterPrimitiveInt): CLASS_NAME = "u8" VALUE_MIN = 0 VALUE_MAX = 2**8 diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py b/uniffi_bindgen/src/bindings/python/templates/VTable.py similarity index 59% rename from uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py rename to uniffi_bindgen/src/bindings/python/templates/VTable.py index 78c7e606f9..effd9e19f9 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py +++ b/uniffi_bindgen/src/bindings/python/templates/VTable.py @@ -1,28 +1,30 @@ -{% if self.include_once_check("CallbackInterfaceRuntime.py") %}{% include "CallbackInterfaceRuntime.py" %}{% endif %} -{%- let trait_impl=format!("_UniffiTraitImpl{}", name) %} - # Put all the bits inside a class to keep the top-level namespace clean -class {{ trait_impl }}: +class {{ vtable.name }}: # For each method, generate a callback function to pass to Rust - {%- for (ffi_callback, meth) in vtable_methods.iter() %} + {%- for (meth, ffi_type) in vtable.methods_and_ffi_types() %} - @{{ ffi_callback.name()|ffi_callback_name }} - def {{ meth.name() }}( - {%- for arg in ffi_callback.arguments() %} - {{ arg.name() }}, - {%- endfor -%} - {%- if ffi_callback.has_rust_call_status_arg() %} - uniffi_call_status_ptr, - {%- endif %} - ): + @{{ ffi_type|ffi_type_name }} + def {{ meth.name }}( + uniffi_handle, + {%- for arg in meth.arguments %} + {{ arg.name }}, + {%- endfor %} + {%- if meth.is_sync() %} + uniffi_out_return, + uniffi_call_status_ptr + {%- else %} + uniffi_future_callback, + uniffi_callback_data, + uniffi_out_return, + {%- endif %} + ): uniffi_obj = {{ ffi_converter_name }}._handle_map.get(uniffi_handle) def make_call(): - args = ({% for arg in meth.arguments() %}{{ arg|lift_fn }}({{ arg.name() }}), {% endfor %}) - method = uniffi_obj.{{ meth.name() }} - return method(*args) + return uniffi_obj.{{ meth.name }}({% for arg in meth.arguments %}{{ arg|lift_fn }}({{ arg.name }}), {% endfor %}) - {% if !meth.is_async() %} - {%- match meth.return_type() %} + {% match meth.async_data %} + {% when None %} + {%- match meth.return_type.ty %} {%- when Some(return_type) %} def write_return_value(v): uniffi_out_return[0] = {{ return_type|lower_fn }}(v) @@ -30,7 +32,7 @@ def write_return_value(v): write_return_value = lambda v: None {%- endmatch %} - {%- match meth.throws_type() %} + {%- match meth.throws_type.ty %} {%- when None %} _uniffi_trait_interface_call( uniffi_call_status_ptr.contents, @@ -46,12 +48,12 @@ def write_return_value(v): {{ error|lower_fn }}, ) {%- endmatch %} - {%- else %} + {% when Some(async_data) %} def handle_success(return_value): uniffi_future_callback( uniffi_callback_data, - {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}( - {%- if let Some(return_type) = meth.return_type() %} + {{ async_data.foreign_future_result_type|ffi_type_name }}( + {%- if let Some(return_type) = meth.return_type.ty %} {{ return_type|lower_fn }}(return_value), {%- endif %} _UniffiRustCallStatus.default() @@ -61,36 +63,34 @@ def handle_success(return_value): def handle_error(status_code, rust_buffer): uniffi_future_callback( uniffi_callback_data, - {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}( - {%- match meth.return_type() %} - {%- when Some(return_type) %} - {{ meth.return_type().map(FfiType::from)|ffi_default_value }}, - {%- when None %} - {%- endmatch %} + {{ async_data.foreign_future_result_type|ffi_type_name }}( + {%- if meth.return_type.is_some() %} + {{ meth.return_type|ffi_default_return }}, + {%- endif %} _UniffiRustCallStatus(status_code, rust_buffer), ) ) - {%- match meth.throws_type() %} + {%- match meth.throws_type.ty %} {%- when None %} uniffi_out_return[0] = _uniffi_trait_interface_call_async(make_call, handle_success, handle_error) {%- when Some(error) %} uniffi_out_return[0] = _uniffi_trait_interface_call_async_with_error(make_call, handle_success, handle_error, {{ error|type_name }}, {{ error|lower_fn }}) {%- endmatch %} - {%- endif %} + {%- endmatch %} {%- endfor %} - @{{ "CallbackInterfaceFree"|ffi_callback_name }} + @{{ globals.callback_interface_free_type|ffi_type_name }} def _uniffi_free(uniffi_handle): {{ ffi_converter_name }}._handle_map.remove(uniffi_handle) # Generate the FFI VTable. This has a field for each callback interface method. - _uniffi_vtable = {{ vtable|ffi_type_name }}( - {%- for (_, meth) in vtable_methods.iter() %} - {{ meth.name() }}, + _uniffi_vtable = {{ vtable.ffi_type|ffi_type_name }}( + {%- for meth in vtable.methods %} + {{ meth.name }}, {%- endfor %} _uniffi_free ) # Send Rust a pointer to the VTable. Note: this means we need to keep the struct alive forever, # or else bad things will happen when Rust tries to access it. - _UniffiLib.{{ ffi_init_callback.name() }}(ctypes.byref(_uniffi_vtable)) + _UniffiLib.{{ vtable.ffi_init_callback.name }}(ctypes.byref(_uniffi_vtable)) diff --git a/uniffi_bindgen/src/bindings/python/templates/macros.py b/uniffi_bindgen/src/bindings/python/templates/macros.py index 92e6df18ad..80efe42af7 100644 --- a/uniffi_bindgen/src/bindings/python/templates/macros.py +++ b/uniffi_bindgen/src/bindings/python/templates/macros.py @@ -1,178 +1,47 @@ -{# -// Template to call into rust. Used in several places. -// Variable names in `arg_list_decl` should match up with arg lists -// passed to rust via `arg_list_lowered` -#} - -{%- macro to_ffi_call(func) -%} -{%- call _to_ffi_call_with_prefix_arg("", func) %} -{%- endmacro -%} - -{%- macro to_ffi_call_with_prefix(prefix, func) -%} -{%- call _to_ffi_call_with_prefix_arg(format!("{},", prefix), func) %} -{%- endmacro -%} - -{%- macro _to_ffi_call_with_prefix_arg(prefix, func) -%} -{%- match func.throws_type() -%} -{%- when Some with (e) -%} -{%- match e -%} -{%- when Type::Enum { .. } -%} -_uniffi_rust_call_with_error({{ e|ffi_converter_name }}, -{%- when Type::Object { .. } -%} -_uniffi_rust_call_with_error({{ e|ffi_converter_name }}__as_error, -{%- else %} -# unsupported error type! -{%- endmatch %} -{%- else -%} -_uniffi_rust_call( -{%- endmatch -%} - _UniffiLib.{{ func.ffi_func().name() }}, - {{- prefix }} - {%- call arg_list_lowered(func) -%} -) -{%- endmacro -%} - -{%- macro arg_list_lowered(func) %} - {%- for arg in func.arguments() %} - {{ arg|lower_fn }}({{ arg.name() }}) - {%- if !loop.last %},{% endif %} - {%- endfor %} -{%- endmacro -%} - -{%- macro docstring_value(maybe_docstring, indent_spaces) %} -{%- if let Some(docstring) = maybe_docstring %} -{{ docstring|docstring(indent_spaces) }} -{{ "" }} -{%- endif %} -{%- endmacro %} - -{%- macro docstring(defn, indent_spaces) %} -{%- call docstring_value(defn.docstring(), indent_spaces) %} -{%- endmacro %} - -{#- -// Arglist as used in Python declarations of methods, functions and constructors. -// Note the type_name filters. --#} - -{% macro arg_list_decl(func) %} - {%- for arg in func.arguments() -%} - {{ arg.name() }} - {%- match arg.default_value() %} - {%- when Some with(literal) %}: "typing.Union[object, {{ arg|type_name -}}]" = _DEFAULT - {%- else %}: "{{ arg|type_name -}}" - {%- endmatch %} - {%- if !loop.last %},{% endif -%} - {%- endfor %} -{%- endmacro %} - -{#- -// Arglist as used in the _UniffiLib function declarations. -// Note unfiltered name but ffi_type_name filters. --#} -{%- macro arg_list_ffi_decl(func) %} - {%- for arg in func.arguments() %} - {{ arg.type_().borrow()|ffi_type_name }}, - {%- endfor %} - {%- if func.has_rust_call_status_arg() %} - ctypes.POINTER(_UniffiRustCallStatus),{% endif %} -{% endmacro -%} - -{# - # Setup function arguments by initializing default values. - #} -{%- macro setup_args(func) %} - {%- for arg in func.arguments() %} - {%- if let Some(literal) = arg.default_value() %} - if {{ arg.name() }} is _DEFAULT: - {{ arg.name() }} = {{ literal|literal_py(arg.as_type().borrow()) }} +{# Generates a definition for a function/method/constructor #} +{%- macro define_callable(callable) -%} +{{ callable|def }} {{ callable.name }}({{ callable|arg_list }}){% if !callable.is_primary_constructor() %} -> {{ callable|return_type }}{% endif %}: + {{ callable.docstring|docindent(4) -}} + {%- for arg in callable.arguments %} + {%- if let Some(literal) = arg.default %} + if {{ arg.name }} is _DEFAULT: + {{ arg.name }} = {{ literal|literal }} {%- endif %} - {{ arg|check_lower_fn }}({{ arg.name() }}) - {% endfor -%} -{%- endmacro -%} + {{ arg|check_lower_fn }}({{ arg.name }}) + {%- endfor %} -{# - # Exactly the same thing as `setup_args()` but with an extra 4 spaces of - # indent so that it works with object methods. - #} -{%- macro setup_args_extra_indent(func) %} - {%- for arg in func.arguments() %} - {%- if let Some(literal) = arg.default_value() %} - if {{ arg.name() }} is _DEFAULT: - {{ arg.name() }} = {{ literal|literal_py(arg.as_type().borrow()) }} + {%- if let Some(async_data) = callable.async_data %} + _uniffi_return = await _uniffi_rust_call_async( + _UniffiLib.{{ callable.ffi_func().name }}( + {%- if callable.is_method() %} + self._uniffi_clone_pointer(), + {%- endif %} + {%- for arg in callable.arguments() %} + {{ arg|lower_fn }}({{ arg.name }}), + {%- endfor %} + ), + _UniffiLib.{{ async_data.ffi_rust_future_poll.name }}, + _UniffiLib.{{ async_data.ffi_rust_future_complete.name }}, + _UniffiLib.{{ async_data.ffi_rust_future_free.name }}, + {{ callable|error_ffi_converter }}, + ) + {%- else %} + _uniffi_return = _uniffi_rust_call_with_error( + {{ callable|error_ffi_converter }}, + _UniffiLib.{{ callable.ffi_func().name }}, + {%- if callable.is_method() %} + self._uniffi_clone_pointer(), {%- endif %} - {{ arg|check_lower_fn }}({{ arg.name() }}) - {% endfor -%} -{%- endmacro -%} - -{# - # Macro to call methods - #} -{%- macro method_decl(py_method_name, meth) %} -{% if meth.is_async() %} - -{%- match meth.return_type() %} -{%- when Some(return_type) %} - async def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> "{{ return_type|type_name }}": -{%- when None %} - async def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> None: -{% endmatch %} - - {%- call docstring(meth, 8) %} - {%- call setup_args_extra_indent(meth) %} - return await _uniffi_rust_call_async( - _UniffiLib.{{ meth.ffi_func().name() }}( - self._uniffi_clone_pointer(), {% call arg_list_lowered(meth) %} - ), - _UniffiLib.{{ meth.ffi_rust_future_poll(ci) }}, - _UniffiLib.{{ meth.ffi_rust_future_complete(ci) }}, - _UniffiLib.{{ meth.ffi_rust_future_free(ci) }}, - # lift function - {%- match meth.return_type() %} - {%- when Some(return_type) %} - {{ return_type|lift_fn }}, - {%- when None %} - lambda val: None, - {% endmatch %} - {% call error_ffi_converter(meth) %} - ) - -{%- else -%} -{%- match meth.return_type() %} - -{%- when Some(return_type) %} - - def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> "{{ return_type|type_name }}": - {%- call docstring(meth, 8) %} - {%- call setup_args_extra_indent(meth) %} - return {{ return_type|lift_fn }}( - {% call to_ffi_call_with_prefix("self._uniffi_clone_pointer()", meth) %} - ) - -{%- when None %} - - def {{ py_method_name }}(self, {% call arg_list_decl(meth) %}) -> None: - {%- call docstring(meth, 8) %} - {%- call setup_args_extra_indent(meth) %} - {% call to_ffi_call_with_prefix("self._uniffi_clone_pointer()", meth) %} -{% endmatch %} -{% endif %} + {%- for arg in callable.arguments() %} + {{ arg|lower_fn }}({{ arg.name }}), + {%- endfor %} + ) + {%- endif %} -{% endmacro %} + {%- if callable.is_primary_constructor() %} + self._pointer = _uniffi_return + {%- else if let Some(return_type) = &callable.return_type().ty %} + return {{ return_type|lift_fn }}(_uniffi_return) + {%- endif %} -{%- macro error_ffi_converter(func) %} - # Error FFI converter -{% match func.throws_type() %} -{%- when Some(e) %} -{%- match e -%} -{%- when Type::Enum { .. } -%} - {{ e|ffi_converter_name }}, -{%- when Type::Object { .. } -%} - {{ e|ffi_converter_name }}__as_error, -{%- else %} - # unsupported error type! -{%- endmatch %} -{%- when None %} - None, -{%- endmatch %} -{% endmacro %} +{%- endmacro %} diff --git a/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/uniffi_bindgen/src/bindings/python/templates/wrapper.py index d97761f192..6840db5f81 100644 --- a/uniffi_bindgen/src/bindings/python/templates/wrapper.py +++ b/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -1,4 +1,7 @@ -{%- call py::docstring_value(ci.namespace_docstring(), 0) %} +{%- import "macros.py" as py %} +{%- if let Some(docstring) = module_docstring %} +{{ docstring }} +{%- endif %} # This file was autogenerated by some hot garbage in the `uniffi` crate. # Trust me, you don't want to mess with it! @@ -27,12 +30,9 @@ import itertools import traceback import typing -{%- if ci.has_async_fns() %} -import asyncio -{%- endif %} import platform -{%- for req in self.imports() %} -{{ req.render() }} +{%- for import in imports %} +{{ import }} {%- endfor %} # Used for default argument values @@ -46,35 +46,29 @@ # Contains loading, initialization code, and the FFI Function declarations. {% include "NamespaceLibraryTemplate.py" %} -# Public interface members begin here. -{{ type_helper_code }} - -# Async support -{%- if ci.has_async_fns() %} +# Runtimes loaded: +# {{ "{:?}"|format(runtimes) }} +{%- if runtimes.callback_interface %} +{%- include "CallbackInterfaceRuntime.py" %} +{%- endif %} +{%- if runtimes.async_ %} {%- include "Async.py" %} {%- endif %} -{%- for func in ci.function_definitions() %} -{%- include "TopLevelFunctionTemplate.py" %} +# Public interface members begin here. +{%- for protocol in protocols %} +{%- include "Protocol.py" %} +{%- endfor %} + +{%- include "Types.py" %} + +{%- for func in functions %} +{% call py::define_callable(func) %} {%- endfor %} __all__ = [ "InternalError", - {%- for e in ci.enum_definitions() %} - "{{ e.name() }}", - {%- endfor %} - {%- for record in ci.record_definitions() %} - "{{ record.name() }}", - {%- endfor %} - {%- for func in ci.function_definitions() %} - "{{ func.name() }}", - {%- endfor %} - {%- for obj in ci.object_definitions() %} - "{{ obj.name() }}", - {%- endfor %} - {%- for c in ci.callback_interface_definitions() %} - "{{ c.name() }}", + {%- for export in exports %} + "{{ export }}", {%- endfor %} ] - -{% import "macros.py" as py %}