Skip to main content

weaveffi_gen_python/
lib.rs

1//! Python (`ctypes`) binding generator for WeaveFFI.
2//!
3//! Emits a pip-installable package containing `ctypes`-based bindings and
4//! `.pyi` type stubs over the C ABI. Async functions surface as
5//! `async def` wrappers. Implements [`LanguageBackend`]; the shared driver
6//! bridges it into the generator pipeline.
7
8use camino::Utf8Path;
9use serde::{Deserialize, Serialize};
10use weaveffi_core::abi::{self, CType};
11use weaveffi_core::backend::{LanguageBackend, OutputFile};
12use weaveffi_core::capabilities::TargetCapabilities;
13use weaveffi_core::codegen::common::{
14    emit_doc as common_emit_doc, is_c_pointer_type, pascal_case, DocCommentStyle,
15};
16use weaveffi_core::model::{
17    BindingModel, CallShape, CallbackBinding, EnumBinding, FieldBinding, FnBinding,
18    ListenerBinding, ModuleBinding, ParamBinding, StructBinding,
19};
20use weaveffi_core::pkg::{self, ResolvedPackage};
21use weaveffi_core::utils::{
22    local_type_name, render_prelude, render_trailer, wrapper_name, CommentStyle,
23};
24use weaveffi_ir::ir::{Api, TypeRef};
25
26/// Per-target configuration for [`PythonGenerator`].
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28#[serde(default)]
29pub struct PythonConfig {
30    /// pip-installable Python package name (default `"weaveffi"`). Also
31    /// determines the on-disk package directory inside `python/`.
32    pub package_name: Option<String>,
33    /// When `true`, strip the IR module name prefix from emitted Python
34    /// function names.
35    pub strip_module_prefix: bool,
36    /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally
37    /// via `[global] c_prefix`; honored so the ctypes bindings call the same
38    /// exported symbols the producer emits.
39    pub prefix: Option<String>,
40    /// Basename of the IDL the CLI was invoked with.
41    #[serde(skip)]
42    pub input_basename: Option<String>,
43}
44
45impl PythonConfig {
46    pub fn package_name(&self) -> &str {
47        self.package_name.as_deref().unwrap_or("weaveffi")
48    }
49
50    pub fn prefix(&self) -> &str {
51        self.prefix.as_deref().unwrap_or("weaveffi")
52    }
53
54    pub fn input_basename(&self) -> &str {
55        self.input_basename.as_deref().unwrap_or("weaveffi.yml")
56    }
57}
58
59pub struct PythonGenerator;
60
61impl PythonGenerator {
62    /// Render the primary `weaveffi.py` source by composing the shared
63    /// [`LanguageBackend::emit_members`] walk over every module. Shared by the
64    /// [`LanguageBackend::files`] hook and the test-facing
65    /// [`render_python_module`] wrapper so there is one assembly path.
66    fn render_py_source(
67        &self,
68        model: &BindingModel,
69        strip_module_prefix: bool,
70        input_basename: &str,
71    ) -> String {
72        let config = PythonConfig {
73            strip_module_prefix,
74            ..PythonConfig::default()
75        };
76        let mut out = render_prelude(CommentStyle::Hash, input_basename);
77        render_preamble(&mut out);
78        let has_async = model.functions().any(|(_, f)| f.is_async);
79        if has_async {
80            out.push_str("\nimport asyncio\nimport threading\n");
81        }
82        let has_listeners = model.modules.iter().any(|m| !m.listeners.is_empty());
83        if has_listeners {
84            out.push_str(
85                "\n\n# Registered listener trampolines, keyed by subscription id. Holding\n\
86                 # the ctypes function objects here keeps them alive until unregistered;\n\
87                 # without this the GC could collect a trampoline the producer still calls.\n\
88                 _listener_refs: Dict[int, object] = {}\n",
89            );
90        }
91        // The model is a flat, pre-order list of modules, each carrying its
92        // joined symbol path — the same traversal order the recursive walk
93        // produced.
94        for m in &model.modules {
95            out.push_str(&format!("\n\n# === Module: {} ===", m.path));
96            self.emit_members(&mut out, m, &config);
97        }
98        out.push('\n');
99        out.push_str(&render_trailer(CommentStyle::Hash, "weaveffi.py"));
100        out
101    }
102}
103
104impl LanguageBackend for PythonGenerator {
105    type Config = PythonConfig;
106
107    fn name(&self) -> &'static str {
108        "python"
109    }
110
111    fn capabilities(&self) -> TargetCapabilities {
112        TargetCapabilities::full()
113    }
114
115    fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str {
116        config.prefix()
117    }
118
119    fn render_enum(&self, out: &mut String, e: &EnumBinding, _config: &Self::Config) {
120        render_enum(out, e);
121    }
122
123    fn render_struct(
124        &self,
125        out: &mut String,
126        _module: &ModuleBinding,
127        s: &StructBinding,
128        _config: &Self::Config,
129    ) {
130        render_struct(out, s);
131        if s.builder.is_some() {
132            render_builder(out, s);
133        }
134    }
135
136    fn render_callback(
137        &self,
138        out: &mut String,
139        _module: &ModuleBinding,
140        c: &CallbackBinding,
141        _config: &Self::Config,
142    ) {
143        render_callback_type(out, c);
144    }
145
146    fn render_listener(
147        &self,
148        out: &mut String,
149        module: &ModuleBinding,
150        l: &ListenerBinding,
151        config: &Self::Config,
152    ) {
153        render_listener(out, module, l, config.strip_module_prefix);
154    }
155
156    fn render_function(
157        &self,
158        out: &mut String,
159        module: &ModuleBinding,
160        f: &FnBinding,
161        config: &Self::Config,
162    ) {
163        render_function(out, &module.path, f, config.strip_module_prefix);
164    }
165
166    fn files(
167        &self,
168        api: &Api,
169        model: &BindingModel,
170        out_dir: &Utf8Path,
171        config: &Self::Config,
172    ) -> Vec<OutputFile> {
173        let package = pkg::resolve(
174            api,
175            config.package_name.as_deref(),
176            config.input_basename.as_deref(),
177        );
178        let import_name = package.ident_name();
179        let input_basename = config.input_basename();
180        let dir = out_dir.join("python");
181        let pkg_dir = dir.join(&import_name);
182        let hash = CommentStyle::Hash;
183        vec![
184            OutputFile::new(
185                pkg_dir.join("__init__.py"),
186                format!(
187                    "{}from .weaveffi import *  # noqa: F401,F403\n\n{}",
188                    render_prelude(hash, input_basename),
189                    render_trailer(hash, "__init__.py"),
190                ),
191            ),
192            OutputFile::new(
193                pkg_dir.join("weaveffi.py"),
194                self.render_py_source(model, config.strip_module_prefix, input_basename),
195            ),
196            OutputFile::new(
197                pkg_dir.join("weaveffi.pyi"),
198                render_pyi_module(api, config.strip_module_prefix, input_basename),
199            ),
200            OutputFile::new(
201                dir.join("pyproject.toml"),
202                render_pyproject_toml(&package, &import_name, input_basename),
203            ),
204            OutputFile::new(
205                dir.join("setup.py"),
206                render_setup_py(&package, &import_name, input_basename),
207            ),
208            OutputFile::new(
209                dir.join("README.md"),
210                render_readme(&package, input_basename),
211            ),
212        ]
213    }
214}
215
216weaveffi_core::impl_generator_via_backend!(PythonGenerator);
217
218// ── Type helpers ──
219
220fn py_ctypes_scalar(ty: &TypeRef) -> &'static str {
221    match ty {
222        TypeRef::I32 => "ctypes.c_int32",
223        TypeRef::U32 => "ctypes.c_uint32",
224        TypeRef::I64 => "ctypes.c_int64",
225        TypeRef::F64 => "ctypes.c_double",
226        TypeRef::Bool => "ctypes.c_int32",
227        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "ctypes.c_char_p",
228        TypeRef::Handle => "ctypes.c_uint64",
229        TypeRef::TypedHandle(_) => "ctypes.c_void_p",
230        TypeRef::Bytes | TypeRef::BorrowedBytes => "ctypes.c_uint8",
231        TypeRef::Struct(_) => "ctypes.c_void_p",
232        TypeRef::Enum(_) => "ctypes.c_int32",
233        TypeRef::Optional(_) | TypeRef::List(_) | TypeRef::Map(_, _) | TypeRef::Iterator(_) => {
234            "ctypes.c_void_p"
235        }
236    }
237}
238
239fn py_type_hint(ty: &TypeRef) -> String {
240    match ty {
241        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::Handle => "int".into(),
242        // Structs, enums, and typed handles all surface as bare local class names
243        // in the generated module. A cross-module reference (e.g. `handle<Store>`
244        // resolved to `kv.Store`) must still annotate the *local* `Store`, not the
245        // qualified IR name, which is not a symbol in this module.
246        TypeRef::TypedHandle(name) => format!("\"{}\"", local_type_name(name)),
247        TypeRef::F64 => "float".into(),
248        TypeRef::Bool => "bool".into(),
249        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "str".into(),
250        TypeRef::Bytes | TypeRef::BorrowedBytes => "bytes".into(),
251        TypeRef::Enum(name) => format!("\"{}\"", local_type_name(name)),
252        TypeRef::Struct(name) => format!("\"{}\"", local_type_name(name)),
253        TypeRef::Optional(inner) => format!("Optional[{}]", py_type_hint(inner)),
254        TypeRef::List(inner) => format!("List[{}]", py_type_hint(inner)),
255        TypeRef::Map(k, v) => format!("Dict[{}, {}]", py_type_hint(k), py_type_hint(v)),
256        TypeRef::Iterator(inner) => format!("Iterator[{}]", py_type_hint(inner)),
257    }
258}
259
260/// Maps a shared ABI [`CType`] onto its `ctypes` spelling. The structural
261/// lowering (which slots exist, in what order) comes from
262/// [`weaveffi_core::abi`]; this is the Python-specific vocabulary applied to
263/// each slot. Opaque handles and structs collapse to `c_void_p`; `char*`
264/// becomes the `c_char_p` convenience type.
265fn py_ctype(ty: &CType) -> String {
266    match ty {
267        CType::Int32 => "ctypes.c_int32".into(),
268        CType::Uint32 => "ctypes.c_uint32".into(),
269        CType::Int64 => "ctypes.c_int64".into(),
270        CType::Uint64 => "ctypes.c_uint64".into(),
271        CType::Double => "ctypes.c_double".into(),
272        CType::Bool => "ctypes.c_int32".into(),
273        CType::Size => "ctypes.c_size_t".into(),
274        CType::Handle => "ctypes.c_uint64".into(),
275        CType::Char => "ctypes.c_char".into(),
276        CType::Uint8 => "ctypes.c_uint8".into(),
277        CType::Void => "None".into(),
278        CType::Enum { .. } => "ctypes.c_int32".into(),
279        CType::CancelToken | CType::Error | CType::StructTag { .. } | CType::Named(_) => {
280            "ctypes.c_void_p".into()
281        }
282        CType::Ptr { pointee, .. } => match pointee.as_ref() {
283            CType::Char => "ctypes.c_char_p".into(),
284            CType::StructTag { .. } | CType::CancelToken | CType::Void | CType::Named(_) => {
285                "ctypes.c_void_p".into()
286            }
287            other => format!("ctypes.POINTER({})", py_ctype(other)),
288        },
289    }
290}
291
292fn py_param_argtypes(ty: &TypeRef) -> Vec<String> {
293    abi::lower_param("_", ty, "", false)
294        .iter()
295        .map(|p| py_ctype(&p.ty))
296        .collect()
297}
298
299/// Returns `(restype, out_param_argtypes)` for a return type.
300fn py_return_info(ty: &TypeRef) -> (String, Vec<String>) {
301    // Map returns marshal via `byref` out-params, which ctypes models with an
302    // extra `POINTER` level beyond the shared C ABI shape. This convention is
303    // Python-specific, so it stays local rather than in the shared model.
304    if let Some((k, v)) = get_map_kv(ty) {
305        return (
306            "None".into(),
307            vec![
308                format!("ctypes.POINTER(ctypes.POINTER({}))", py_ctypes_scalar(k)),
309                format!("ctypes.POINTER(ctypes.POINTER({}))", py_ctypes_scalar(v)),
310                "ctypes.POINTER(ctypes.c_size_t)".into(),
311            ],
312        );
313    }
314    // Iterator constructors return the opaque iterator handle; the `_next`
315    // signature is emitted separately by the iterator code path.
316    if matches!(ty, TypeRef::Iterator(_)) {
317        return ("ctypes.c_void_p".into(), vec![]);
318    }
319    let r = abi::lower_return(ty, "");
320    let out = r.out_params.iter().map(|p| py_ctype(&p.ty)).collect();
321    (py_ctype(&r.ret), out)
322}
323
324fn get_map_kv(ty: &TypeRef) -> Option<(&TypeRef, &TypeRef)> {
325    match ty {
326        TypeRef::Map(k, v) => Some((k, v)),
327        TypeRef::Optional(inner) => get_map_kv(inner),
328        _ => None,
329    }
330}
331
332/// `(param_name, ctypes_type)` pairs for async C callback parameters after `(context, err)`.
333fn py_async_cb_trailing_fields(ret: &Option<TypeRef>) -> Vec<(String, String)> {
334    match ret {
335        None => vec![],
336        // Optional peeling stays local so `Optional<bytes>`/`<list>`/`<map>`
337        // still surface their trailing `result_len`, matching the inner type.
338        Some(TypeRef::Optional(inner)) if is_c_pointer_type(inner) => {
339            py_async_cb_trailing_fields(&Some((**inner).clone()))
340        }
341        Some(ty) => abi::callback_result_params(ty, "")
342            .into_iter()
343            .map(|p| (p.name, py_ctype(&p.ty)))
344            .collect(),
345    }
346}
347
348fn append_async_success_handler(out: &mut String, ret: &Option<TypeRef>, ind: &str) {
349    match ret {
350        None => {
351            out.push_str(&format!("{ind}_state[\"val\"] = None\n"));
352        }
353        Some(TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle) => {
354            out.push_str(&format!("{ind}_state[\"val\"] = result\n"));
355        }
356        Some(TypeRef::Bool) => {
357            out.push_str(&format!("{ind}_state[\"val\"] = bool(result)\n"));
358        }
359        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
360            out.push_str(&format!(
361                "{ind}_s = _bytes_to_string(result) or \"\" if result else \"\"\n"
362            ));
363            out.push_str(&format!("{ind}if result:\n"));
364            out.push_str(&format!("{ind}    _lib.weaveffi_free_string(result)\n"));
365            out.push_str(&format!("{ind}_state[\"val\"] = _s\n"));
366        }
367        Some(TypeRef::Enum(name)) => {
368            let name = local_type_name(name);
369            out.push_str(&format!("{ind}_state[\"val\"] = {name}(result)\n"));
370        }
371        Some(TypeRef::Struct(name)) | Some(TypeRef::TypedHandle(name)) => {
372            let name = local_type_name(name);
373            out.push_str(&format!("{ind}if result is None:\n"));
374            out.push_str(&format!(
375                "{ind}    _state[\"err\"] = WeaveFFIError(-1, \"null pointer\")\n"
376            ));
377            out.push_str(&format!("{ind}else:\n"));
378            out.push_str(&format!("{ind}    _state[\"val\"] = {name}(result)\n"));
379        }
380        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
381            out.push_str(&format!("{ind}if not result:\n"));
382            out.push_str(&format!("{ind}    _state[\"val\"] = b\"\"\n"));
383            out.push_str(&format!("{ind}else:\n"));
384            out.push_str(&format!("{ind}    _n = int(result_len)\n"));
385            out.push_str(&format!("{ind}    _state[\"val\"] = bytes(result[:_n])\n"));
386            out.push_str(&format!(
387                "{ind}    _lib.weaveffi_free_bytes(result, ctypes.c_size_t(_n))\n"
388            ));
389        }
390        Some(TypeRef::List(inner)) => {
391            let elem = py_read_element("result[_i]", inner);
392            out.push_str(&format!("{ind}if not result:\n"));
393            out.push_str(&format!("{ind}    _state[\"val\"] = []\n"));
394            out.push_str(&format!("{ind}else:\n"));
395            out.push_str(&format!("{ind}    _rl = int(result_len)\n"));
396            out.push_str(&format!(
397                "{ind}    _state[\"val\"] = [{elem} for _i in range(_rl)]\n"
398            ));
399        }
400        Some(TypeRef::Map(k, v)) => {
401            let kread = py_read_element("result_keys[_i]", k);
402            let vread = py_read_element("result_values[_i]", v);
403            out.push_str(&format!("{ind}if not result_keys or not result_values:\n"));
404            out.push_str(&format!("{ind}    _state[\"val\"] = {{}}\n"));
405            out.push_str(&format!("{ind}else:\n"));
406            out.push_str(&format!("{ind}    _ml = int(result_len)\n"));
407            out.push_str(&format!(
408                "{ind}    _state[\"val\"] = {{{kread}: {vread} for _i in range(_ml)}}\n"
409            ));
410        }
411        Some(TypeRef::Optional(inner)) => {
412            if is_c_pointer_type(inner) {
413                match inner.as_ref() {
414                    TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
415                        out.push_str(&format!("{ind}if not result:\n"));
416                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
417                        out.push_str(&format!("{ind}else:\n"));
418                        out.push_str(&format!("{ind}    _s = _bytes_to_string(result)\n"));
419                        out.push_str(&format!("{ind}    _lib.weaveffi_free_string(result)\n"));
420                        out.push_str(&format!("{ind}    _state[\"val\"] = _s\n"));
421                    }
422                    TypeRef::Struct(name) | TypeRef::TypedHandle(name) => {
423                        let name = local_type_name(name);
424                        out.push_str(&format!("{ind}if not result:\n"));
425                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
426                        out.push_str(&format!("{ind}else:\n"));
427                        out.push_str(&format!("{ind}    _state[\"val\"] = {name}(result)\n"));
428                    }
429                    TypeRef::Bytes | TypeRef::BorrowedBytes => {
430                        out.push_str(&format!("{ind}if not result:\n"));
431                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
432                        out.push_str(&format!("{ind}else:\n"));
433                        out.push_str(&format!("{ind}    _n = int(result_len)\n"));
434                        out.push_str(&format!("{ind}    _b = bytes(result[:_n])\n"));
435                        out.push_str(&format!(
436                            "{ind}    _lib.weaveffi_free_bytes(result, ctypes.c_size_t(_n))\n"
437                        ));
438                        out.push_str(&format!("{ind}    _state[\"val\"] = _b\n"));
439                    }
440                    TypeRef::List(elem) => {
441                        let read = py_read_element("result[_i]", elem);
442                        out.push_str(&format!("{ind}if not result:\n"));
443                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
444                        out.push_str(&format!("{ind}else:\n"));
445                        out.push_str(&format!("{ind}    _rl = int(result_len)\n"));
446                        out.push_str(&format!(
447                            "{ind}    _state[\"val\"] = [{read} for _i in range(_rl)]\n"
448                        ));
449                    }
450                    TypeRef::Map(k, v) => {
451                        let kread = py_read_element("result_keys[_i]", k);
452                        let vread = py_read_element("result_values[_i]", v);
453                        out.push_str(&format!("{ind}if not result_keys or not result_values:\n"));
454                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
455                        out.push_str(&format!("{ind}else:\n"));
456                        out.push_str(&format!("{ind}    _ml = int(result_len)\n"));
457                        out.push_str(&format!(
458                            "{ind}    _state[\"val\"] = {{{kread}: {vread} for _i in range(_ml)}}\n"
459                        ));
460                    }
461                    _ => append_async_success_handler(out, &Some(*inner.clone()), ind),
462                }
463            } else {
464                match inner.as_ref() {
465                    TypeRef::Bool => {
466                        out.push_str(&format!("{ind}if not result:\n"));
467                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
468                        out.push_str(&format!("{ind}else:\n"));
469                        out.push_str(&format!("{ind}    _state[\"val\"] = bool(result[0])\n"));
470                    }
471                    TypeRef::Enum(name) => {
472                        let name = local_type_name(name);
473                        out.push_str(&format!("{ind}if not result:\n"));
474                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
475                        out.push_str(&format!("{ind}else:\n"));
476                        out.push_str(&format!("{ind}    _state[\"val\"] = {name}(result[0])\n"));
477                    }
478                    _ => {
479                        out.push_str(&format!("{ind}if not result:\n"));
480                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
481                        out.push_str(&format!("{ind}else:\n"));
482                        out.push_str(&format!("{ind}    _state[\"val\"] = result[0]\n"));
483                    }
484                }
485            }
486        }
487        // Validation rejects `async` + `iter<T>` (AsyncIteratorReturn), so an
488        // iterator can never reach an async completion handler.
489        Some(TypeRef::Iterator(_)) => unreachable!("async iterator returns are rejected upstream"),
490    }
491}
492
493fn render_async_ffi_call_body(out: &mut String, f: &FnBinding) {
494    let c_async = format!("{}_async", f.c_base);
495    let ind = "    ";
496
497    out.push_str(&format!("{ind}_fn = _lib.{c_async}\n"));
498    out.push_str(&format!("{ind}_ev = threading.Event()\n"));
499    out.push_str(&format!("{ind}_state = {{\"err\": None, \"val\": None}}\n"));
500
501    let trailing = py_async_cb_trailing_fields(&f.ret);
502    let mut cb_param_list: Vec<String> = vec!["context".into(), "err".into()];
503    cb_param_list.extend(trailing.iter().map(|(n, _)| n.clone()));
504    let cb_params_joined = cb_param_list.join(", ");
505
506    out.push_str(&format!("{ind}def _cb_impl({cb_params_joined}):\n"));
507    out.push_str(&format!("{ind}    try:\n"));
508    out.push_str(&format!(
509        "{ind}        if err and err.contents.code != 0:\n"
510    ));
511    out.push_str(&format!("{ind}            _code = err.contents.code\n"));
512    out.push_str(&format!(
513        "{ind}            _msg = err.contents.message.decode(\"utf-8\") if err.contents.message else \"\"\n"
514    ));
515    out.push_str(&format!(
516        "{ind}            _lib.weaveffi_error_clear(ctypes.byref(err.contents))\n"
517    ));
518    out.push_str(&format!(
519        "{ind}            _state[\"err\"] = WeaveFFIError(_code, _msg)\n"
520    ));
521    out.push_str(&format!("{ind}        else:\n"));
522    append_async_success_handler(out, &f.ret, "                ");
523    out.push_str(&format!("{ind}    finally:\n"));
524    out.push_str(&format!("{ind}        _ev.set()\n"));
525
526    let mut cf_parts: Vec<String> = vec![
527        "ctypes.c_void_p".into(),
528        "ctypes.POINTER(_WeaveFFIErrorStruct)".into(),
529    ];
530    cf_parts.extend(trailing.iter().map(|(_, t)| t.clone()));
531    out.push_str(&format!(
532        "{ind}_cb_type = ctypes.CFUNCTYPE(None, {})\n",
533        cf_parts.join(", ")
534    ));
535    out.push_str(&format!("{ind}_cb = _cb_type(_cb_impl)\n"));
536
537    let mut argtypes: Vec<String> = Vec::new();
538    for p in &f.params {
539        argtypes.extend(py_param_argtypes(&p.ty));
540    }
541    if f.cancellable {
542        argtypes.push("ctypes.c_void_p".into());
543    }
544    argtypes.push("_cb_type".into());
545    argtypes.push("ctypes.c_void_p".into());
546
547    out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", ")));
548    out.push_str(&format!("{ind}_fn.restype = None\n"));
549
550    for p in &f.params {
551        for line in py_param_conversion(&p.name, &p.ty, ind) {
552            out.push_str(&line);
553            out.push('\n');
554        }
555    }
556
557    let mut call_args: Vec<String> = Vec::new();
558    for p in &f.params {
559        call_args.extend(py_param_call_args(&p.name, &p.ty));
560    }
561    if f.cancellable {
562        call_args.push("None".into());
563    }
564    call_args.push("_cb".into());
565    call_args.push("None".into());
566
567    out.push_str(&format!("{ind}_fn({})\n", call_args.join(", ")));
568    out.push_str(&format!("{ind}_ev.wait()\n"));
569    out.push_str(&format!("{ind}if _state[\"err\"] is not None:\n"));
570    out.push_str(&format!("{ind}    raise _state[\"err\"]\n"));
571    if f.ret.is_some() {
572        out.push_str(&format!("{ind}return _state[\"val\"]\n"));
573    }
574}
575
576// ── Rendering ──
577
578/// Render the `weaveffi.py` module source. Thin wrapper over the shared
579/// [`LanguageBackend::emit_members`] walk (via
580/// [`PythonGenerator::render_py_source`]); retained for direct use in tests.
581#[cfg(test)]
582fn render_python_module(
583    api: &Api,
584    strip_module_prefix: bool,
585    prefix: &str,
586    input_basename: &str,
587) -> String {
588    let model = BindingModel::build(api, prefix);
589    PythonGenerator.render_py_source(&model, strip_module_prefix, input_basename)
590}
591
592/// Emits a Python `# ...` line comment at `indent`. Used above C ABI binding
593/// declarations (`attach_function`-style binds) where docstrings can't live.
594fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str) {
595    common_emit_doc(out, doc, indent, DocCommentStyle::Hash);
596}
597
598/// Emits a Python triple-quoted `"""..."""` docstring as the first statement
599/// of a class or function body, at the given `indent`.
600fn emit_docstring(out: &mut String, doc: &Option<String>, indent: &str) {
601    let Some(doc) = doc else {
602        return;
603    };
604    let doc = doc.trim();
605    if doc.is_empty() {
606        return;
607    }
608    if doc.contains('\n') {
609        out.push_str(indent);
610        out.push_str("\"\"\"\n");
611        for line in doc.lines() {
612            if line.is_empty() {
613                out.push('\n');
614            } else {
615                out.push_str(indent);
616                out.push_str(line);
617                out.push('\n');
618            }
619        }
620        out.push_str(indent);
621        out.push_str("\"\"\"\n");
622    } else {
623        out.push_str(indent);
624        out.push_str("\"\"\"");
625        out.push_str(doc);
626        out.push_str("\"\"\"\n");
627    }
628}
629
630/// Emits a NumPy/Google-style docstring with a `Parameters` section listing
631/// each parameter that has a `doc:` value. Skips entirely when there is
632/// nothing to document.
633fn emit_fn_docstring(
634    out: &mut String,
635    doc: &Option<String>,
636    params: &[ParamBinding],
637    indent: &str,
638) {
639    let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty());
640    let documented_params: Vec<&ParamBinding> = params
641        .iter()
642        .filter(|p| {
643            p.doc
644                .as_ref()
645                .map(|d| !d.trim().is_empty())
646                .unwrap_or(false)
647        })
648        .collect();
649    if trimmed_doc.is_none() && documented_params.is_empty() {
650        return;
651    }
652    out.push_str(indent);
653    out.push_str("\"\"\"");
654    if let Some(d) = trimmed_doc {
655        if d.contains('\n') {
656            out.push('\n');
657            for line in d.lines() {
658                if line.is_empty() {
659                    out.push('\n');
660                } else {
661                    out.push_str(indent);
662                    out.push_str(line);
663                    out.push('\n');
664                }
665            }
666        } else {
667            out.push_str(d);
668            out.push('\n');
669        }
670    } else {
671        out.push('\n');
672    }
673    if !documented_params.is_empty() {
674        out.push('\n');
675        out.push_str(indent);
676        out.push_str("Parameters\n");
677        out.push_str(indent);
678        out.push_str("----------\n");
679        for p in documented_params {
680            let pdoc = p.doc.as_ref().unwrap().trim();
681            let mut lines = pdoc.lines();
682            let first = lines.next().unwrap_or("");
683            out.push_str(indent);
684            out.push_str(&format!("{} : {}\n", p.name, first));
685            for line in lines {
686                out.push_str(indent);
687                if line.is_empty() {
688                    out.push('\n');
689                } else {
690                    out.push_str("    ");
691                    out.push_str(line);
692                    out.push('\n');
693                }
694            }
695        }
696    }
697    out.push_str(indent);
698    out.push_str("\"\"\"\n");
699}
700
701fn render_preamble(out: &mut String) {
702    out.push_str(
703        r#""""WeaveFFI Python ctypes bindings (auto-generated)"""
704import contextlib
705import ctypes
706import os
707import platform
708from enum import IntEnum
709from typing import Callable, Dict, Iterator, List, Optional
710
711
712class WeaveFFIError(Exception):
713    def __init__(self, code: int, message: str) -> None:
714        self.code = code
715        self.message = message
716        super().__init__(f"({code}) {message}")
717
718
719class _WeaveFFIErrorStruct(ctypes.Structure):
720    _fields_ = [
721        ("code", ctypes.c_int32),
722        ("message", ctypes.c_char_p),
723    ]
724
725
726def _load_library() -> ctypes.CDLL:
727    # An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a
728    # specific build artifact regardless of its file name or location.
729    override = os.environ.get("WEAVEFFI_LIBRARY")
730    if override:
731        return ctypes.CDLL(override)
732    system = platform.system()
733    if system == "Darwin":
734        name = "libweaveffi.dylib"
735    elif system == "Windows":
736        name = "weaveffi.dll"
737    else:
738        name = "libweaveffi.so"
739    return ctypes.CDLL(name)
740
741
742_lib = _load_library()
743_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)]
744_lib.weaveffi_error_clear.restype = None
745_lib.weaveffi_free_string.argtypes = [ctypes.c_char_p]
746_lib.weaveffi_free_string.restype = None
747_lib.weaveffi_free_bytes.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
748_lib.weaveffi_free_bytes.restype = None
749
750
751def _check_error(err: _WeaveFFIErrorStruct) -> None:
752    if err.code != 0:
753        code = err.code
754        message = err.message.decode("utf-8") if err.message else ""
755        _lib.weaveffi_error_clear(ctypes.byref(err))
756        raise WeaveFFIError(code, message)
757
758
759class _PointerGuard(contextlib.AbstractContextManager):
760    def __init__(self, ptr, free_fn) -> None:
761        self.ptr = ptr
762        self._free_fn = free_fn
763
764    def __exit__(self, *exc) -> bool:
765        if self.ptr is not None:
766            self._free_fn(self.ptr)
767            self.ptr = None
768        return False
769
770
771def _string_to_bytes(s: Optional[str]) -> Optional[bytes]:
772    if s is None:
773        return None
774    return s.encode("utf-8")
775
776
777def _bytes_to_string(ptr) -> Optional[str]:
778    if ptr is None:
779        return None
780    return ptr.decode("utf-8")
781"#,
782    );
783}
784
785fn render_enum(out: &mut String, e: &EnumBinding) {
786    out.push_str(&format!("\n\nclass {}(IntEnum):\n", e.name));
787    emit_docstring(out, &e.doc, "    ");
788    for v in &e.variants {
789        if let Some(d) = &v.doc {
790            let trimmed = d.trim();
791            if !trimmed.is_empty() {
792                for line in trimmed.lines() {
793                    out.push_str(&format!("    # {}\n", line));
794                }
795            }
796        }
797        out.push_str(&format!("    {} = {}\n", v.name, v.value));
798    }
799}
800
801fn render_struct(out: &mut String, s: &StructBinding) {
802    let destroy = &s.destroy_symbol;
803
804    out.push_str(&format!("\n\nclass {}:\n", s.name));
805    emit_docstring(out, &s.doc, "    ");
806
807    out.push_str("\n    def __init__(self, _ptr: int) -> None:");
808    out.push_str("\n        self._ptr = _ptr");
809
810    out.push_str("\n\n    def __del__(self) -> None:");
811    out.push_str("\n        if self._ptr is not None:");
812    out.push_str(&format!(
813        "\n            _lib.{destroy}.argtypes = [ctypes.c_void_p]"
814    ));
815    out.push_str(&format!("\n            _lib.{destroy}.restype = None"));
816    out.push_str(&format!("\n            _lib.{destroy}(self._ptr)"));
817    out.push_str("\n            self._ptr = None");
818
819    for field in &s.fields {
820        render_getter(out, field);
821    }
822    out.push('\n');
823}
824
825fn render_builder(out: &mut String, s: &StructBinding) {
826    let builder_name = format!("{}Builder", s.name);
827    out.push_str(&format!("\n\nclass {}:\n", builder_name));
828    emit_docstring(out, &s.doc, "    ");
829    out.push_str("    def __init__(self) -> None:");
830    // Zero-value defaults (the same contract as the other backends): scalars
831    // start at 0/False/""/b"", collections empty, optionals absent. Unset
832    // fields therefore lower to valid C arguments instead of raising.
833    for field in &s.fields {
834        let (default, hint) = py_field_default(&field.ty);
835        out.push_str(&format!(
836            "\n        self._{}: {} = {}",
837            field.name, hint, default
838        ));
839    }
840    for field in &s.fields {
841        let py_ty = py_type_hint(&field.ty);
842        out.push_str(&format!(
843            "\n\n    def with_{}(self, value: {}) -> \"{}\":",
844            field.name, py_ty, builder_name
845        ));
846        if let Some(d) = &field.doc {
847            let trimmed = d.trim();
848            if !trimmed.is_empty() {
849                if trimmed.contains('\n') {
850                    out.push_str("\n        \"\"\"\n");
851                    for line in trimmed.lines() {
852                        if line.is_empty() {
853                            out.push('\n');
854                        } else {
855                            out.push_str("        ");
856                            out.push_str(line);
857                            out.push('\n');
858                        }
859                    }
860                    out.push_str("        \"\"\"");
861                } else {
862                    out.push_str(&format!("\n        \"\"\"{}\"\"\"", trimmed));
863                }
864            }
865        }
866        out.push_str(&format!("\n        self._{} = value", field.name));
867        out.push_str("\n        return self");
868    }
869    let ret_ty = py_type_hint(&TypeRef::Struct(s.name.clone()));
870    out.push_str(&format!("\n\n    def build(self) -> {}:", ret_ty));
871    // Marshal every field into the struct's C `create` call with the same
872    // lowering used for function parameters, then wrap the returned handle.
873    let ind = "        ";
874    for field in &s.fields {
875        out.push_str(&format!("\n{ind}{} = self._{}", field.name, field.name));
876    }
877    out.push_str(&format!("\n{ind}_fn = _lib.{}", s.create.symbol));
878    let mut argtypes: Vec<String> = Vec::new();
879    for field in &s.fields {
880        argtypes.extend(py_param_argtypes(&field.ty));
881    }
882    argtypes.push("ctypes.POINTER(_WeaveFFIErrorStruct)".into());
883    out.push_str(&format!("\n{ind}_fn.argtypes = [{}]", argtypes.join(", ")));
884    out.push_str(&format!("\n{ind}_fn.restype = ctypes.c_void_p"));
885    for field in &s.fields {
886        for line in py_param_conversion(&field.name, &field.ty, ind) {
887            out.push('\n');
888            out.push_str(&line);
889        }
890    }
891    out.push_str(&format!("\n{ind}_err = _WeaveFFIErrorStruct()"));
892    let mut call_args: Vec<String> = Vec::new();
893    for field in &s.fields {
894        call_args.extend(py_param_call_args(&field.name, &field.ty));
895    }
896    call_args.push("ctypes.byref(_err)".into());
897    out.push_str(&format!("\n{ind}_result = _fn({})", call_args.join(", ")));
898    out.push_str(&format!("\n{ind}_check_error(_err)"));
899    out.push_str(&format!("\n{ind}if _result is None:"));
900    out.push_str(&format!(
901        "\n{ind}    raise WeaveFFIError(-1, \"null pointer\")"
902    ));
903    out.push_str(&format!("\n{ind}return {}(_result)", s.name));
904    out.push('\n');
905}
906
907/// The zero-value default (and matching type hint) for one builder slot.
908fn py_field_default(ty: &TypeRef) -> (String, String) {
909    let hint = py_type_hint(ty);
910    match ty {
911        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::Handle => ("0".into(), hint),
912        TypeRef::F64 => ("0.0".into(), hint),
913        TypeRef::Bool => ("False".into(), hint),
914        TypeRef::StringUtf8 | TypeRef::BorrowedStr => ("\"\"".into(), hint),
915        TypeRef::Bytes | TypeRef::BorrowedBytes => ("b\"\"".into(), hint),
916        TypeRef::List(_) => ("[]".into(), hint),
917        TypeRef::Map(_, _) => ("{}".into(), hint),
918        TypeRef::Optional(_) => ("None".into(), hint),
919        // No synthesizable zero value; the with_ setter is the only path.
920        _ => ("None".into(), format!("Optional[{hint}]")),
921    }
922}
923
924// ── Callbacks & listeners ──
925
926/// The module-level `ctypes.CFUNCTYPE` alias for one callback type. Listener
927/// registration binds against this; the C side sees the matching
928/// `typedef void (*{c_fn_type})(…, void* context)`.
929fn render_callback_type(out: &mut String, c: &CallbackBinding) {
930    let mut parts: Vec<String> = vec!["None".into()];
931    parts.extend(c.abi_params.iter().map(|p| py_ctype(&p.ty)));
932    out.push_str("\n\n");
933    emit_doc(out, &c.doc, "");
934    out.push_str(&format!(
935        "# Callback type {}: {}\n",
936        c.name,
937        py_callable_hint(&c.params)
938    ));
939    out.push_str(&format!(
940        "_CFUNC_{} = ctypes.CFUNCTYPE({})\n",
941        c.c_fn_type,
942        parts.join(", ")
943    ));
944}
945
946/// `Callable[[<param hints>], None]` for a callback's idiomatic signature.
947fn py_callable_hint(params: &[ParamBinding]) -> String {
948    let hints: Vec<String> = params.iter().map(|p| py_type_hint(&p.ty)).collect();
949    format!("Callable[[{}], None]", hints.join(", "))
950}
951
952/// The Python expression converting one trampoline parameter's C slots into
953/// the idiomatic value passed to the user callback. `n` is the IR parameter
954/// name (slot names derive from it, mirroring [`abi::lower_param`]).
955fn py_cb_param_expr(n: &str, ty: &TypeRef) -> String {
956    match ty {
957        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => n.into(),
958        TypeRef::Bool => format!("bool({n})"),
959        TypeRef::StringUtf8 | TypeRef::BorrowedStr => format!("_bytes_to_string({n})"),
960        TypeRef::Bytes | TypeRef::BorrowedBytes => {
961            format!("bytes({n}_ptr[:{n}_len]) if {n}_ptr else b\"\"")
962        }
963        TypeRef::Enum(name) => format!("{}({n})", local_type_name(name)),
964        // Borrowed by contract: the producer owns callback arguments for the
965        // duration of the call, so opaque pointers pass through raw rather
966        // than being wrapped in an owning class whose __del__ would free them.
967        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => n.into(),
968        TypeRef::Optional(inner) => match inner.as_ref() {
969            TypeRef::StringUtf8 | TypeRef::BorrowedStr => format!("_bytes_to_string({n})"),
970            TypeRef::Bytes | TypeRef::BorrowedBytes => {
971                format!("bytes({n}_ptr[:{n}_len]) if {n}_ptr else None")
972            }
973            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => n.into(),
974            TypeRef::List(elem) => {
975                let read = py_read_element(&format!("{n}[_i]"), elem);
976                format!("[{read} for _i in range({n}_len)] if {n} else None")
977            }
978            TypeRef::Map(k, v) => {
979                let kread = py_read_element(&format!("{n}_keys[_i]"), k);
980                let vread = py_read_element(&format!("{n}_values[_i]"), v);
981                format!("{{{kread}: {vread} for _i in range({n}_len)}} if {n}_keys else None")
982            }
983            TypeRef::Bool => format!("bool({n}[0]) if {n} else None"),
984            TypeRef::Enum(name) => format!("{}({n}[0]) if {n} else None", local_type_name(name)),
985            _ => format!("{n}[0] if {n} else None"),
986        },
987        TypeRef::List(inner) => {
988            let read = py_read_element(&format!("{n}[_i]"), inner);
989            format!("[{read} for _i in range({n}_len)] if {n} else []")
990        }
991        TypeRef::Map(k, v) => {
992            let kread = py_read_element(&format!("{n}_keys[_i]"), k);
993            let vread = py_read_element(&format!("{n}_values[_i]"), v);
994            format!("{{{kread}: {vread} for _i in range({n}_len)}} if {n}_keys else {{}}")
995        }
996        TypeRef::Iterator(_) => unreachable!("iterator not valid as callback parameter"),
997    }
998}
999
1000/// Register/unregister wrapper pair for one listener. The trampoline converts
1001/// each C slot to its idiomatic value, and the `ctypes` function object is
1002/// pinned in `_listener_refs` until `unregister` so the producer never calls
1003/// a collected trampoline.
1004fn render_listener(
1005    out: &mut String,
1006    module: &ModuleBinding,
1007    l: &ListenerBinding,
1008    strip_module_prefix: bool,
1009) {
1010    let Some(cb) = module.callbacks.iter().find(|c| c.name == l.event_callback) else {
1011        // Validation guarantees the referenced callback exists in-module.
1012        unreachable!("listener '{}' references unknown callback", l.name);
1013    };
1014    let register_name = wrapper_name(
1015        &module.path,
1016        &format!("register_{}", l.name),
1017        strip_module_prefix,
1018    );
1019    let unregister_name = wrapper_name(
1020        &module.path,
1021        &format!("unregister_{}", l.name),
1022        strip_module_prefix,
1023    );
1024    let cfunc = format!("_CFUNC_{}", cb.c_fn_type);
1025    let ind = "    ";
1026
1027    // register_{listener}(callback) -> int
1028    out.push_str(&format!(
1029        "\n\ndef {register_name}(callback: {}) -> int:\n",
1030        py_callable_hint(&cb.params)
1031    ));
1032    let reg_doc = match &l.doc {
1033        Some(d) => format!(
1034            "{}\n\nReturns a subscription id for {unregister_name}().",
1035            d.trim()
1036        ),
1037        None => format!(
1038            "Register a {} listener. Returns a subscription id for {unregister_name}().",
1039            cb.name
1040        ),
1041    };
1042    emit_docstring(out, &Some(reg_doc), ind);
1043
1044    let tramp_params: Vec<String> = cb
1045        .params
1046        .iter()
1047        .flat_map(|p| p.abi.iter().map(|slot| slot.name.clone()))
1048        .chain(std::iter::once("_context".to_string()))
1049        .collect();
1050    let call_args: Vec<String> = cb
1051        .params
1052        .iter()
1053        .map(|p| py_cb_param_expr(&p.name, &p.ty))
1054        .collect();
1055    out.push_str(&format!(
1056        "{ind}def _trampoline({}):\n",
1057        tramp_params.join(", ")
1058    ));
1059    out.push_str(&format!("{ind}    callback({})\n", call_args.join(", ")));
1060    out.push_str(&format!("{ind}_cfunc = {cfunc}(_trampoline)\n"));
1061    out.push_str(&format!("{ind}_fn = _lib.{}\n", l.register_symbol));
1062    out.push_str(&format!("{ind}_fn.argtypes = [{cfunc}, ctypes.c_void_p]\n"));
1063    out.push_str(&format!("{ind}_fn.restype = ctypes.c_uint64\n"));
1064    out.push_str(&format!("{ind}_listener_id = int(_fn(_cfunc, None))\n"));
1065    out.push_str(&format!("{ind}_listener_refs[_listener_id] = _cfunc\n"));
1066    out.push_str(&format!("{ind}return _listener_id\n"));
1067
1068    // unregister_{listener}(listener_id) -> None
1069    out.push_str(&format!(
1070        "\n\ndef {unregister_name}(listener_id: int) -> None:\n"
1071    ));
1072    emit_docstring(
1073        out,
1074        &Some(format!(
1075            "Unregister a listener previously registered with {register_name}()."
1076        )),
1077        ind,
1078    );
1079    out.push_str(&format!("{ind}_fn = _lib.{}\n", l.unregister_symbol));
1080    out.push_str(&format!("{ind}_fn.argtypes = [ctypes.c_uint64]\n"));
1081    out.push_str(&format!("{ind}_fn.restype = None\n"));
1082    out.push_str(&format!("{ind}_fn(ctypes.c_uint64(listener_id))\n"));
1083    out.push_str(&format!("{ind}_listener_refs.pop(listener_id, None)\n"));
1084}
1085
1086fn render_getter(out: &mut String, field: &FieldBinding) {
1087    let getter = &field.getter_symbol;
1088    let py_ty = py_type_hint(&field.ty);
1089    let ind = "        ";
1090
1091    out.push_str(&format!(
1092        "\n\n    @property\n    def {}(self) -> {}:\n",
1093        field.name, py_ty
1094    ));
1095    emit_docstring(out, &field.doc, ind);
1096    out.push_str(&format!("{ind}_fn = _lib.{getter}\n"));
1097
1098    let (restype, out_argtypes) = py_return_info(&field.ty);
1099    let mut argtypes = vec!["ctypes.c_void_p".to_string()];
1100    argtypes.extend(out_argtypes.iter().cloned());
1101
1102    out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", ")));
1103    out.push_str(&format!("{ind}_fn.restype = {restype}\n"));
1104
1105    if out_argtypes.is_empty() {
1106        out.push_str(&format!("{ind}_result = _fn(self._ptr)\n"));
1107    } else if let Some((k, v)) = get_map_kv(&field.ty) {
1108        out.push_str(&format!(
1109            "{ind}_out_keys = ctypes.POINTER({})()\n",
1110            py_ctypes_scalar(k)
1111        ));
1112        out.push_str(&format!(
1113            "{ind}_out_values = ctypes.POINTER({})()\n",
1114            py_ctypes_scalar(v)
1115        ));
1116        out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
1117        out.push_str(&format!("{ind}_fn(self._ptr, ctypes.byref(_out_keys), ctypes.byref(_out_values), ctypes.byref(_out_len))\n"));
1118    } else {
1119        out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
1120        out.push_str(&format!(
1121            "{ind}_result = _fn(self._ptr, ctypes.byref(_out_len))\n"
1122        ));
1123    }
1124
1125    render_return_value(out, &field.ty, ind);
1126}
1127
1128fn render_function(out: &mut String, module_name: &str, f: &FnBinding, strip_module_prefix: bool) {
1129    let func_name = wrapper_name(module_name, &f.name, strip_module_prefix);
1130    let params_sig: Vec<String> = f
1131        .params
1132        .iter()
1133        .map(|p| format!("{}: {}", p.name, py_type_hint(&p.ty)))
1134        .collect();
1135    let ret_hint = f
1136        .ret
1137        .as_ref()
1138        .map(py_type_hint)
1139        .unwrap_or_else(|| "None".to_string());
1140
1141    let def_name = if f.is_async {
1142        format!("_{func_name}_sync")
1143    } else {
1144        func_name.clone()
1145    };
1146
1147    if let (Some(TypeRef::Iterator(inner)), CallShape::Iterator(it)) = (&f.ret, &f.shape) {
1148        render_iterator_class(out, &it.iter_tag, &f.name, inner);
1149    }
1150
1151    out.push_str(&format!(
1152        "\n\ndef {}({}) -> {}:\n",
1153        def_name,
1154        params_sig.join(", "),
1155        ret_hint
1156    ));
1157
1158    let ind = "    ";
1159
1160    emit_fn_docstring(out, &f.doc, &f.params, ind);
1161
1162    if let Some(msg) = &f.deprecated {
1163        out.push_str(&format!(
1164            "{ind}import warnings\n{ind}warnings.warn(\"{}\", DeprecationWarning, stacklevel=2)\n",
1165            msg.replace('"', "\\\"")
1166        ));
1167    }
1168
1169    if f.is_async {
1170        render_async_ffi_call_body(out, f);
1171    } else {
1172        out.push_str(&format!("{ind}_fn = _lib.{}\n", f.c_base));
1173
1174        let mut argtypes: Vec<String> = Vec::new();
1175        for p in &f.params {
1176            argtypes.extend(py_param_argtypes(&p.ty));
1177        }
1178        let mut out_ret_argtypes = Vec::new();
1179        let restype;
1180        if let Some(ret_ty) = &f.ret {
1181            let (rt, oat) = py_return_info(ret_ty);
1182            argtypes.extend(oat.iter().cloned());
1183            restype = rt;
1184            out_ret_argtypes = oat;
1185        } else {
1186            restype = "None".to_string();
1187        }
1188        argtypes.push("ctypes.POINTER(_WeaveFFIErrorStruct)".into());
1189
1190        out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", ")));
1191        out.push_str(&format!("{ind}_fn.restype = {restype}\n"));
1192
1193        for p in &f.params {
1194            for line in py_param_conversion(&p.name, &p.ty, ind) {
1195                out.push_str(&line);
1196                out.push('\n');
1197            }
1198        }
1199
1200        out.push_str(&format!("{ind}_err = _WeaveFFIErrorStruct()\n"));
1201
1202        let is_map_ret = f.ret.as_ref().and_then(get_map_kv).is_some();
1203        let has_out_len = !out_ret_argtypes.is_empty() && !is_map_ret;
1204
1205        if let Some((k, v)) = f.ret.as_ref().and_then(get_map_kv) {
1206            out.push_str(&format!(
1207                "{ind}_out_keys = ctypes.POINTER({})()\n",
1208                py_ctypes_scalar(k)
1209            ));
1210            out.push_str(&format!(
1211                "{ind}_out_values = ctypes.POINTER({})()\n",
1212                py_ctypes_scalar(v)
1213            ));
1214            out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
1215        } else if has_out_len {
1216            out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
1217        }
1218
1219        let mut call_args: Vec<String> = Vec::new();
1220        for p in &f.params {
1221            call_args.extend(py_param_call_args(&p.name, &p.ty));
1222        }
1223        if is_map_ret {
1224            call_args.push("ctypes.byref(_out_keys)".into());
1225            call_args.push("ctypes.byref(_out_values)".into());
1226            call_args.push("ctypes.byref(_out_len)".into());
1227        } else if has_out_len {
1228            call_args.push("ctypes.byref(_out_len)".into());
1229        }
1230        call_args.push("ctypes.byref(_err)".into());
1231
1232        let call_expr = format!("_fn({})", call_args.join(", "));
1233        if f.ret.is_some() && !is_map_ret {
1234            out.push_str(&format!("{ind}_result = {call_expr}\n"));
1235        } else {
1236            out.push_str(&format!("{ind}{call_expr}\n"));
1237        }
1238
1239        out.push_str(&format!("{ind}_check_error(_err)\n"));
1240
1241        if let Some(ret_ty) = &f.ret {
1242            if let (TypeRef::Iterator(inner), CallShape::Iterator(it)) = (ret_ty, &f.shape) {
1243                render_iterator_return(out, &it.iter_tag, inner, ind);
1244            } else {
1245                render_return_value(out, ret_ty, ind);
1246            }
1247        }
1248    }
1249
1250    if f.is_async {
1251        let params_joined = params_sig.join(", ");
1252        out.push_str(&format!(
1253            "\n\nasync def {}({}) -> {}:\n",
1254            func_name, params_joined, ret_hint
1255        ));
1256        emit_fn_docstring(out, &f.doc, &f.params, ind);
1257        out.push_str("    _loop = asyncio.get_event_loop()\n");
1258        let arg_names: Vec<&str> = f.params.iter().map(|p| p.name.as_str()).collect();
1259        let executor_args = if arg_names.is_empty() {
1260            def_name
1261        } else {
1262            format!("{def_name}, {}", arg_names.join(", "))
1263        };
1264        if f.ret.is_some() {
1265            out.push_str(&format!(
1266                "    return await _loop.run_in_executor(None, {executor_args})\n"
1267            ));
1268        } else {
1269            out.push_str(&format!(
1270                "    await _loop.run_in_executor(None, {executor_args})\n"
1271            ));
1272        }
1273    }
1274}
1275
1276// ── Param helpers ──
1277
1278fn py_list_convert_expr(name: &str, elem: &TypeRef) -> String {
1279    match elem {
1280        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1281            format!("*[_string_to_bytes(v) for v in {name}]")
1282        }
1283        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => format!("*[v._ptr for v in {name}]"),
1284        TypeRef::Enum(_) => format!("*[v.value for v in {name}]"),
1285        TypeRef::Bool => format!("*[1 if v else 0 for v in {name}]"),
1286        _ => format!("*{name}"),
1287    }
1288}
1289
1290fn py_map_elem_convert(list_name: &str, ty: &TypeRef, var: &str) -> String {
1291    match ty {
1292        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1293            format!("*[_string_to_bytes({var}) for {var} in {list_name}]")
1294        }
1295        TypeRef::Enum(_) => format!("*[{var}.value for {var} in {list_name}]"),
1296        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
1297            format!("*[{var}._ptr for {var} in {list_name}]")
1298        }
1299        TypeRef::Bool => format!("*[1 if {var} else 0 for {var} in {list_name}]"),
1300        _ => format!("*{list_name}"),
1301    }
1302}
1303
1304fn py_param_conversion(name: &str, ty: &TypeRef, ind: &str) -> Vec<String> {
1305    match ty {
1306        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1307            let s = py_ctypes_scalar(&TypeRef::Bytes);
1308            vec![format!("{ind}_{name}_arr = ({s} * len({name}))(*{name})")]
1309        }
1310        TypeRef::Optional(inner) => match inner.as_ref() {
1311            TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
1312                let s = py_ctypes_scalar(inner);
1313                vec![format!(
1314                    "{ind}_{name}_c = ctypes.byref({s}({name})) if {name} is not None else None"
1315                )]
1316            }
1317            TypeRef::Bool => {
1318                vec![format!(
1319                    "{ind}_{name}_c = ctypes.byref(ctypes.c_int32(1 if {name} else 0)) if {name} is not None else None"
1320                )]
1321            }
1322            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1323                vec![format!("{ind}_{name}_c = _string_to_bytes({name})")]
1324            }
1325            TypeRef::Enum(_) => {
1326                vec![format!(
1327                    "{ind}_{name}_c = ctypes.byref(ctypes.c_int32({name}.value)) if {name} is not None else None"
1328                )]
1329            }
1330            TypeRef::Bytes | TypeRef::BorrowedBytes => {
1331                let s = py_ctypes_scalar(&TypeRef::Bytes);
1332                vec![
1333                    format!("{ind}if {name} is not None:"),
1334                    format!("{ind}    _{name}_arr = ({s} * len({name}))(*{name})"),
1335                    format!("{ind}    _{name}_len = len({name})"),
1336                    format!("{ind}else:"),
1337                    format!("{ind}    _{name}_arr = None"),
1338                    format!("{ind}    _{name}_len = 0"),
1339                ]
1340            }
1341            TypeRef::List(elem) => {
1342                let s = py_ctypes_scalar(elem);
1343                let convert = py_list_convert_expr(name, elem);
1344                vec![
1345                    format!("{ind}if {name} is not None:"),
1346                    format!("{ind}    _{name}_arr = ({s} * len({name}))({convert})"),
1347                    format!("{ind}    _{name}_len = len({name})"),
1348                    format!("{ind}else:"),
1349                    format!("{ind}    _{name}_arr = None"),
1350                    format!("{ind}    _{name}_len = 0"),
1351                ]
1352            }
1353            _ => vec![],
1354        },
1355        TypeRef::List(inner) => {
1356            let s = py_ctypes_scalar(inner);
1357            let convert = py_list_convert_expr(name, inner);
1358            vec![format!("{ind}_{name}_arr = ({s} * len({name}))({convert})")]
1359        }
1360        TypeRef::Map(k, v) => {
1361            let ks = py_ctypes_scalar(k);
1362            let vs = py_ctypes_scalar(v);
1363            let kconv = py_map_elem_convert(&format!("_{name}_keys"), k, "_k");
1364            let vconv = py_map_elem_convert(&format!("_{name}_vals"), v, "_v");
1365            vec![
1366                format!("{ind}_{name}_keys = list({name}.keys())"),
1367                format!("{ind}_{name}_vals = [{name}[_k] for _k in _{name}_keys]"),
1368                format!("{ind}_{name}_ka = ({ks} * len(_{name}_keys))({kconv})"),
1369                format!("{ind}_{name}_va = ({vs} * len(_{name}_vals))({vconv})"),
1370            ]
1371        }
1372        _ => vec![],
1373    }
1374}
1375
1376fn py_param_call_args(name: &str, ty: &TypeRef) -> Vec<String> {
1377    match ty {
1378        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
1379            vec![name.to_string()]
1380        }
1381        TypeRef::Bool => vec![format!("1 if {name} else 0")],
1382        TypeRef::StringUtf8 | TypeRef::BorrowedStr => vec![format!("_string_to_bytes({name})")],
1383        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1384            vec![format!("_{name}_arr"), format!("len({name})")]
1385        }
1386        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => vec![format!("{name}._ptr")],
1387        TypeRef::Enum(_) => vec![format!("{name}.value")],
1388        TypeRef::Optional(inner) => match inner.as_ref() {
1389            TypeRef::StringUtf8 | TypeRef::BorrowedStr => vec![format!("_{name}_c")],
1390            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
1391                vec![format!("{name}._ptr if {name} is not None else None")]
1392            }
1393            TypeRef::Bytes | TypeRef::BorrowedBytes | TypeRef::List(_) => {
1394                vec![format!("_{name}_arr"), format!("_{name}_len")]
1395            }
1396            TypeRef::Map(_, _) => vec![
1397                format!("_{name}_ka"),
1398                format!("_{name}_va"),
1399                format!("_{name}_len"),
1400            ],
1401            _ if !is_c_pointer_type(inner) => vec![format!("_{name}_c")],
1402            _ => py_param_call_args(name, inner),
1403        },
1404        TypeRef::List(_) => vec![format!("_{name}_arr"), format!("len({name})")],
1405        TypeRef::Map(_, _) => vec![
1406            format!("_{name}_ka"),
1407            format!("_{name}_va"),
1408            format!("len(_{name}_keys)"),
1409        ],
1410        TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"),
1411    }
1412}
1413
1414// ── Return helpers ──
1415
1416fn py_read_element(expr: &str, ty: &TypeRef) -> String {
1417    match ty {
1418        TypeRef::StringUtf8 | TypeRef::BorrowedStr => format!("_bytes_to_string({expr})"),
1419        TypeRef::Struct(name) | TypeRef::TypedHandle(name) | TypeRef::Enum(name) => {
1420            let name = local_type_name(name);
1421            format!("{name}({expr})")
1422        }
1423        TypeRef::Bool => format!("bool({expr})"),
1424        _ => expr.to_string(),
1425    }
1426}
1427
1428fn render_return_value(out: &mut String, ty: &TypeRef, ind: &str) {
1429    match ty {
1430        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
1431            out.push_str(&format!("{ind}return _result\n"));
1432        }
1433        TypeRef::Bool => {
1434            out.push_str(&format!("{ind}return bool(_result)\n"));
1435        }
1436        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1437            out.push_str(&format!("{ind}return _bytes_to_string(_result) or \"\"\n"));
1438        }
1439        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1440            out.push_str(&format!("{ind}if not _result:\n"));
1441            out.push_str(&format!("{ind}    return b\"\"\n"));
1442            out.push_str(&format!("{ind}return bytes(_result[:_out_len.value])\n"));
1443        }
1444        TypeRef::Struct(name) | TypeRef::TypedHandle(name) => {
1445            let name = local_type_name(name);
1446            out.push_str(&format!("{ind}if _result is None:\n"));
1447            out.push_str(&format!(
1448                "{ind}    raise WeaveFFIError(-1, \"null pointer\")\n"
1449            ));
1450            out.push_str(&format!("{ind}return {name}(_result)\n"));
1451        }
1452        TypeRef::Enum(name) => {
1453            let name = local_type_name(name);
1454            out.push_str(&format!("{ind}return {name}(_result)\n"));
1455        }
1456        TypeRef::Optional(inner) => render_optional_return(out, inner, ind),
1457        TypeRef::List(inner) => render_list_return(out, inner, ind),
1458        TypeRef::Map(k, v) => render_map_return(out, k, v, ind),
1459        TypeRef::Iterator(_) => unreachable!("iterator return handled in render_function"),
1460    }
1461}
1462
1463fn render_optional_return(out: &mut String, inner: &TypeRef, ind: &str) {
1464    match inner {
1465        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1466            out.push_str(&format!("{ind}return _bytes_to_string(_result)\n"));
1467        }
1468        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1469            out.push_str(&format!("{ind}if not _result:\n"));
1470            out.push_str(&format!("{ind}    return None\n"));
1471            out.push_str(&format!("{ind}return bytes(_result[:_out_len.value])\n"));
1472        }
1473        TypeRef::Struct(name) | TypeRef::TypedHandle(name) => {
1474            let name = local_type_name(name);
1475            out.push_str(&format!("{ind}if _result is None:\n"));
1476            out.push_str(&format!("{ind}    return None\n"));
1477            out.push_str(&format!("{ind}return {name}(_result)\n"));
1478        }
1479        TypeRef::Enum(name) => {
1480            let name = local_type_name(name);
1481            out.push_str(&format!("{ind}if not _result:\n"));
1482            out.push_str(&format!("{ind}    return None\n"));
1483            out.push_str(&format!("{ind}return {name}(_result[0])\n"));
1484        }
1485        TypeRef::Bool => {
1486            out.push_str(&format!("{ind}if not _result:\n"));
1487            out.push_str(&format!("{ind}    return None\n"));
1488            out.push_str(&format!("{ind}return bool(_result[0])\n"));
1489        }
1490        _ if !is_c_pointer_type(inner) => {
1491            out.push_str(&format!("{ind}if not _result:\n"));
1492            out.push_str(&format!("{ind}    return None\n"));
1493            out.push_str(&format!("{ind}return _result[0]\n"));
1494        }
1495        _ => {
1496            out.push_str(&format!("{ind}return _result\n"));
1497        }
1498    }
1499}
1500
1501fn render_list_return(out: &mut String, inner: &TypeRef, ind: &str) {
1502    out.push_str(&format!("{ind}if not _result:\n"));
1503    out.push_str(&format!("{ind}    return []\n"));
1504    let elem = py_read_element("_result[_i]", inner);
1505    out.push_str(&format!(
1506        "{ind}return [{elem} for _i in range(_out_len.value)]\n"
1507    ));
1508}
1509
1510fn render_map_return(out: &mut String, k: &TypeRef, v: &TypeRef, ind: &str) {
1511    out.push_str(&format!("{ind}if not _out_keys or not _out_values:\n"));
1512    out.push_str(&format!("{ind}    return {{}}\n"));
1513    let key_read = py_read_element("_out_keys[_i]", k);
1514    let val_read = py_read_element("_out_values[_i]", v);
1515    out.push_str(&format!(
1516        "{ind}return {{{key_read}: {val_read} for _i in range(_out_len.value)}}\n"
1517    ));
1518}
1519
1520// ── Iterator helpers ──
1521
1522fn py_read_iter_item(inner: &TypeRef) -> String {
1523    match inner {
1524        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "_bytes_to_string(_out_item.value)".into(),
1525        TypeRef::Struct(name) | TypeRef::TypedHandle(name) | TypeRef::Enum(name) => {
1526            let name = local_type_name(name);
1527            format!("{name}(_out_item.value)")
1528        }
1529        TypeRef::Bool => "bool(_out_item.value)".into(),
1530        _ => "_out_item.value".into(),
1531    }
1532}
1533
1534fn render_iterator_class(out: &mut String, iter_tag: &str, func_name: &str, inner: &TypeRef) {
1535    let pascal = pascal_case(func_name);
1536    let class_name = format!("_{pascal}Iterator");
1537    let item_scalar = py_ctypes_scalar(inner);
1538    let read_expr = py_read_iter_item(inner);
1539
1540    out.push_str(&format!("\n\nclass {class_name}:"));
1541    out.push_str("\n    def __init__(self, ptr):");
1542    out.push_str("\n        self._ptr = ptr");
1543    out.push_str("\n        self._done = False");
1544
1545    out.push_str("\n\n    def __iter__(self):");
1546    out.push_str("\n        return self");
1547
1548    out.push_str("\n\n    def __next__(self):");
1549    out.push_str("\n        if self._done:");
1550    out.push_str("\n            raise StopIteration");
1551    out.push_str(&format!("\n        _next_fn = _lib.{iter_tag}_next"));
1552    out.push_str(&format!(
1553        "\n        _next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER({item_scalar}), ctypes.POINTER(_WeaveFFIErrorStruct)]"
1554    ));
1555    out.push_str("\n        _next_fn.restype = ctypes.c_int32");
1556    out.push_str(&format!("\n        _out_item = {item_scalar}()"));
1557    out.push_str("\n        _err = _WeaveFFIErrorStruct()");
1558    out.push_str(
1559        "\n        _has = _next_fn(self._ptr, ctypes.byref(_out_item), ctypes.byref(_err))",
1560    );
1561    out.push_str("\n        _check_error(_err)");
1562    out.push_str("\n        if not _has:");
1563    out.push_str("\n            self._done = True");
1564    out.push_str("\n            self._destroy()");
1565    out.push_str("\n            raise StopIteration");
1566    out.push_str(&format!("\n        return {read_expr}"));
1567
1568    out.push_str("\n\n    def _destroy(self):");
1569    out.push_str("\n        if self._ptr is not None:");
1570    out.push_str(&format!(
1571        "\n            _destroy_fn = _lib.{iter_tag}_destroy"
1572    ));
1573    out.push_str("\n            _destroy_fn.argtypes = [ctypes.c_void_p]");
1574    out.push_str("\n            _destroy_fn.restype = None");
1575    out.push_str("\n            _destroy_fn(self._ptr)");
1576    out.push_str("\n            self._ptr = None");
1577
1578    out.push_str("\n\n    def __del__(self):");
1579    out.push_str("\n        self._destroy()");
1580    out.push('\n');
1581}
1582
1583fn render_iterator_return(out: &mut String, iter_tag: &str, inner: &TypeRef, ind: &str) {
1584    let item_scalar = py_ctypes_scalar(inner);
1585    let read_expr = py_read_iter_item(inner);
1586
1587    out.push_str(&format!("{ind}_next_fn = _lib.{iter_tag}_next\n"));
1588    out.push_str(&format!(
1589        "{ind}_next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER({item_scalar}), ctypes.POINTER(_WeaveFFIErrorStruct)]\n"
1590    ));
1591    out.push_str(&format!("{ind}_next_fn.restype = ctypes.c_int32\n"));
1592
1593    out.push_str(&format!("{ind}_destroy_fn = _lib.{iter_tag}_destroy\n"));
1594    out.push_str(&format!("{ind}_destroy_fn.argtypes = [ctypes.c_void_p]\n"));
1595    out.push_str(&format!("{ind}_destroy_fn.restype = None\n"));
1596
1597    out.push_str(&format!("{ind}_items = []\n"));
1598    out.push_str(&format!("{ind}while True:\n"));
1599    out.push_str(&format!("{ind}    _out_item = {item_scalar}()\n"));
1600    out.push_str(&format!("{ind}    _item_err = _WeaveFFIErrorStruct()\n"));
1601    out.push_str(&format!(
1602        "{ind}    _has = _next_fn(_result, ctypes.byref(_out_item), ctypes.byref(_item_err))\n"
1603    ));
1604    out.push_str(&format!("{ind}    _check_error(_item_err)\n"));
1605    out.push_str(&format!("{ind}    if not _has:\n"));
1606    out.push_str(&format!("{ind}        break\n"));
1607    out.push_str(&format!("{ind}    _items.append({read_expr})\n"));
1608
1609    out.push_str(&format!("{ind}_destroy_fn(_result)\n"));
1610    out.push_str(&format!("{ind}return _items\n"));
1611}
1612
1613// ── Packaging ──
1614
1615fn render_pyproject_toml(
1616    package: &ResolvedPackage,
1617    import_name: &str,
1618    input_basename: &str,
1619) -> String {
1620    let prelude = render_prelude(CommentStyle::Hash, input_basename);
1621    let trailer = render_trailer(CommentStyle::Hash, "pyproject.toml");
1622    let name = &package.name;
1623    let version = &package.version;
1624    let description = package.description_or_default();
1625    let mut extra = String::new();
1626    if let Some(license) = &package.license {
1627        extra.push_str(&format!("license = {{ text = \"{license}\" }}\n"));
1628    }
1629    if !package.authors.is_empty() {
1630        let authors = package
1631            .authors
1632            .iter()
1633            .map(|a| format!("{{ name = \"{a}\" }}"))
1634            .collect::<Vec<_>>()
1635            .join(", ");
1636        extra.push_str(&format!("authors = [{authors}]\n"));
1637    }
1638    if let Some(homepage) = &package.homepage {
1639        extra.push_str(&format!("[project.urls]\nHomepage = \"{homepage}\"\n"));
1640    } else if let Some(repository) = &package.repository {
1641        extra.push_str(&format!("[project.urls]\nRepository = \"{repository}\"\n"));
1642    }
1643    format!(
1644        r#"{prelude}[build-system]
1645requires = ["setuptools>=61.0"]
1646build-backend = "setuptools.build_meta"
1647
1648[project]
1649name = "{name}"
1650version = "{version}"
1651description = "{description}"
1652requires-python = ">=3.8"
1653{extra}
1654[tool.setuptools]
1655packages = ["{import_name}"]
1656
1657{trailer}"#,
1658    )
1659}
1660
1661fn render_setup_py(package: &ResolvedPackage, import_name: &str, input_basename: &str) -> String {
1662    let prelude = render_prelude(CommentStyle::Hash, input_basename);
1663    let trailer = render_trailer(CommentStyle::Hash, "setup.py");
1664    let name = &package.name;
1665    let version = &package.version;
1666    format!(
1667        r#"{prelude}from setuptools import setup
1668
1669setup(
1670    name="{name}",
1671    version="{version}",
1672    packages=["{import_name}"],
1673)
1674
1675{trailer}"#,
1676    )
1677}
1678
1679fn render_readme(package: &ResolvedPackage, input_basename: &str) -> String {
1680    let prelude = render_prelude(CommentStyle::Xml, input_basename);
1681    let trailer = render_trailer(CommentStyle::Xml, "README.md");
1682    let name = &package.name;
1683    let import_name = package.ident_name();
1684    format!(
1685        r#"{prelude}# {name} (Python)
1686
1687Auto-generated Python bindings using ctypes.
1688
1689## Prerequisites
1690
1691- Python >= 3.8
1692- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path.
1693
1694## Install
1695
1696```bash
1697pip install .
1698```
1699
1700## Development install
1701
1702```bash
1703pip install -e .
1704```
1705
1706## Usage
1707
1708```python
1709from {import_name} import *
1710```
1711
1712{trailer}"#
1713    )
1714}
1715
1716// ── Type stub (.pyi) rendering ──
1717
1718fn render_pyi_module(api: &Api, strip_module_prefix: bool, input_basename: &str) -> String {
1719    // Type stubs contain no C symbols, so the ABI prefix is irrelevant here; the
1720    // model is used purely for its flattened, path-carrying module traversal.
1721    let model = BindingModel::build(api, "weaveffi");
1722    let mut out = render_prelude(CommentStyle::Hash, input_basename);
1723    out.push_str(
1724        "from enum import IntEnum\nfrom typing import Callable, Dict, Iterator, List, Optional\n",
1725    );
1726    for m in &model.modules {
1727        for e in &m.enums {
1728            render_pyi_enum(&mut out, e);
1729        }
1730        for s in &m.structs {
1731            render_pyi_struct(&mut out, s);
1732        }
1733        for l in &m.listeners {
1734            render_pyi_listener(&mut out, m, l, strip_module_prefix);
1735        }
1736        for f in &m.functions {
1737            render_pyi_function(&mut out, &m.path, f, strip_module_prefix);
1738        }
1739    }
1740    out.push('\n');
1741    out.push_str(&render_trailer(CommentStyle::Hash, "weaveffi.pyi"));
1742    out
1743}
1744
1745fn render_pyi_enum(out: &mut String, e: &EnumBinding) {
1746    out.push('\n');
1747    emit_doc(out, &e.doc, "");
1748    out.push_str(&format!("class {}(IntEnum):\n", e.name));
1749    for v in &e.variants {
1750        emit_doc(out, &v.doc, "    ");
1751        out.push_str(&format!("    {}: int\n", v.name));
1752    }
1753}
1754
1755fn render_pyi_struct(out: &mut String, s: &StructBinding) {
1756    out.push('\n');
1757    emit_doc(out, &s.doc, "");
1758    out.push_str(&format!("class {}:\n", s.name));
1759    for field in &s.fields {
1760        let py_ty = py_type_hint(&field.ty);
1761        emit_doc(out, &field.doc, "    ");
1762        out.push_str(&format!(
1763            "    @property\n    def {}(self) -> {}: ...\n",
1764            field.name, py_ty
1765        ));
1766    }
1767}
1768
1769fn render_pyi_listener(
1770    out: &mut String,
1771    module: &ModuleBinding,
1772    l: &ListenerBinding,
1773    strip_module_prefix: bool,
1774) {
1775    let Some(cb) = module.callbacks.iter().find(|c| c.name == l.event_callback) else {
1776        unreachable!("listener '{}' references unknown callback", l.name);
1777    };
1778    let register_name = wrapper_name(
1779        &module.path,
1780        &format!("register_{}", l.name),
1781        strip_module_prefix,
1782    );
1783    let unregister_name = wrapper_name(
1784        &module.path,
1785        &format!("unregister_{}", l.name),
1786        strip_module_prefix,
1787    );
1788    out.push('\n');
1789    emit_doc(out, &l.doc, "");
1790    out.push_str(&format!(
1791        "def {register_name}(callback: {}) -> int: ...\n",
1792        py_callable_hint(&cb.params)
1793    ));
1794    out.push_str(&format!(
1795        "def {unregister_name}(listener_id: int) -> None: ...\n"
1796    ));
1797}
1798
1799fn render_pyi_function(
1800    out: &mut String,
1801    module_name: &str,
1802    f: &FnBinding,
1803    strip_module_prefix: bool,
1804) {
1805    let func_name = wrapper_name(module_name, &f.name, strip_module_prefix);
1806    let params: Vec<String> = f
1807        .params
1808        .iter()
1809        .map(|p| format!("{}: {}", p.name, py_type_hint(&p.ty)))
1810        .collect();
1811    let ret = f
1812        .ret
1813        .as_ref()
1814        .map(py_type_hint)
1815        .unwrap_or_else(|| "None".into());
1816    let async_kw = if f.is_async { "async " } else { "" };
1817    out.push('\n');
1818    emit_doc(out, &f.doc, "");
1819    out.push_str(&format!(
1820        "{async_kw}def {}({}) -> {}: ...\n",
1821        func_name,
1822        params.join(", "),
1823        ret
1824    ));
1825}
1826
1827#[cfg(test)]
1828mod tests {
1829    use super::*;
1830    use camino::Utf8Path;
1831    use weaveffi_core::codegen::Generator;
1832    use weaveffi_ir::ir::{
1833        Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef,
1834    };
1835
1836    fn make_api(modules: Vec<Module>) -> Api {
1837        Api {
1838            version: "0.3.0".into(),
1839            modules,
1840            generators: None,
1841            package: None,
1842        }
1843    }
1844
1845    fn simple_module(functions: Vec<Function>) -> Module {
1846        Module {
1847            name: "math".into(),
1848            functions,
1849            structs: vec![],
1850            enums: vec![],
1851            callbacks: vec![],
1852            listeners: vec![],
1853            errors: None,
1854            modules: vec![],
1855        }
1856    }
1857
1858    #[test]
1859    fn generator_name_is_python() {
1860        assert_eq!(Generator::name(&PythonGenerator), "python");
1861    }
1862
1863    #[test]
1864    fn generate_creates_output_files() {
1865        let api = make_api(vec![simple_module(vec![Function {
1866            name: "add".into(),
1867            params: vec![
1868                Param {
1869                    name: "a".into(),
1870                    ty: TypeRef::I32,
1871                    mutable: false,
1872                    doc: None,
1873                },
1874                Param {
1875                    name: "b".into(),
1876                    ty: TypeRef::I32,
1877                    mutable: false,
1878                    doc: None,
1879                },
1880            ],
1881            returns: Some(TypeRef::I32),
1882            doc: None,
1883            r#async: false,
1884            cancellable: false,
1885            deprecated: None,
1886            since: None,
1887        }])]);
1888
1889        let tmp = std::env::temp_dir().join("weaveffi_test_python_gen_output");
1890        let _ = std::fs::remove_dir_all(&tmp);
1891        std::fs::create_dir_all(&tmp).unwrap();
1892        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
1893
1894        PythonGenerator
1895            .generate(
1896                &api,
1897                out_dir,
1898                &PythonConfig {
1899                    strip_module_prefix: true,
1900                    ..PythonConfig::default()
1901                },
1902            )
1903            .unwrap();
1904
1905        let init = std::fs::read_to_string(tmp.join("python/weaveffi/__init__.py")).unwrap();
1906        assert!(init.contains("from .weaveffi import *"));
1907
1908        let weaveffi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
1909        assert!(weaveffi.contains("WeaveFFI"));
1910        assert!(weaveffi.contains("def add("));
1911
1912        let _ = std::fs::remove_dir_all(&tmp);
1913    }
1914
1915    #[test]
1916    fn output_files_lists_all() {
1917        let api = make_api(vec![]);
1918        let out = Utf8Path::new("/tmp/out");
1919        let files = PythonGenerator.output_files(&api, out, &PythonConfig::default());
1920        assert_eq!(
1921            files,
1922            vec![
1923                format!("{out}/python/README.md"),
1924                format!("{out}/python/pyproject.toml"),
1925                format!("{out}/python/setup.py"),
1926                format!("{out}/python/weaveffi/__init__.py"),
1927                format!("{out}/python/weaveffi/weaveffi.py"),
1928                format!("{out}/python/weaveffi/weaveffi.pyi"),
1929            ]
1930        );
1931    }
1932
1933    #[test]
1934    fn preamble_has_load_library() {
1935        let api = make_api(vec![]);
1936        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
1937        assert!(py.contains("def _load_library()"), "missing _load_library");
1938        assert!(
1939            py.contains("libweaveffi.dylib"),
1940            "missing macOS library name"
1941        );
1942        assert!(py.contains("libweaveffi.so"), "missing Linux library name");
1943        assert!(py.contains("weaveffi.dll"), "missing Windows library name");
1944        assert!(py.contains("ctypes.CDLL(name)"), "missing CDLL call");
1945    }
1946
1947    #[test]
1948    fn preamble_has_error_handling() {
1949        let api = make_api(vec![]);
1950        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
1951        assert!(
1952            py.contains("class WeaveFFIError(Exception):"),
1953            "missing error class"
1954        );
1955        assert!(
1956            py.contains("class _WeaveFFIErrorStruct(ctypes.Structure):"),
1957            "missing error struct"
1958        );
1959        assert!(py.contains("def _check_error("), "missing _check_error");
1960        assert!(
1961            py.contains("weaveffi_error_clear"),
1962            "missing error_clear setup"
1963        );
1964    }
1965
1966    #[test]
1967    fn simple_i32_function() {
1968        let api = make_api(vec![simple_module(vec![Function {
1969            name: "add".into(),
1970            params: vec![
1971                Param {
1972                    name: "a".into(),
1973                    ty: TypeRef::I32,
1974                    mutable: false,
1975                    doc: None,
1976                },
1977                Param {
1978                    name: "b".into(),
1979                    ty: TypeRef::I32,
1980                    mutable: false,
1981                    doc: None,
1982                },
1983            ],
1984            returns: Some(TypeRef::I32),
1985            doc: None,
1986            r#async: false,
1987            cancellable: false,
1988            deprecated: None,
1989            since: None,
1990        }])]);
1991
1992        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
1993        assert!(
1994            py.contains("def add(a: int, b: int) -> int:"),
1995            "missing function signature: {py}"
1996        );
1997        assert!(
1998            py.contains("_lib.weaveffi_math_add"),
1999            "missing C symbol: {py}"
2000        );
2001        assert!(
2002            py.contains("ctypes.c_int32, ctypes.c_int32"),
2003            "missing argtypes: {py}"
2004        );
2005        assert!(
2006            py.contains("_fn.restype = ctypes.c_int32"),
2007            "missing restype: {py}"
2008        );
2009        assert!(
2010            py.contains("_check_error(_err)"),
2011            "missing error check: {py}"
2012        );
2013        assert!(py.contains("return _result"), "missing return: {py}");
2014    }
2015
2016    #[test]
2017    fn string_function_encode_decode() {
2018        let api = make_api(vec![Module {
2019            name: "text".into(),
2020            functions: vec![Function {
2021                name: "echo".into(),
2022                params: vec![Param {
2023                    name: "msg".into(),
2024                    ty: TypeRef::StringUtf8,
2025                    mutable: false,
2026                    doc: None,
2027                }],
2028                returns: Some(TypeRef::StringUtf8),
2029                doc: None,
2030                r#async: false,
2031                cancellable: false,
2032                deprecated: None,
2033                since: None,
2034            }],
2035            structs: vec![],
2036            enums: vec![],
2037            callbacks: vec![],
2038            listeners: vec![],
2039            errors: None,
2040            modules: vec![],
2041        }]);
2042
2043        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2044        assert!(
2045            py.contains("def echo(msg: str) -> str:"),
2046            "missing signature: {py}"
2047        );
2048        assert!(py.contains("ctypes.c_char_p"), "missing c_char_p: {py}");
2049        assert!(
2050            py.contains("_string_to_bytes(msg)"),
2051            "missing _string_to_bytes call: {py}"
2052        );
2053        assert!(
2054            py.contains("_bytes_to_string(_result)"),
2055            "missing _bytes_to_string call: {py}"
2056        );
2057    }
2058
2059    #[test]
2060    fn void_function() {
2061        let api = make_api(vec![simple_module(vec![Function {
2062            name: "reset".into(),
2063            params: vec![],
2064            returns: None,
2065            doc: None,
2066            r#async: false,
2067            cancellable: false,
2068            deprecated: None,
2069            since: None,
2070        }])]);
2071
2072        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2073        assert!(
2074            py.contains("def reset() -> None:"),
2075            "missing void signature: {py}"
2076        );
2077        assert!(
2078            py.contains("_fn.restype = None"),
2079            "missing None restype: {py}"
2080        );
2081        assert!(
2082            !py.contains("_result ="),
2083            "void function should not assign _result: {py}"
2084        );
2085    }
2086
2087    #[test]
2088    fn enum_intenum_class() {
2089        let api = make_api(vec![Module {
2090            name: "paint".into(),
2091            functions: vec![],
2092            structs: vec![],
2093            enums: vec![EnumDef {
2094                name: "Color".into(),
2095                doc: Some("Primary colors".into()),
2096                variants: vec![
2097                    EnumVariant {
2098                        name: "Red".into(),
2099                        value: 0,
2100                        doc: None,
2101                    },
2102                    EnumVariant {
2103                        name: "Green".into(),
2104                        value: 1,
2105                        doc: None,
2106                    },
2107                    EnumVariant {
2108                        name: "Blue".into(),
2109                        value: 2,
2110                        doc: None,
2111                    },
2112                ],
2113            }],
2114            callbacks: vec![],
2115            listeners: vec![],
2116            errors: None,
2117            modules: vec![],
2118        }]);
2119
2120        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2121        assert!(
2122            py.contains("class Color(IntEnum):"),
2123            "missing IntEnum class: {py}"
2124        );
2125        assert!(
2126            py.contains("\"\"\"Primary colors\"\"\""),
2127            "missing doc: {py}"
2128        );
2129        assert!(py.contains("Red = 0"), "missing Red: {py}");
2130        assert!(py.contains("Green = 1"), "missing Green: {py}");
2131        assert!(py.contains("Blue = 2"), "missing Blue: {py}");
2132    }
2133
2134    #[test]
2135    fn enum_param_and_return() {
2136        let api = make_api(vec![Module {
2137            name: "paint".into(),
2138            functions: vec![Function {
2139                name: "mix".into(),
2140                params: vec![Param {
2141                    name: "a".into(),
2142                    ty: TypeRef::Enum("Color".into()),
2143                    mutable: false,
2144                    doc: None,
2145                }],
2146                returns: Some(TypeRef::Enum("Color".into())),
2147                doc: None,
2148                r#async: false,
2149                cancellable: false,
2150                deprecated: None,
2151                since: None,
2152            }],
2153            structs: vec![],
2154            enums: vec![],
2155            callbacks: vec![],
2156            listeners: vec![],
2157            errors: None,
2158            modules: vec![],
2159        }]);
2160
2161        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2162        assert!(py.contains("a: \"Color\""), "missing enum param hint: {py}");
2163        assert!(
2164            py.contains("-> \"Color\":"),
2165            "missing enum return hint: {py}"
2166        );
2167        assert!(py.contains("a.value"), "missing .value conversion: {py}");
2168        assert!(
2169            py.contains("return Color(_result)"),
2170            "missing enum return wrap: {py}"
2171        );
2172    }
2173
2174    #[test]
2175    fn struct_class_with_getters() {
2176        let api = make_api(vec![Module {
2177            name: "contacts".into(),
2178            functions: vec![],
2179            structs: vec![StructDef {
2180                name: "Contact".into(),
2181                doc: None,
2182                fields: vec![
2183                    StructField {
2184                        name: "name".into(),
2185                        ty: TypeRef::StringUtf8,
2186                        doc: None,
2187                        default: None,
2188                    },
2189                    StructField {
2190                        name: "age".into(),
2191                        ty: TypeRef::I32,
2192                        doc: None,
2193                        default: None,
2194                    },
2195                ],
2196                builder: false,
2197            }],
2198            enums: vec![],
2199            callbacks: vec![],
2200            listeners: vec![],
2201            errors: None,
2202            modules: vec![],
2203        }]);
2204
2205        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2206        assert!(py.contains("class Contact:"), "missing class: {py}");
2207        assert!(
2208            py.contains("def __init__(self, _ptr: int)"),
2209            "missing __init__: {py}"
2210        );
2211        assert!(
2212            py.contains("self._ptr = _ptr"),
2213            "missing _ptr assignment: {py}"
2214        );
2215        assert!(py.contains("def __del__(self)"), "missing __del__: {py}");
2216        assert!(
2217            py.contains("weaveffi_contacts_Contact_destroy"),
2218            "missing destroy call: {py}"
2219        );
2220        assert!(
2221            py.contains("def name(self) -> str:"),
2222            "missing name getter: {py}"
2223        );
2224        assert!(
2225            py.contains("weaveffi_contacts_Contact_get_name"),
2226            "missing name getter C call: {py}"
2227        );
2228        assert!(
2229            py.contains("_bytes_to_string(_result)"),
2230            "missing _bytes_to_string in getter: {py}"
2231        );
2232        assert!(
2233            py.contains("def age(self) -> int:"),
2234            "missing age getter: {py}"
2235        );
2236        assert!(
2237            py.contains("weaveffi_contacts_Contact_get_age"),
2238            "missing age getter C call: {py}"
2239        );
2240    }
2241
2242    #[test]
2243    fn python_builder_generated() {
2244        let api = Api {
2245            version: "0.3.0".into(),
2246            modules: vec![Module {
2247                name: "contacts".into(),
2248                functions: vec![],
2249                structs: vec![StructDef {
2250                    name: "Contact".into(),
2251                    doc: None,
2252                    fields: vec![
2253                        StructField {
2254                            name: "name".into(),
2255                            ty: TypeRef::StringUtf8,
2256                            doc: None,
2257                            default: None,
2258                        },
2259                        StructField {
2260                            name: "age".into(),
2261                            ty: TypeRef::I32,
2262                            doc: None,
2263                            default: None,
2264                        },
2265                    ],
2266                    builder: true,
2267                }],
2268                enums: vec![],
2269                callbacks: vec![],
2270                listeners: vec![],
2271                errors: None,
2272                modules: vec![],
2273            }],
2274            generators: None,
2275            package: None,
2276        };
2277        let dir = tempfile::tempdir().unwrap();
2278        let out = Utf8Path::from_path(dir.path()).unwrap();
2279        PythonGenerator
2280            .generate(&api, out, &PythonConfig::default())
2281            .unwrap();
2282        let py = std::fs::read_to_string(out.join("python/weaveffi/weaveffi.py")).unwrap();
2283        assert!(
2284            py.contains("class ContactBuilder"),
2285            "missing builder class: {py}"
2286        );
2287        assert!(py.contains("def with_name("), "missing with_name: {py}");
2288        assert!(py.contains("def with_age("), "missing with_age: {py}");
2289        assert!(py.contains("def build("), "missing build: {py}");
2290        // Build is FFI-backed: it calls the C create symbol, checks the
2291        // error, and wraps the returned handle. Unset fields default to zero
2292        // values rather than raising.
2293        assert!(
2294            py.contains("_fn = _lib.weaveffi_contacts_Contact_create"),
2295            "missing create call: {py}"
2296        );
2297        assert!(
2298            py.contains("return Contact(_result)"),
2299            "missing handle wrap: {py}"
2300        );
2301        assert!(
2302            py.contains("self._name: str = \"\"") && py.contains("self._age: int = 0"),
2303            "missing zero defaults: {py}"
2304        );
2305        assert!(
2306            !py.contains("requires FFI backing"),
2307            "stub must be gone: {py}"
2308        );
2309    }
2310
2311    #[test]
2312    fn struct_return() {
2313        let api = make_api(vec![Module {
2314            name: "contacts".into(),
2315            functions: vec![Function {
2316                name: "get_contact".into(),
2317                params: vec![Param {
2318                    name: "id".into(),
2319                    ty: TypeRef::Handle,
2320                    mutable: false,
2321                    doc: None,
2322                }],
2323                returns: Some(TypeRef::Struct("Contact".into())),
2324                doc: None,
2325                r#async: false,
2326                cancellable: false,
2327                deprecated: None,
2328                since: None,
2329            }],
2330            structs: vec![],
2331            enums: vec![],
2332            callbacks: vec![],
2333            listeners: vec![],
2334            errors: None,
2335            modules: vec![],
2336        }]);
2337
2338        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2339        assert!(
2340            py.contains("-> \"Contact\":"),
2341            "missing struct return hint: {py}"
2342        );
2343        assert!(
2344            py.contains("ctypes.c_void_p"),
2345            "missing void_p for struct: {py}"
2346        );
2347        assert!(
2348            py.contains("return Contact(_result)"),
2349            "missing struct wrapping: {py}"
2350        );
2351    }
2352
2353    #[test]
2354    fn bool_uses_c_int32() {
2355        let api = make_api(vec![simple_module(vec![Function {
2356            name: "is_valid".into(),
2357            params: vec![Param {
2358                name: "flag".into(),
2359                ty: TypeRef::Bool,
2360                mutable: false,
2361                doc: None,
2362            }],
2363            returns: Some(TypeRef::Bool),
2364            doc: None,
2365            r#async: false,
2366            cancellable: false,
2367            deprecated: None,
2368            since: None,
2369        }])]);
2370
2371        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2372        assert!(py.contains("flag: bool"), "missing bool param: {py}");
2373        assert!(py.contains("-> bool:"), "missing bool return: {py}");
2374        assert!(
2375            py.contains("ctypes.c_int32"),
2376            "missing c_int32 for Bool: {py}"
2377        );
2378        assert!(
2379            py.contains("1 if flag else 0"),
2380            "missing bool-to-int conversion: {py}"
2381        );
2382        assert!(
2383            py.contains("return bool(_result)"),
2384            "missing int-to-bool conversion: {py}"
2385        );
2386    }
2387
2388    #[test]
2389    fn handle_uses_c_uint64() {
2390        let api = make_api(vec![simple_module(vec![Function {
2391            name: "create".into(),
2392            params: vec![],
2393            returns: Some(TypeRef::Handle),
2394            doc: None,
2395            r#async: false,
2396            cancellable: false,
2397            deprecated: None,
2398            since: None,
2399        }])]);
2400
2401        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2402        assert!(
2403            py.contains("ctypes.c_uint64"),
2404            "missing c_uint64 for Handle: {py}"
2405        );
2406    }
2407
2408    #[test]
2409    fn bytes_param_and_return() {
2410        let api = make_api(vec![Module {
2411            name: "store".into(),
2412            functions: vec![Function {
2413                name: "process".into(),
2414                params: vec![Param {
2415                    name: "data".into(),
2416                    ty: TypeRef::Bytes,
2417                    mutable: false,
2418                    doc: None,
2419                }],
2420                returns: Some(TypeRef::Bytes),
2421                doc: None,
2422                r#async: false,
2423                cancellable: false,
2424                deprecated: None,
2425                since: None,
2426            }],
2427            structs: vec![],
2428            enums: vec![],
2429            callbacks: vec![],
2430            listeners: vec![],
2431            errors: None,
2432            modules: vec![],
2433        }]);
2434
2435        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2436        assert!(py.contains("data: bytes"), "missing bytes param: {py}");
2437        assert!(py.contains("-> bytes:"), "missing bytes return: {py}");
2438        assert!(
2439            py.contains("ctypes.POINTER(ctypes.c_uint8)"),
2440            "missing uint8 pointer: {py}"
2441        );
2442        assert!(py.contains("ctypes.c_size_t"), "missing size_t: {py}");
2443        assert!(py.contains("_out_len"), "missing out_len: {py}");
2444    }
2445
2446    #[test]
2447    fn optional_value_param_and_return() {
2448        let api = make_api(vec![Module {
2449            name: "store".into(),
2450            functions: vec![Function {
2451                name: "find".into(),
2452                params: vec![Param {
2453                    name: "id".into(),
2454                    ty: TypeRef::Optional(Box::new(TypeRef::I32)),
2455                    mutable: false,
2456                    doc: None,
2457                }],
2458                returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))),
2459                doc: None,
2460                r#async: false,
2461                cancellable: false,
2462                deprecated: None,
2463                since: None,
2464            }],
2465            structs: vec![],
2466            enums: vec![],
2467            callbacks: vec![],
2468            listeners: vec![],
2469            errors: None,
2470            modules: vec![],
2471        }]);
2472
2473        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2474        assert!(
2475            py.contains("id: Optional[int]"),
2476            "missing optional param: {py}"
2477        );
2478        assert!(
2479            py.contains("-> Optional[int]:"),
2480            "missing optional return: {py}"
2481        );
2482        assert!(
2483            py.contains("ctypes.POINTER(ctypes.c_int32)"),
2484            "missing POINTER for optional: {py}"
2485        );
2486        assert!(
2487            py.contains("ctypes.byref(ctypes.c_int32(id)) if id is not None else None"),
2488            "missing optional param conversion: {py}"
2489        );
2490        assert!(py.contains("return None"), "missing None return path: {py}");
2491        assert!(
2492            py.contains("return _result[0]"),
2493            "missing pointer deref: {py}"
2494        );
2495    }
2496
2497    #[test]
2498    fn optional_string_return() {
2499        let api = make_api(vec![Module {
2500            name: "store".into(),
2501            functions: vec![Function {
2502                name: "get_name".into(),
2503                params: vec![],
2504                returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))),
2505                doc: None,
2506                r#async: false,
2507                cancellable: false,
2508                deprecated: None,
2509                since: None,
2510            }],
2511            structs: vec![],
2512            enums: vec![],
2513            callbacks: vec![],
2514            listeners: vec![],
2515            errors: None,
2516            modules: vec![],
2517        }]);
2518
2519        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2520        assert!(
2521            py.contains("-> Optional[str]:"),
2522            "missing optional str return: {py}"
2523        );
2524        assert!(
2525            py.contains("return _bytes_to_string(_result)"),
2526            "missing _bytes_to_string for optional string: {py}"
2527        );
2528    }
2529
2530    #[test]
2531    fn list_param_and_return() {
2532        let api = make_api(vec![Module {
2533            name: "batch".into(),
2534            functions: vec![
2535                Function {
2536                    name: "process".into(),
2537                    params: vec![Param {
2538                        name: "ids".into(),
2539                        ty: TypeRef::List(Box::new(TypeRef::I32)),
2540                        mutable: false,
2541                        doc: None,
2542                    }],
2543                    returns: None,
2544                    doc: None,
2545                    r#async: false,
2546                    cancellable: false,
2547                    deprecated: None,
2548                    since: None,
2549                },
2550                Function {
2551                    name: "get_ids".into(),
2552                    params: vec![],
2553                    returns: Some(TypeRef::List(Box::new(TypeRef::I32))),
2554                    doc: None,
2555                    r#async: false,
2556                    cancellable: false,
2557                    deprecated: None,
2558                    since: None,
2559                },
2560            ],
2561            structs: vec![],
2562            enums: vec![],
2563            callbacks: vec![],
2564            listeners: vec![],
2565            errors: None,
2566            modules: vec![],
2567        }]);
2568
2569        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2570        assert!(py.contains("ids: List[int]"), "missing list param: {py}");
2571        assert!(py.contains("-> List[int]:"), "missing list return: {py}");
2572        assert!(
2573            py.contains("ctypes.c_int32 * len(ids)"),
2574            "missing ctypes array creation: {py}"
2575        );
2576        assert!(
2577            py.contains("_out_len"),
2578            "missing out_len for list return: {py}"
2579        );
2580        assert!(
2581            py.contains("for _i in range(_out_len.value)"),
2582            "missing list iteration: {py}"
2583        );
2584    }
2585
2586    #[test]
2587    fn map_param_and_return() {
2588        let api = make_api(vec![Module {
2589            name: "store".into(),
2590            functions: vec![
2591                Function {
2592                    name: "update".into(),
2593                    params: vec![Param {
2594                        name: "scores".into(),
2595                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
2596                        mutable: false,
2597                        doc: None,
2598                    }],
2599                    returns: None,
2600                    doc: None,
2601                    r#async: false,
2602                    cancellable: false,
2603                    deprecated: None,
2604                    since: None,
2605                },
2606                Function {
2607                    name: "get_scores".into(),
2608                    params: vec![],
2609                    returns: Some(TypeRef::Map(
2610                        Box::new(TypeRef::StringUtf8),
2611                        Box::new(TypeRef::I32),
2612                    )),
2613                    doc: None,
2614                    r#async: false,
2615                    cancellable: false,
2616                    deprecated: None,
2617                    since: None,
2618                },
2619            ],
2620            structs: vec![],
2621            enums: vec![],
2622            callbacks: vec![],
2623            listeners: vec![],
2624            errors: None,
2625            modules: vec![],
2626        }]);
2627
2628        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2629        assert!(
2630            py.contains("scores: Dict[str, int]"),
2631            "missing map param: {py}"
2632        );
2633        assert!(
2634            py.contains("-> Dict[str, int]:"),
2635            "missing map return: {py}"
2636        );
2637        assert!(
2638            py.contains("list(scores.keys())"),
2639            "missing keys extraction: {py}"
2640        );
2641        assert!(py.contains("_out_keys"), "missing out_keys: {py}");
2642        assert!(py.contains("_out_values"), "missing out_values: {py}");
2643    }
2644
2645    #[test]
2646    fn struct_optional_string_getter() {
2647        let api = make_api(vec![Module {
2648            name: "contacts".into(),
2649            functions: vec![],
2650            structs: vec![StructDef {
2651                name: "Contact".into(),
2652                doc: None,
2653                fields: vec![StructField {
2654                    name: "email".into(),
2655                    ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2656                    doc: None,
2657                    default: None,
2658                }],
2659                builder: false,
2660            }],
2661            enums: vec![],
2662            callbacks: vec![],
2663            listeners: vec![],
2664            errors: None,
2665            modules: vec![],
2666        }]);
2667
2668        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2669        assert!(
2670            py.contains("def email(self) -> Optional[str]:"),
2671            "missing optional getter: {py}"
2672        );
2673        assert!(
2674            py.contains("_bytes_to_string(_result)"),
2675            "missing _bytes_to_string in optional getter: {py}"
2676        );
2677    }
2678
2679    #[test]
2680    fn struct_enum_field_getter() {
2681        let api = make_api(vec![Module {
2682            name: "contacts".into(),
2683            functions: vec![],
2684            structs: vec![StructDef {
2685                name: "Contact".into(),
2686                doc: None,
2687                fields: vec![StructField {
2688                    name: "role".into(),
2689                    ty: TypeRef::Enum("Role".into()),
2690                    doc: None,
2691                    default: None,
2692                }],
2693                builder: false,
2694            }],
2695            enums: vec![],
2696            callbacks: vec![],
2697            listeners: vec![],
2698            errors: None,
2699            modules: vec![],
2700        }]);
2701
2702        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2703        assert!(
2704            py.contains("def role(self) -> \"Role\":"),
2705            "missing enum getter: {py}"
2706        );
2707        assert!(
2708            py.contains("return Role(_result)"),
2709            "missing enum wrapping in getter: {py}"
2710        );
2711    }
2712
2713    #[test]
2714    fn comprehensive_contacts_api() {
2715        let api = make_api(vec![Module {
2716            name: "contacts".into(),
2717            enums: vec![EnumDef {
2718                name: "ContactType".into(),
2719                doc: None,
2720                variants: vec![
2721                    EnumVariant {
2722                        name: "Personal".into(),
2723                        value: 0,
2724                        doc: None,
2725                    },
2726                    EnumVariant {
2727                        name: "Work".into(),
2728                        value: 1,
2729                        doc: None,
2730                    },
2731                ],
2732            }],
2733            callbacks: vec![],
2734            listeners: vec![],
2735            structs: vec![StructDef {
2736                name: "Contact".into(),
2737                doc: None,
2738                fields: vec![
2739                    StructField {
2740                        name: "id".into(),
2741                        ty: TypeRef::I64,
2742                        doc: None,
2743                        default: None,
2744                    },
2745                    StructField {
2746                        name: "first_name".into(),
2747                        ty: TypeRef::StringUtf8,
2748                        doc: None,
2749                        default: None,
2750                    },
2751                    StructField {
2752                        name: "email".into(),
2753                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2754                        doc: None,
2755                        default: None,
2756                    },
2757                    StructField {
2758                        name: "contact_type".into(),
2759                        ty: TypeRef::Enum("ContactType".into()),
2760                        doc: None,
2761                        default: None,
2762                    },
2763                ],
2764                builder: false,
2765            }],
2766            functions: vec![
2767                Function {
2768                    name: "create_contact".into(),
2769                    params: vec![
2770                        Param {
2771                            name: "first_name".into(),
2772                            ty: TypeRef::StringUtf8,
2773                            mutable: false,
2774                            doc: None,
2775                        },
2776                        Param {
2777                            name: "email".into(),
2778                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2779                            mutable: false,
2780                            doc: None,
2781                        },
2782                        Param {
2783                            name: "contact_type".into(),
2784                            ty: TypeRef::Enum("ContactType".into()),
2785                            mutable: false,
2786                            doc: None,
2787                        },
2788                    ],
2789                    returns: Some(TypeRef::Handle),
2790                    doc: None,
2791                    r#async: false,
2792                    cancellable: false,
2793                    deprecated: None,
2794                    since: None,
2795                },
2796                Function {
2797                    name: "get_contact".into(),
2798                    params: vec![Param {
2799                        name: "id".into(),
2800                        ty: TypeRef::Handle,
2801                        mutable: false,
2802                        doc: None,
2803                    }],
2804                    returns: Some(TypeRef::Struct("Contact".into())),
2805                    doc: None,
2806                    r#async: false,
2807                    cancellable: false,
2808                    deprecated: None,
2809                    since: None,
2810                },
2811                Function {
2812                    name: "list_contacts".into(),
2813                    params: vec![],
2814                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
2815                    doc: None,
2816                    r#async: false,
2817                    cancellable: false,
2818                    deprecated: None,
2819                    since: None,
2820                },
2821                Function {
2822                    name: "count_contacts".into(),
2823                    params: vec![],
2824                    returns: Some(TypeRef::I32),
2825                    doc: None,
2826                    r#async: false,
2827                    cancellable: false,
2828                    deprecated: None,
2829                    since: None,
2830                },
2831            ],
2832            errors: None,
2833            modules: vec![],
2834        }]);
2835
2836        let tmp = std::env::temp_dir().join("weaveffi_test_python_gen_contacts");
2837        let _ = std::fs::remove_dir_all(&tmp);
2838        std::fs::create_dir_all(&tmp).unwrap();
2839        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2840
2841        PythonGenerator
2842            .generate(
2843                &api,
2844                out_dir,
2845                &PythonConfig {
2846                    strip_module_prefix: true,
2847                    ..PythonConfig::default()
2848                },
2849            )
2850            .unwrap();
2851
2852        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
2853
2854        assert!(py.contains("class ContactType(IntEnum):"));
2855        assert!(py.contains("Personal = 0"));
2856        assert!(py.contains("Work = 1"));
2857
2858        assert!(py.contains("class Contact:"));
2859        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
2860        assert!(py.contains("def id(self) -> int:"));
2861        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
2862        assert!(py.contains("def first_name(self) -> str:"));
2863        assert!(py.contains("def email(self) -> Optional[str]:"));
2864        assert!(py.contains("def contact_type(self) -> \"ContactType\":"));
2865
2866        assert!(py.contains("def create_contact("));
2867        assert!(py.contains("weaveffi_contacts_create_contact"));
2868        assert!(py.contains("def get_contact(id: int) -> \"Contact\":"));
2869        assert!(py.contains("def list_contacts() -> List[\"Contact\"]:"));
2870        assert!(py.contains("def count_contacts() -> int:"));
2871
2872        let _ = std::fs::remove_dir_all(&tmp);
2873    }
2874
2875    #[test]
2876    fn type_hint_mapping() {
2877        assert_eq!(py_type_hint(&TypeRef::I32), "int");
2878        assert_eq!(py_type_hint(&TypeRef::U32), "int");
2879        assert_eq!(py_type_hint(&TypeRef::I64), "int");
2880        assert_eq!(py_type_hint(&TypeRef::F64), "float");
2881        assert_eq!(py_type_hint(&TypeRef::Bool), "bool");
2882        assert_eq!(py_type_hint(&TypeRef::StringUtf8), "str");
2883        assert_eq!(py_type_hint(&TypeRef::Bytes), "bytes");
2884        assert_eq!(py_type_hint(&TypeRef::Handle), "int");
2885        assert_eq!(py_type_hint(&TypeRef::Struct("Foo".into())), "\"Foo\"");
2886        assert_eq!(py_type_hint(&TypeRef::Enum("Bar".into())), "\"Bar\"");
2887        assert_eq!(py_type_hint(&TypeRef::TypedHandle("Foo".into())), "\"Foo\"");
2888        // Cross-module references (resolved to a qualified IR name) must still
2889        // annotate the bare *local* class, which is the only symbol that exists
2890        // in the generated module.
2891        assert_eq!(
2892            py_type_hint(&TypeRef::TypedHandle("kv.Store".into())),
2893            "\"Store\"",
2894            "qualified typed handle must annotate the local class name"
2895        );
2896        assert_eq!(
2897            py_type_hint(&TypeRef::Struct("kv.Store".into())),
2898            "\"Store\""
2899        );
2900        assert_eq!(py_type_hint(&TypeRef::Enum("kv.Kind".into())), "\"Kind\"");
2901        assert_eq!(
2902            py_type_hint(&TypeRef::Optional(Box::new(TypeRef::I32))),
2903            "Optional[int]"
2904        );
2905        assert_eq!(
2906            py_type_hint(&TypeRef::List(Box::new(TypeRef::I32))),
2907            "List[int]"
2908        );
2909        assert_eq!(
2910            py_type_hint(&TypeRef::Map(
2911                Box::new(TypeRef::StringUtf8),
2912                Box::new(TypeRef::I32)
2913            )),
2914            "Dict[str, int]"
2915        );
2916    }
2917
2918    #[test]
2919    fn ctypes_scalar_mapping() {
2920        assert_eq!(py_ctypes_scalar(&TypeRef::I32), "ctypes.c_int32");
2921        assert_eq!(py_ctypes_scalar(&TypeRef::U32), "ctypes.c_uint32");
2922        assert_eq!(py_ctypes_scalar(&TypeRef::I64), "ctypes.c_int64");
2923        assert_eq!(py_ctypes_scalar(&TypeRef::F64), "ctypes.c_double");
2924        assert_eq!(py_ctypes_scalar(&TypeRef::Bool), "ctypes.c_int32");
2925        assert_eq!(py_ctypes_scalar(&TypeRef::StringUtf8), "ctypes.c_char_p");
2926        assert_eq!(py_ctypes_scalar(&TypeRef::Handle), "ctypes.c_uint64");
2927        assert_eq!(py_ctypes_scalar(&TypeRef::Bytes), "ctypes.c_uint8");
2928        assert_eq!(
2929            py_ctypes_scalar(&TypeRef::Struct("X".into())),
2930            "ctypes.c_void_p"
2931        );
2932        assert_eq!(
2933            py_ctypes_scalar(&TypeRef::Enum("X".into())),
2934            "ctypes.c_int32"
2935        );
2936    }
2937
2938    #[test]
2939    fn list_struct_return() {
2940        let api = make_api(vec![Module {
2941            name: "store".into(),
2942            functions: vec![Function {
2943                name: "list_items".into(),
2944                params: vec![],
2945                returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Item".into())))),
2946                doc: None,
2947                r#async: false,
2948                cancellable: false,
2949                deprecated: None,
2950                since: None,
2951            }],
2952            structs: vec![],
2953            enums: vec![],
2954            callbacks: vec![],
2955            listeners: vec![],
2956            errors: None,
2957            modules: vec![],
2958        }]);
2959
2960        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2961        assert!(
2962            py.contains("-> List[\"Item\"]:"),
2963            "missing list struct return: {py}"
2964        );
2965        assert!(
2966            py.contains("Item(_result[_i])"),
2967            "missing struct wrapping in list: {py}"
2968        );
2969    }
2970
2971    #[test]
2972    fn struct_bytes_field_getter() {
2973        let api = make_api(vec![Module {
2974            name: "storage".into(),
2975            functions: vec![],
2976            structs: vec![StructDef {
2977                name: "Blob".into(),
2978                doc: None,
2979                fields: vec![StructField {
2980                    name: "data".into(),
2981                    ty: TypeRef::Bytes,
2982                    doc: None,
2983                    default: None,
2984                }],
2985                builder: false,
2986            }],
2987            enums: vec![],
2988            callbacks: vec![],
2989            listeners: vec![],
2990            errors: None,
2991            modules: vec![],
2992        }]);
2993
2994        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
2995        assert!(
2996            py.contains("def data(self) -> bytes:"),
2997            "missing bytes getter: {py}"
2998        );
2999        assert!(
3000            py.contains("_out_len = ctypes.c_size_t(0)"),
3001            "missing out_len in bytes getter: {py}"
3002        );
3003        assert!(
3004            py.contains("_result[:_out_len.value]"),
3005            "missing bytes slice: {py}"
3006        );
3007    }
3008
3009    #[test]
3010    fn python_generates_type_stubs() {
3011        let api = make_api(vec![Module {
3012            name: "contacts".into(),
3013            enums: vec![EnumDef {
3014                name: "ContactType".into(),
3015                doc: None,
3016                variants: vec![
3017                    EnumVariant {
3018                        name: "Personal".into(),
3019                        value: 0,
3020                        doc: None,
3021                    },
3022                    EnumVariant {
3023                        name: "Work".into(),
3024                        value: 1,
3025                        doc: None,
3026                    },
3027                ],
3028            }],
3029            callbacks: vec![],
3030            listeners: vec![],
3031            structs: vec![StructDef {
3032                name: "Contact".into(),
3033                doc: None,
3034                fields: vec![
3035                    StructField {
3036                        name: "id".into(),
3037                        ty: TypeRef::I64,
3038                        doc: None,
3039                        default: None,
3040                    },
3041                    StructField {
3042                        name: "name".into(),
3043                        ty: TypeRef::StringUtf8,
3044                        doc: None,
3045                        default: None,
3046                    },
3047                    StructField {
3048                        name: "email".into(),
3049                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3050                        doc: None,
3051                        default: None,
3052                    },
3053                    StructField {
3054                        name: "tags".into(),
3055                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
3056                        doc: None,
3057                        default: None,
3058                    },
3059                    StructField {
3060                        name: "metadata".into(),
3061                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
3062                        doc: None,
3063                        default: None,
3064                    },
3065                ],
3066                builder: false,
3067            }],
3068            functions: vec![
3069                Function {
3070                    name: "create_contact".into(),
3071                    params: vec![
3072                        Param {
3073                            name: "name".into(),
3074                            ty: TypeRef::StringUtf8,
3075                            mutable: false,
3076                            doc: None,
3077                        },
3078                        Param {
3079                            name: "email".into(),
3080                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3081                            mutable: false,
3082                            doc: None,
3083                        },
3084                    ],
3085                    returns: Some(TypeRef::Handle),
3086                    doc: None,
3087                    r#async: false,
3088                    cancellable: false,
3089                    deprecated: None,
3090                    since: None,
3091                },
3092                Function {
3093                    name: "get_contact".into(),
3094                    params: vec![Param {
3095                        name: "id".into(),
3096                        ty: TypeRef::Handle,
3097                        mutable: false,
3098                        doc: None,
3099                    }],
3100                    returns: Some(TypeRef::Struct("Contact".into())),
3101                    doc: None,
3102                    r#async: false,
3103                    cancellable: false,
3104                    deprecated: None,
3105                    since: None,
3106                },
3107                Function {
3108                    name: "delete_contact".into(),
3109                    params: vec![Param {
3110                        name: "id".into(),
3111                        ty: TypeRef::Handle,
3112                        mutable: false,
3113                        doc: None,
3114                    }],
3115                    returns: None,
3116                    doc: None,
3117                    r#async: false,
3118                    cancellable: false,
3119                    deprecated: None,
3120                    since: None,
3121                },
3122            ],
3123            errors: None,
3124            modules: vec![],
3125        }]);
3126
3127        let tmp = std::env::temp_dir().join("weaveffi_test_python_pyi");
3128        let _ = std::fs::remove_dir_all(&tmp);
3129        std::fs::create_dir_all(&tmp).unwrap();
3130        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
3131
3132        PythonGenerator
3133            .generate(
3134                &api,
3135                out_dir,
3136                &PythonConfig {
3137                    strip_module_prefix: true,
3138                    ..PythonConfig::default()
3139                },
3140            )
3141            .unwrap();
3142
3143        let pyi_path = tmp.join("python/weaveffi/weaveffi.pyi");
3144        assert!(pyi_path.exists(), ".pyi file must exist");
3145
3146        let pyi = std::fs::read_to_string(&pyi_path).unwrap();
3147
3148        assert!(
3149            pyi.contains("from enum import IntEnum"),
3150            "missing IntEnum import"
3151        );
3152        assert!(
3153            pyi.contains("from typing import Callable, Dict, Iterator, List, Optional"),
3154            "missing typing imports"
3155        );
3156
3157        assert!(
3158            pyi.contains("class ContactType(IntEnum):"),
3159            "missing enum stub"
3160        );
3161        assert!(
3162            pyi.contains("    Personal: int"),
3163            "missing enum variant Personal"
3164        );
3165        assert!(pyi.contains("    Work: int"), "missing enum variant Work");
3166
3167        assert!(pyi.contains("class Contact:"), "missing struct stub");
3168        assert!(
3169            pyi.contains("    def id(self) -> int: ..."),
3170            "missing id property: {pyi}"
3171        );
3172        assert!(
3173            pyi.contains("    def name(self) -> str: ..."),
3174            "missing name property: {pyi}"
3175        );
3176        assert!(
3177            pyi.contains("    def email(self) -> Optional[str]: ..."),
3178            "missing email property: {pyi}"
3179        );
3180        assert!(
3181            pyi.contains("    def tags(self) -> List[str]: ..."),
3182            "missing tags property: {pyi}"
3183        );
3184        assert!(
3185            pyi.contains("    def metadata(self) -> Dict[str, int]: ..."),
3186            "missing metadata property: {pyi}"
3187        );
3188
3189        assert!(
3190            pyi.contains("def create_contact(name: str, email: Optional[str]) -> int: ..."),
3191            "missing create_contact stub: {pyi}"
3192        );
3193        assert!(
3194            pyi.contains("def get_contact(id: int) -> \"Contact\": ..."),
3195            "missing get_contact stub: {pyi}"
3196        );
3197        assert!(
3198            pyi.contains("def delete_contact(id: int) -> None: ..."),
3199            "missing delete_contact stub: {pyi}"
3200        );
3201
3202        let _ = std::fs::remove_dir_all(&tmp);
3203    }
3204
3205    #[test]
3206    fn generate_python_basic() {
3207        let api = make_api(vec![simple_module(vec![Function {
3208            name: "add".into(),
3209            params: vec![
3210                Param {
3211                    name: "a".into(),
3212                    ty: TypeRef::I32,
3213                    mutable: false,
3214                    doc: None,
3215                },
3216                Param {
3217                    name: "b".into(),
3218                    ty: TypeRef::I32,
3219                    mutable: false,
3220                    doc: None,
3221                },
3222            ],
3223            returns: Some(TypeRef::I32),
3224            doc: None,
3225            r#async: false,
3226            cancellable: false,
3227            deprecated: None,
3228            since: None,
3229        }])]);
3230
3231        let tmp = std::env::temp_dir().join("weaveffi_test_py_basic");
3232        let _ = std::fs::remove_dir_all(&tmp);
3233        std::fs::create_dir_all(&tmp).unwrap();
3234        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
3235
3236        PythonGenerator
3237            .generate(
3238                &api,
3239                out_dir,
3240                &PythonConfig {
3241                    strip_module_prefix: true,
3242                    ..PythonConfig::default()
3243                },
3244            )
3245            .unwrap();
3246
3247        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
3248
3249        assert!(py.contains("def add(a: int, b: int) -> int:"));
3250        assert!(py.contains("_fn = _lib.weaveffi_math_add"));
3251        assert!(py.contains("ctypes.c_int32, ctypes.c_int32"));
3252        assert!(py.contains("_fn.restype = ctypes.c_int32"));
3253        assert!(py.contains("_err = _WeaveFFIErrorStruct()"));
3254        assert!(py.contains("_check_error(_err)"));
3255        assert!(py.contains("return _result"));
3256
3257        assert!(py.contains("import ctypes"));
3258        assert!(py.contains("from enum import IntEnum"));
3259        assert!(py.contains("from typing import Callable, Dict, Iterator, List, Optional"));
3260        assert!(py.contains("class WeaveFFIError(Exception):"));
3261        assert!(py.contains("def _load_library()"));
3262        assert!(py.contains("_lib = _load_library()"));
3263
3264        let _ = std::fs::remove_dir_all(&tmp);
3265    }
3266
3267    #[test]
3268    fn generate_python_with_structs() {
3269        let api = make_api(vec![Module {
3270            name: "contacts".into(),
3271            functions: vec![],
3272            structs: vec![StructDef {
3273                name: "Contact".into(),
3274                doc: Some("A contact record".into()),
3275                fields: vec![
3276                    StructField {
3277                        name: "id".into(),
3278                        ty: TypeRef::I64,
3279                        doc: None,
3280                        default: None,
3281                    },
3282                    StructField {
3283                        name: "first_name".into(),
3284                        ty: TypeRef::StringUtf8,
3285                        doc: None,
3286                        default: None,
3287                    },
3288                    StructField {
3289                        name: "last_name".into(),
3290                        ty: TypeRef::StringUtf8,
3291                        doc: None,
3292                        default: None,
3293                    },
3294                    StructField {
3295                        name: "email".into(),
3296                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3297                        doc: None,
3298                        default: None,
3299                    },
3300                ],
3301                builder: false,
3302            }],
3303            enums: vec![],
3304            callbacks: vec![],
3305            listeners: vec![],
3306            errors: None,
3307            modules: vec![],
3308        }]);
3309
3310        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
3311
3312        assert!(py.contains("class Contact:"), "missing class decl");
3313        assert!(
3314            py.contains("\"\"\"A contact record\"\"\""),
3315            "missing doc: {py}"
3316        );
3317        assert!(py.contains("def __init__(self, _ptr: int) -> None:"));
3318        assert!(py.contains("self._ptr = _ptr"));
3319        assert!(py.contains("def __del__(self) -> None:"));
3320        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
3321
3322        assert!(py.contains("@property\n    def id(self) -> int:"));
3323        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
3324        assert!(py.contains("_fn.restype = ctypes.c_int64"));
3325
3326        assert!(py.contains("@property\n    def first_name(self) -> str:"));
3327        assert!(py.contains("weaveffi_contacts_Contact_get_first_name"));
3328
3329        assert!(py.contains("@property\n    def last_name(self) -> str:"));
3330        assert!(py.contains("weaveffi_contacts_Contact_get_last_name"));
3331
3332        assert!(py.contains("@property\n    def email(self) -> Optional[str]:"));
3333        assert!(py.contains("weaveffi_contacts_Contact_get_email"));
3334    }
3335
3336    #[test]
3337    fn generate_python_with_enums() {
3338        let api = make_api(vec![Module {
3339            name: "contacts".into(),
3340            functions: vec![Function {
3341                name: "get_type".into(),
3342                params: vec![Param {
3343                    name: "ct".into(),
3344                    ty: TypeRef::Enum("ContactType".into()),
3345                    mutable: false,
3346                    doc: None,
3347                }],
3348                returns: Some(TypeRef::Enum("ContactType".into())),
3349                doc: None,
3350                r#async: false,
3351                cancellable: false,
3352                deprecated: None,
3353                since: None,
3354            }],
3355            structs: vec![],
3356            enums: vec![EnumDef {
3357                name: "ContactType".into(),
3358                doc: Some("Type of contact".into()),
3359                variants: vec![
3360                    EnumVariant {
3361                        name: "Personal".into(),
3362                        value: 0,
3363                        doc: None,
3364                    },
3365                    EnumVariant {
3366                        name: "Work".into(),
3367                        value: 1,
3368                        doc: None,
3369                    },
3370                    EnumVariant {
3371                        name: "Other".into(),
3372                        value: 2,
3373                        doc: None,
3374                    },
3375                ],
3376            }],
3377            callbacks: vec![],
3378            listeners: vec![],
3379            errors: None,
3380            modules: vec![],
3381        }]);
3382
3383        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
3384
3385        assert!(py.contains("class ContactType(IntEnum):"));
3386        assert!(py.contains("\"\"\"Type of contact\"\"\""));
3387        assert!(py.contains("Personal = 0"));
3388        assert!(py.contains("Work = 1"));
3389        assert!(py.contains("Other = 2"));
3390
3391        assert!(
3392            py.contains("ct: \"ContactType\""),
3393            "missing enum param hint"
3394        );
3395        assert!(
3396            py.contains("-> \"ContactType\":"),
3397            "missing enum return hint"
3398        );
3399        assert!(py.contains("ct.value"), "missing .value for enum param");
3400        assert!(
3401            py.contains("return ContactType(_result)"),
3402            "missing enum return wrap"
3403        );
3404        assert!(py.contains("ctypes.c_int32"), "enum should use c_int32 ABI");
3405    }
3406
3407    #[test]
3408    fn generate_python_with_optionals() {
3409        let api = make_api(vec![Module {
3410            name: "store".into(),
3411            functions: vec![
3412                Function {
3413                    name: "find_int".into(),
3414                    params: vec![Param {
3415                        name: "key".into(),
3416                        ty: TypeRef::Optional(Box::new(TypeRef::I32)),
3417                        mutable: false,
3418                        doc: None,
3419                    }],
3420                    returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))),
3421                    doc: None,
3422                    r#async: false,
3423                    cancellable: false,
3424                    deprecated: None,
3425                    since: None,
3426                },
3427                Function {
3428                    name: "find_name".into(),
3429                    params: vec![Param {
3430                        name: "prefix".into(),
3431                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3432                        mutable: false,
3433                        doc: None,
3434                    }],
3435                    returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))),
3436                    doc: None,
3437                    r#async: false,
3438                    cancellable: false,
3439                    deprecated: None,
3440                    since: None,
3441                },
3442                Function {
3443                    name: "find_contact".into(),
3444                    params: vec![],
3445                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
3446                        "Contact".into(),
3447                    )))),
3448                    doc: None,
3449                    r#async: false,
3450                    cancellable: false,
3451                    deprecated: None,
3452                    since: None,
3453                },
3454                Function {
3455                    name: "find_flag".into(),
3456                    params: vec![],
3457                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Bool))),
3458                    doc: None,
3459                    r#async: false,
3460                    cancellable: false,
3461                    deprecated: None,
3462                    since: None,
3463                },
3464            ],
3465            structs: vec![],
3466            enums: vec![],
3467            callbacks: vec![],
3468            listeners: vec![],
3469            errors: None,
3470            modules: vec![],
3471        }]);
3472
3473        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
3474
3475        assert!(
3476            py.contains("key: Optional[int]"),
3477            "missing Optional[int] param"
3478        );
3479        assert!(
3480            py.contains("-> Optional[int]:"),
3481            "missing Optional[int] return"
3482        );
3483        assert!(
3484            py.contains("ctypes.byref(ctypes.c_int32(key)) if key is not None else None"),
3485            "missing optional i32 conversion"
3486        );
3487        assert!(
3488            py.contains("ctypes.POINTER(ctypes.c_int32)"),
3489            "missing POINTER for optional i32"
3490        );
3491
3492        assert!(
3493            py.contains("prefix: Optional[str]"),
3494            "missing Optional[str] param"
3495        );
3496        assert!(
3497            py.contains("-> Optional[str]:"),
3498            "missing Optional[str] return"
3499        );
3500        assert!(
3501            py.contains("_string_to_bytes(prefix)"),
3502            "missing optional _string_to_bytes"
3503        );
3504
3505        assert!(
3506            py.contains("-> Optional[\"Contact\"]:"),
3507            "missing Optional struct return"
3508        );
3509        assert!(
3510            py.contains("if _result is None:\n        return None\n    return Contact(_result)"),
3511            "missing optional struct None check"
3512        );
3513
3514        assert!(
3515            py.contains("-> Optional[bool]:"),
3516            "missing Optional[bool] return"
3517        );
3518        assert!(
3519            py.contains("return bool(_result[0])"),
3520            "missing optional bool deref"
3521        );
3522    }
3523
3524    #[test]
3525    fn generate_python_with_lists() {
3526        let api = make_api(vec![Module {
3527            name: "batch".into(),
3528            functions: vec![
3529                Function {
3530                    name: "process_ids".into(),
3531                    params: vec![Param {
3532                        name: "ids".into(),
3533                        ty: TypeRef::List(Box::new(TypeRef::I32)),
3534                        mutable: false,
3535                        doc: None,
3536                    }],
3537                    returns: None,
3538                    doc: None,
3539                    r#async: false,
3540                    cancellable: false,
3541                    deprecated: None,
3542                    since: None,
3543                },
3544                Function {
3545                    name: "get_names".into(),
3546                    params: vec![],
3547                    returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
3548                    doc: None,
3549                    r#async: false,
3550                    cancellable: false,
3551                    deprecated: None,
3552                    since: None,
3553                },
3554                Function {
3555                    name: "get_items".into(),
3556                    params: vec![],
3557                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Item".into())))),
3558                    doc: None,
3559                    r#async: false,
3560                    cancellable: false,
3561                    deprecated: None,
3562                    since: None,
3563                },
3564            ],
3565            structs: vec![],
3566            enums: vec![],
3567            callbacks: vec![],
3568            listeners: vec![],
3569            errors: None,
3570            modules: vec![],
3571        }]);
3572
3573        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
3574
3575        assert!(py.contains("ids: List[int]"), "missing List[int] param");
3576        assert!(
3577            py.contains("(ctypes.c_int32 * len(ids))(*ids)"),
3578            "missing list-to-array conversion"
3579        );
3580        assert!(
3581            py.contains("ctypes.POINTER(ctypes.c_int32)"),
3582            "missing POINTER for list param"
3583        );
3584        assert!(py.contains("ctypes.c_size_t"), "missing size_t for length");
3585
3586        assert!(
3587            py.contains("-> List[str]:"),
3588            "missing List[str] return: {py}"
3589        );
3590        assert!(
3591            py.contains("_bytes_to_string(_result[_i]) for _i in range(_out_len.value)"),
3592            "missing string list _bytes_to_string: {py}"
3593        );
3594
3595        assert!(
3596            py.contains("-> List[\"Item\"]:"),
3597            "missing List struct return"
3598        );
3599        assert!(
3600            py.contains("Item(_result[_i]) for _i in range(_out_len.value)"),
3601            "missing struct wrapping in list"
3602        );
3603    }
3604
3605    #[test]
3606    fn generate_python_with_maps() {
3607        let api = make_api(vec![Module {
3608            name: "config".into(),
3609            functions: vec![
3610                Function {
3611                    name: "set_config".into(),
3612                    params: vec![Param {
3613                        name: "settings".into(),
3614                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
3615                        mutable: false,
3616                        doc: None,
3617                    }],
3618                    returns: None,
3619                    doc: None,
3620                    r#async: false,
3621                    cancellable: false,
3622                    deprecated: None,
3623                    since: None,
3624                },
3625                Function {
3626                    name: "get_config".into(),
3627                    params: vec![],
3628                    returns: Some(TypeRef::Map(
3629                        Box::new(TypeRef::StringUtf8),
3630                        Box::new(TypeRef::I32),
3631                    )),
3632                    doc: None,
3633                    r#async: false,
3634                    cancellable: false,
3635                    deprecated: None,
3636                    since: None,
3637                },
3638            ],
3639            structs: vec![],
3640            enums: vec![],
3641            callbacks: vec![],
3642            listeners: vec![],
3643            errors: None,
3644            modules: vec![],
3645        }]);
3646
3647        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
3648
3649        assert!(
3650            py.contains("settings: Dict[str, int]"),
3651            "missing Dict param hint"
3652        );
3653        assert!(
3654            py.contains("list(settings.keys())"),
3655            "missing keys extraction"
3656        );
3657        assert!(
3658            py.contains("_settings_vals = [settings[_k] for _k in _settings_keys]"),
3659            "missing values extraction"
3660        );
3661        assert!(
3662            py.contains("ctypes.c_char_p * len(_settings_keys)"),
3663            "missing key array creation"
3664        );
3665        assert!(
3666            py.contains("ctypes.c_int32 * len(_settings_vals)"),
3667            "missing value array creation"
3668        );
3669
3670        assert!(
3671            py.contains("-> Dict[str, int]:"),
3672            "missing Dict return hint"
3673        );
3674        assert!(
3675            py.contains("_out_keys = ctypes.POINTER(ctypes.c_char_p)()"),
3676            "missing out_keys init"
3677        );
3678        assert!(
3679            py.contains("_out_values = ctypes.POINTER(ctypes.c_int32)()"),
3680            "missing out_values init"
3681        );
3682        assert!(
3683            py.contains("_out_len = ctypes.c_size_t(0)"),
3684            "missing out_len init"
3685        );
3686        assert!(
3687            py.contains("if not _out_keys or not _out_values:"),
3688            "missing empty map check"
3689        );
3690        assert!(
3691            py.contains("_bytes_to_string(_out_keys[_i]): _out_values[_i]"),
3692            "missing map comprehension"
3693        );
3694    }
3695
3696    #[test]
3697    fn generate_python_pyi_types() {
3698        let api = make_api(vec![Module {
3699            name: "contacts".into(),
3700            enums: vec![EnumDef {
3701                name: "ContactType".into(),
3702                doc: None,
3703                variants: vec![
3704                    EnumVariant {
3705                        name: "Personal".into(),
3706                        value: 0,
3707                        doc: None,
3708                    },
3709                    EnumVariant {
3710                        name: "Work".into(),
3711                        value: 1,
3712                        doc: None,
3713                    },
3714                    EnumVariant {
3715                        name: "Other".into(),
3716                        value: 2,
3717                        doc: None,
3718                    },
3719                ],
3720            }],
3721            callbacks: vec![],
3722            listeners: vec![],
3723            structs: vec![StructDef {
3724                name: "Contact".into(),
3725                doc: None,
3726                fields: vec![
3727                    StructField {
3728                        name: "id".into(),
3729                        ty: TypeRef::I64,
3730                        doc: None,
3731                        default: None,
3732                    },
3733                    StructField {
3734                        name: "first_name".into(),
3735                        ty: TypeRef::StringUtf8,
3736                        doc: None,
3737                        default: None,
3738                    },
3739                    StructField {
3740                        name: "email".into(),
3741                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3742                        doc: None,
3743                        default: None,
3744                    },
3745                    StructField {
3746                        name: "tags".into(),
3747                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
3748                        doc: None,
3749                        default: None,
3750                    },
3751                    StructField {
3752                        name: "scores".into(),
3753                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
3754                        doc: None,
3755                        default: None,
3756                    },
3757                ],
3758                builder: false,
3759            }],
3760            functions: vec![
3761                Function {
3762                    name: "create_contact".into(),
3763                    params: vec![
3764                        Param {
3765                            name: "name".into(),
3766                            ty: TypeRef::StringUtf8,
3767                            mutable: false,
3768                            doc: None,
3769                        },
3770                        Param {
3771                            name: "email".into(),
3772                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3773                            mutable: false,
3774                            doc: None,
3775                        },
3776                    ],
3777                    returns: Some(TypeRef::Handle),
3778                    doc: None,
3779                    r#async: false,
3780                    cancellable: false,
3781                    deprecated: None,
3782                    since: None,
3783                },
3784                Function {
3785                    name: "get_contact".into(),
3786                    params: vec![Param {
3787                        name: "id".into(),
3788                        ty: TypeRef::Handle,
3789                        mutable: false,
3790                        doc: None,
3791                    }],
3792                    returns: Some(TypeRef::Struct("Contact".into())),
3793                    doc: None,
3794                    r#async: false,
3795                    cancellable: false,
3796                    deprecated: None,
3797                    since: None,
3798                },
3799                Function {
3800                    name: "list_contacts".into(),
3801                    params: vec![],
3802                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
3803                    doc: None,
3804                    r#async: false,
3805                    cancellable: false,
3806                    deprecated: None,
3807                    since: None,
3808                },
3809                Function {
3810                    name: "delete_contact".into(),
3811                    params: vec![Param {
3812                        name: "id".into(),
3813                        ty: TypeRef::Handle,
3814                        mutable: false,
3815                        doc: None,
3816                    }],
3817                    returns: None,
3818                    doc: None,
3819                    r#async: false,
3820                    cancellable: false,
3821                    deprecated: None,
3822                    since: None,
3823                },
3824            ],
3825            errors: None,
3826            modules: vec![],
3827        }]);
3828
3829        let pyi = render_pyi_module(&api, true, "weaveffi.yml");
3830
3831        assert!(pyi.contains("from enum import IntEnum"));
3832        assert!(pyi.contains("from typing import Callable, Dict, Iterator, List, Optional"));
3833
3834        assert!(pyi.contains("class ContactType(IntEnum):"));
3835        assert!(pyi.contains("    Personal: int"));
3836        assert!(pyi.contains("    Work: int"));
3837        assert!(pyi.contains("    Other: int"));
3838
3839        assert!(pyi.contains("class Contact:"));
3840        assert!(pyi.contains("    def id(self) -> int: ..."));
3841        assert!(pyi.contains("    def first_name(self) -> str: ..."));
3842        assert!(pyi.contains("    def email(self) -> Optional[str]: ..."));
3843        assert!(pyi.contains("    def tags(self) -> List[str]: ..."));
3844        assert!(pyi.contains("    def scores(self) -> Dict[str, int]: ..."));
3845
3846        assert!(pyi.contains("def create_contact(name: str, email: Optional[str]) -> int: ..."));
3847        assert!(pyi.contains("def get_contact(id: int) -> \"Contact\": ..."));
3848        assert!(pyi.contains("def list_contacts() -> List[\"Contact\"]: ..."));
3849        assert!(pyi.contains("def delete_contact(id: int) -> None: ..."));
3850    }
3851
3852    #[test]
3853    fn generate_python_full_contacts() {
3854        let api = make_api(vec![Module {
3855            name: "contacts".into(),
3856            enums: vec![EnumDef {
3857                name: "ContactType".into(),
3858                doc: None,
3859                variants: vec![
3860                    EnumVariant {
3861                        name: "Personal".into(),
3862                        value: 0,
3863                        doc: None,
3864                    },
3865                    EnumVariant {
3866                        name: "Work".into(),
3867                        value: 1,
3868                        doc: None,
3869                    },
3870                    EnumVariant {
3871                        name: "Other".into(),
3872                        value: 2,
3873                        doc: None,
3874                    },
3875                ],
3876            }],
3877            callbacks: vec![],
3878            listeners: vec![],
3879            structs: vec![StructDef {
3880                name: "Contact".into(),
3881                doc: None,
3882                fields: vec![
3883                    StructField {
3884                        name: "id".into(),
3885                        ty: TypeRef::I64,
3886                        doc: None,
3887                        default: None,
3888                    },
3889                    StructField {
3890                        name: "first_name".into(),
3891                        ty: TypeRef::StringUtf8,
3892                        doc: None,
3893                        default: None,
3894                    },
3895                    StructField {
3896                        name: "last_name".into(),
3897                        ty: TypeRef::StringUtf8,
3898                        doc: None,
3899                        default: None,
3900                    },
3901                    StructField {
3902                        name: "email".into(),
3903                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3904                        doc: None,
3905                        default: None,
3906                    },
3907                    StructField {
3908                        name: "contact_type".into(),
3909                        ty: TypeRef::Enum("ContactType".into()),
3910                        doc: None,
3911                        default: None,
3912                    },
3913                ],
3914                builder: false,
3915            }],
3916            functions: vec![
3917                Function {
3918                    name: "create_contact".into(),
3919                    params: vec![
3920                        Param {
3921                            name: "first_name".into(),
3922                            ty: TypeRef::StringUtf8,
3923                            mutable: false,
3924                            doc: None,
3925                        },
3926                        Param {
3927                            name: "last_name".into(),
3928                            ty: TypeRef::StringUtf8,
3929                            mutable: false,
3930                            doc: None,
3931                        },
3932                        Param {
3933                            name: "email".into(),
3934                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3935                            mutable: false,
3936                            doc: None,
3937                        },
3938                        Param {
3939                            name: "contact_type".into(),
3940                            ty: TypeRef::Enum("ContactType".into()),
3941                            mutable: false,
3942                            doc: None,
3943                        },
3944                    ],
3945                    returns: Some(TypeRef::Handle),
3946                    doc: None,
3947                    r#async: false,
3948                    cancellable: false,
3949                    deprecated: None,
3950                    since: None,
3951                },
3952                Function {
3953                    name: "get_contact".into(),
3954                    params: vec![Param {
3955                        name: "id".into(),
3956                        ty: TypeRef::Handle,
3957                        mutable: false,
3958                        doc: None,
3959                    }],
3960                    returns: Some(TypeRef::Struct("Contact".into())),
3961                    doc: None,
3962                    r#async: false,
3963                    cancellable: false,
3964                    deprecated: None,
3965                    since: None,
3966                },
3967                Function {
3968                    name: "list_contacts".into(),
3969                    params: vec![],
3970                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
3971                    doc: None,
3972                    r#async: false,
3973                    cancellable: false,
3974                    deprecated: None,
3975                    since: None,
3976                },
3977                Function {
3978                    name: "delete_contact".into(),
3979                    params: vec![Param {
3980                        name: "id".into(),
3981                        ty: TypeRef::Handle,
3982                        mutable: false,
3983                        doc: None,
3984                    }],
3985                    returns: Some(TypeRef::Bool),
3986                    doc: None,
3987                    r#async: false,
3988                    cancellable: false,
3989                    deprecated: None,
3990                    since: None,
3991                },
3992                Function {
3993                    name: "count_contacts".into(),
3994                    params: vec![],
3995                    returns: Some(TypeRef::I32),
3996                    doc: None,
3997                    r#async: false,
3998                    cancellable: false,
3999                    deprecated: None,
4000                    since: None,
4001                },
4002            ],
4003            errors: None,
4004            modules: vec![],
4005        }]);
4006
4007        let tmp = std::env::temp_dir().join("weaveffi_test_py_full_contacts");
4008        let _ = std::fs::remove_dir_all(&tmp);
4009        std::fs::create_dir_all(&tmp).unwrap();
4010        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
4011
4012        PythonGenerator
4013            .generate(
4014                &api,
4015                out_dir,
4016                &PythonConfig {
4017                    strip_module_prefix: true,
4018                    ..PythonConfig::default()
4019                },
4020            )
4021            .unwrap();
4022
4023        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
4024        let pyi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.pyi")).unwrap();
4025
4026        assert!(py.contains("class ContactType(IntEnum):"));
4027        assert!(py.contains("Personal = 0"));
4028        assert!(py.contains("Work = 1"));
4029        assert!(py.contains("Other = 2"));
4030
4031        assert!(py.contains("class Contact:"));
4032        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
4033        assert!(py.contains("@property\n    def id(self) -> int:"));
4034        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
4035        assert!(py.contains("@property\n    def first_name(self) -> str:"));
4036        assert!(py.contains("weaveffi_contacts_Contact_get_first_name"));
4037        assert!(py.contains("@property\n    def last_name(self) -> str:"));
4038        assert!(py.contains("weaveffi_contacts_Contact_get_last_name"));
4039        assert!(py.contains("@property\n    def email(self) -> Optional[str]:"));
4040        assert!(py.contains("weaveffi_contacts_Contact_get_email"));
4041        assert!(py.contains("@property\n    def contact_type(self) -> \"ContactType\":"));
4042        assert!(py.contains("weaveffi_contacts_Contact_get_contact_type"));
4043        assert!(py.contains("return ContactType(_result)"));
4044
4045        assert!(py.contains("def create_contact("));
4046        assert!(py.contains("first_name: str"));
4047        assert!(py.contains("last_name: str"));
4048        assert!(py.contains("email: Optional[str]"));
4049        assert!(py.contains("contact_type: \"ContactType\""));
4050        assert!(py.contains("-> int:"));
4051        assert!(py.contains("weaveffi_contacts_create_contact"));
4052        assert!(py.contains("_string_to_bytes(first_name)"));
4053        assert!(py.contains("contact_type.value"));
4054
4055        assert!(py.contains("def get_contact(id: int) -> \"Contact\":"));
4056        assert!(py.contains("weaveffi_contacts_get_contact"));
4057        assert!(py.contains("return Contact(_result)"));
4058
4059        assert!(py.contains("def list_contacts() -> List[\"Contact\"]:"));
4060        assert!(py.contains("weaveffi_contacts_list_contacts"));
4061        assert!(py.contains("Contact(_result[_i]) for _i in range(_out_len.value)"));
4062
4063        assert!(py.contains("def delete_contact(id: int) -> bool:"));
4064        assert!(py.contains("weaveffi_contacts_delete_contact"));
4065        assert!(py.contains("return bool(_result)"));
4066
4067        assert!(py.contains("def count_contacts() -> int:"));
4068        assert!(py.contains("weaveffi_contacts_count_contacts"));
4069
4070        assert!(pyi.contains("class ContactType(IntEnum):"));
4071        assert!(pyi.contains("    Personal: int"));
4072        assert!(pyi.contains("    Work: int"));
4073        assert!(pyi.contains("    Other: int"));
4074        assert!(pyi.contains("class Contact:"));
4075        assert!(pyi.contains("def create_contact("));
4076        assert!(pyi.contains("def get_contact(id: int) -> \"Contact\": ..."));
4077        assert!(pyi.contains("def list_contacts() -> List[\"Contact\"]: ..."));
4078        assert!(pyi.contains("def delete_contact(id: int) -> bool: ..."));
4079        assert!(pyi.contains("def count_contacts() -> int: ..."));
4080
4081        let _ = std::fs::remove_dir_all(&tmp);
4082    }
4083
4084    #[test]
4085    fn python_generates_packaging() {
4086        let api = make_api(vec![simple_module(vec![Function {
4087            name: "add".into(),
4088            params: vec![
4089                Param {
4090                    name: "a".into(),
4091                    ty: TypeRef::I32,
4092                    mutable: false,
4093                    doc: None,
4094                },
4095                Param {
4096                    name: "b".into(),
4097                    ty: TypeRef::I32,
4098                    mutable: false,
4099                    doc: None,
4100                },
4101            ],
4102            returns: Some(TypeRef::I32),
4103            doc: None,
4104            r#async: false,
4105            cancellable: false,
4106            deprecated: None,
4107            since: None,
4108        }])]);
4109
4110        let tmp = std::env::temp_dir().join("weaveffi_test_python_packaging");
4111        let _ = std::fs::remove_dir_all(&tmp);
4112        std::fs::create_dir_all(&tmp).unwrap();
4113        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
4114
4115        PythonGenerator
4116            .generate(&api, out_dir, &PythonConfig::default())
4117            .unwrap();
4118
4119        let pyproject = std::fs::read_to_string(tmp.join("python/pyproject.toml")).unwrap();
4120        assert!(
4121            pyproject.contains("[build-system]"),
4122            "missing build-system: {pyproject}"
4123        );
4124        assert!(
4125            pyproject.contains("setuptools"),
4126            "missing setuptools: {pyproject}"
4127        );
4128        assert!(
4129            pyproject.contains("[project]"),
4130            "missing project section: {pyproject}"
4131        );
4132        assert!(
4133            pyproject.contains("name = \"weaveffi\""),
4134            "missing project name: {pyproject}"
4135        );
4136        assert!(
4137            pyproject.contains("version = \"0.1.0\""),
4138            "missing version: {pyproject}"
4139        );
4140        assert!(
4141            pyproject.contains("[tool.setuptools]"),
4142            "missing tool.setuptools: {pyproject}"
4143        );
4144        assert!(
4145            pyproject.contains("packages = [\"weaveffi\"]"),
4146            "missing packages list: {pyproject}"
4147        );
4148
4149        let setup = std::fs::read_to_string(tmp.join("python/setup.py")).unwrap();
4150        assert!(
4151            setup.contains("from setuptools import setup"),
4152            "missing setuptools import: {setup}"
4153        );
4154        assert!(
4155            setup.contains("name=\"weaveffi\""),
4156            "missing package name: {setup}"
4157        );
4158
4159        let readme = std::fs::read_to_string(tmp.join("python/README.md")).unwrap();
4160        assert!(
4161            readme.contains("pip install"),
4162            "missing install instructions: {readme}"
4163        );
4164
4165        let _ = std::fs::remove_dir_all(&tmp);
4166    }
4167
4168    #[test]
4169    fn python_has_memory_helpers() {
4170        let api = make_api(vec![]);
4171        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
4172        assert!(
4173            py.contains("import contextlib"),
4174            "missing contextlib import"
4175        );
4176        assert!(
4177            py.contains("class _PointerGuard(contextlib.AbstractContextManager):"),
4178            "missing _PointerGuard class"
4179        );
4180        assert!(
4181            py.contains("def __exit__(self, *exc)"),
4182            "missing _PointerGuard.__exit__"
4183        );
4184        assert!(
4185            py.contains("def _string_to_bytes("),
4186            "missing _string_to_bytes helper"
4187        );
4188        assert!(
4189            py.contains("def _bytes_to_string("),
4190            "missing _bytes_to_string helper"
4191        );
4192    }
4193
4194    #[test]
4195    fn python_custom_package_name() {
4196        let api = make_api(vec![simple_module(vec![Function {
4197            name: "add".into(),
4198            params: vec![
4199                Param {
4200                    name: "a".into(),
4201                    ty: TypeRef::I32,
4202                    mutable: false,
4203                    doc: None,
4204                },
4205                Param {
4206                    name: "b".into(),
4207                    ty: TypeRef::I32,
4208                    mutable: false,
4209                    doc: None,
4210                },
4211            ],
4212            returns: Some(TypeRef::I32),
4213            doc: None,
4214            r#async: false,
4215            cancellable: false,
4216            deprecated: None,
4217            since: None,
4218        }])]);
4219
4220        let config = PythonConfig {
4221            package_name: Some("my_bindings".into()),
4222            ..PythonConfig::default()
4223        };
4224
4225        let tmp = std::env::temp_dir().join("weaveffi_test_py_custom_pkg");
4226        let _ = std::fs::remove_dir_all(&tmp);
4227        std::fs::create_dir_all(&tmp).unwrap();
4228        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
4229
4230        PythonGenerator.generate(&api, out_dir, &config).unwrap();
4231
4232        assert!(
4233            tmp.join("python/my_bindings/__init__.py").exists(),
4234            "package dir should use custom name"
4235        );
4236        assert!(
4237            tmp.join("python/my_bindings/weaveffi.py").exists(),
4238            "module file should be inside custom package dir"
4239        );
4240
4241        let pyproject = std::fs::read_to_string(tmp.join("python/pyproject.toml")).unwrap();
4242        assert!(
4243            pyproject.contains("name = \"my_bindings\""),
4244            "pyproject.toml should use custom name: {pyproject}"
4245        );
4246        assert!(
4247            pyproject.contains("packages = [\"my_bindings\"]"),
4248            "pyproject.toml packages should use custom name: {pyproject}"
4249        );
4250
4251        let setup = std::fs::read_to_string(tmp.join("python/setup.py")).unwrap();
4252        assert!(
4253            setup.contains("name=\"my_bindings\""),
4254            "setup.py should use custom name: {setup}"
4255        );
4256
4257        let _ = std::fs::remove_dir_all(&tmp);
4258    }
4259
4260    #[test]
4261    fn python_strip_module_prefix() {
4262        let api = make_api(vec![Module {
4263            name: "contacts".into(),
4264            functions: vec![Function {
4265                name: "create_contact".into(),
4266                params: vec![Param {
4267                    name: "name".into(),
4268                    ty: TypeRef::StringUtf8,
4269                    mutable: false,
4270                    doc: None,
4271                }],
4272                returns: Some(TypeRef::I32),
4273                doc: None,
4274                r#async: false,
4275                cancellable: false,
4276                deprecated: None,
4277                since: None,
4278            }],
4279            structs: vec![],
4280            enums: vec![],
4281            callbacks: vec![],
4282            listeners: vec![],
4283            errors: None,
4284            modules: vec![],
4285        }]);
4286
4287        let config = PythonConfig {
4288            strip_module_prefix: true,
4289            ..PythonConfig::default()
4290        };
4291
4292        let tmp = std::env::temp_dir().join("weaveffi_test_python_strip_prefix");
4293        let _ = std::fs::remove_dir_all(&tmp);
4294        std::fs::create_dir_all(&tmp).unwrap();
4295        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
4296
4297        PythonGenerator.generate(&api, out_dir, &config).unwrap();
4298
4299        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
4300        assert!(
4301            py.contains("def create_contact("),
4302            "stripped name should be create_contact: {py}"
4303        );
4304        assert!(
4305            !py.contains("def contacts_create_contact("),
4306            "should not contain module-prefixed name: {py}"
4307        );
4308        assert!(
4309            py.contains("weaveffi_contacts_create_contact"),
4310            "C ABI call should still use full name: {py}"
4311        );
4312
4313        let pyi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.pyi")).unwrap();
4314        assert!(
4315            pyi.contains("def create_contact("),
4316            "pyi stripped name should be create_contact: {pyi}"
4317        );
4318
4319        let no_strip = PythonConfig::default();
4320        let tmp2 = std::env::temp_dir().join("weaveffi_test_python_no_strip_prefix");
4321        let _ = std::fs::remove_dir_all(&tmp2);
4322        std::fs::create_dir_all(&tmp2).unwrap();
4323        let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8");
4324
4325        PythonGenerator.generate(&api, out_dir2, &no_strip).unwrap();
4326
4327        let py2 = std::fs::read_to_string(tmp2.join("python/weaveffi/weaveffi.py")).unwrap();
4328        assert!(
4329            py2.contains("def contacts_create_contact("),
4330            "default should use module-prefixed name: {py2}"
4331        );
4332
4333        let pyi2 = std::fs::read_to_string(tmp2.join("python/weaveffi/weaveffi.pyi")).unwrap();
4334        assert!(
4335            pyi2.contains("def contacts_create_contact("),
4336            "pyi default should use module-prefixed name: {pyi2}"
4337        );
4338
4339        let _ = std::fs::remove_dir_all(&tmp);
4340        let _ = std::fs::remove_dir_all(&tmp2);
4341    }
4342
4343    #[test]
4344    fn python_deeply_nested_optional() {
4345        let api = make_api(vec![Module {
4346            name: "edge".into(),
4347            functions: vec![Function {
4348                name: "process".into(),
4349                params: vec![Param {
4350                    name: "data".into(),
4351                    ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
4352                        Box::new(TypeRef::Struct("Contact".into())),
4353                    ))))),
4354                    mutable: false,
4355                    doc: None,
4356                }],
4357                returns: None,
4358                doc: None,
4359                r#async: false,
4360                cancellable: false,
4361                deprecated: None,
4362                since: None,
4363            }],
4364            structs: vec![StructDef {
4365                name: "Contact".into(),
4366                doc: None,
4367                fields: vec![StructField {
4368                    name: "name".into(),
4369                    ty: TypeRef::StringUtf8,
4370                    doc: None,
4371                    default: None,
4372                }],
4373                builder: false,
4374            }],
4375            enums: vec![],
4376            callbacks: vec![],
4377            listeners: vec![],
4378            errors: None,
4379            modules: vec![],
4380        }]);
4381        let pyi = render_pyi_module(&api, true, "weaveffi.yml");
4382        assert!(
4383            pyi.contains("Optional[List[Optional["),
4384            "should contain deeply nested optional type: {pyi}"
4385        );
4386    }
4387
4388    #[test]
4389    fn python_map_of_lists() {
4390        let api = make_api(vec![Module {
4391            name: "edge".into(),
4392            functions: vec![Function {
4393                name: "process".into(),
4394                params: vec![Param {
4395                    name: "scores".into(),
4396                    ty: TypeRef::Map(
4397                        Box::new(TypeRef::StringUtf8),
4398                        Box::new(TypeRef::List(Box::new(TypeRef::I32))),
4399                    ),
4400                    mutable: false,
4401                    doc: None,
4402                }],
4403                returns: None,
4404                doc: None,
4405                r#async: false,
4406                cancellable: false,
4407                deprecated: None,
4408                since: None,
4409            }],
4410            structs: vec![],
4411            enums: vec![],
4412            callbacks: vec![],
4413            listeners: vec![],
4414            errors: None,
4415            modules: vec![],
4416        }]);
4417        let pyi = render_pyi_module(&api, true, "weaveffi.yml");
4418        assert!(
4419            pyi.contains("Dict[str, List[int]]"),
4420            "should contain map of lists type: {pyi}"
4421        );
4422    }
4423
4424    #[test]
4425    fn python_enum_keyed_map() {
4426        let api = make_api(vec![Module {
4427            name: "edge".into(),
4428            functions: vec![Function {
4429                name: "process".into(),
4430                params: vec![Param {
4431                    name: "contacts".into(),
4432                    ty: TypeRef::Map(
4433                        Box::new(TypeRef::Enum("Color".into())),
4434                        Box::new(TypeRef::Struct("Contact".into())),
4435                    ),
4436                    mutable: false,
4437                    doc: None,
4438                }],
4439                returns: None,
4440                doc: None,
4441                r#async: false,
4442                cancellable: false,
4443                deprecated: None,
4444                since: None,
4445            }],
4446            structs: vec![StructDef {
4447                name: "Contact".into(),
4448                doc: None,
4449                fields: vec![StructField {
4450                    name: "name".into(),
4451                    ty: TypeRef::StringUtf8,
4452                    doc: None,
4453                    default: None,
4454                }],
4455                builder: false,
4456            }],
4457            enums: vec![EnumDef {
4458                name: "Color".into(),
4459                doc: None,
4460                variants: vec![
4461                    EnumVariant {
4462                        name: "Red".into(),
4463                        value: 0,
4464                        doc: None,
4465                    },
4466                    EnumVariant {
4467                        name: "Green".into(),
4468                        value: 1,
4469                        doc: None,
4470                    },
4471                    EnumVariant {
4472                        name: "Blue".into(),
4473                        value: 2,
4474                        doc: None,
4475                    },
4476                ],
4477            }],
4478            callbacks: vec![],
4479            listeners: vec![],
4480            errors: None,
4481            modules: vec![],
4482        }]);
4483        let pyi = render_pyi_module(&api, true, "weaveffi.yml");
4484        assert!(
4485            pyi.contains("Dict[\"Color\", \"Contact\"]"),
4486            "should contain enum-keyed map type: {pyi}"
4487        );
4488    }
4489
4490    #[test]
4491    fn python_typed_handle_type() {
4492        let api = Api {
4493            version: "0.3.0".into(),
4494            modules: vec![Module {
4495                name: "contacts".into(),
4496                functions: vec![Function {
4497                    name: "get_info".into(),
4498                    params: vec![Param {
4499                        name: "contact".into(),
4500                        ty: TypeRef::TypedHandle("Contact".into()),
4501                        mutable: false,
4502                        doc: None,
4503                    }],
4504                    returns: None,
4505                    doc: None,
4506                    r#async: false,
4507                    cancellable: false,
4508                    deprecated: None,
4509                    since: None,
4510                }],
4511                structs: vec![StructDef {
4512                    name: "Contact".into(),
4513                    doc: None,
4514                    fields: vec![StructField {
4515                        name: "name".into(),
4516                        ty: TypeRef::StringUtf8,
4517                        doc: None,
4518                        default: None,
4519                    }],
4520                    builder: false,
4521                }],
4522                enums: vec![],
4523                callbacks: vec![],
4524                listeners: vec![],
4525                errors: None,
4526                modules: vec![],
4527            }],
4528            generators: None,
4529            package: None,
4530        };
4531        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
4532        assert!(
4533            py.contains("contact: \"Contact\""),
4534            "TypedHandle should use class type not int: {py}"
4535        );
4536        assert!(
4537            py.contains("contact._ptr"),
4538            "TypedHandle call arg should extract ._ptr: {py}"
4539        );
4540        assert!(
4541            py.contains("ctypes.c_void_p"),
4542            "TypedHandle ctypes type should be c_void_p: {py}"
4543        );
4544    }
4545
4546    #[test]
4547    fn python_no_double_free_on_error() {
4548        let api = make_api(vec![Module {
4549            name: "contacts".into(),
4550            functions: vec![Function {
4551                name: "find_contact".into(),
4552                params: vec![Param {
4553                    name: "name".into(),
4554                    ty: TypeRef::StringUtf8,
4555                    mutable: false,
4556                    doc: None,
4557                }],
4558                returns: Some(TypeRef::Struct("Contact".into())),
4559                doc: None,
4560                r#async: false,
4561                cancellable: false,
4562                deprecated: None,
4563                since: None,
4564            }],
4565            structs: vec![StructDef {
4566                name: "Contact".into(),
4567                doc: None,
4568                fields: vec![StructField {
4569                    name: "name".into(),
4570                    ty: TypeRef::StringUtf8,
4571                    doc: None,
4572                    default: None,
4573                }],
4574                builder: false,
4575            }],
4576            enums: vec![],
4577            callbacks: vec![],
4578            listeners: vec![],
4579            errors: None,
4580            modules: vec![],
4581        }]);
4582
4583        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
4584
4585        assert!(
4586            py.contains("_string_to_bytes(name)"),
4587            "string param should use _string_to_bytes(name): {py}"
4588        );
4589        assert!(
4590            !py.contains("weaveffi_free_string(name"),
4591            "input string param must not be freed with weaveffi_free_string(name): {py}"
4592        );
4593        assert!(
4594            !py.contains("free(name"),
4595            "input string param must not be passed to free(name: {py}"
4596        );
4597
4598        let fn_sig = "def find_contact(name: str) -> \"Contact\":";
4599        let start = py
4600            .find(fn_sig)
4601            .unwrap_or_else(|| panic!("missing find_contact signature: {py}"));
4602        let rest = &py[start..];
4603        let end_offset = rest[1..]
4604            .find("\n\ndef ")
4605            .or_else(|| rest[1..].find("\n\nclass "))
4606            .map(|i| i + 1)
4607            .unwrap_or(rest.len());
4608        let body = &rest[..end_offset];
4609        let err_pos = body
4610            .find("_check_error(_err)")
4611            .expect("_check_error should appear in find_contact");
4612        let contact_pos = body
4613            .find("return Contact(_result)")
4614            .expect("return Contact(_result) should appear in find_contact");
4615        assert!(
4616            err_pos < contact_pos,
4617            "_check_error(_err) should precede return Contact(_result): {body}"
4618        );
4619
4620        let class_start = py
4621            .find("class Contact:")
4622            .expect("Contact class should be defined");
4623        let after_class = &py[class_start..];
4624        let class_end = after_class[1..]
4625            .find("\n\nclass ")
4626            .or_else(|| after_class[1..].find("\n\ndef "))
4627            .map(|i| i + 1)
4628            .unwrap_or(after_class.len());
4629        let contact_class = &after_class[..class_end];
4630        assert!(
4631            contact_class.contains("def __del__(self)"),
4632            "Contact should define __del__: {contact_class}"
4633        );
4634        assert!(
4635            contact_class.contains("_destroy"),
4636            "Contact.__del__ should call _destroy: {contact_class}"
4637        );
4638    }
4639
4640    #[test]
4641    fn python_null_check_on_optional_return() {
4642        let api = make_api(vec![Module {
4643            name: "contacts".into(),
4644            functions: vec![Function {
4645                name: "find_contact".into(),
4646                params: vec![Param {
4647                    name: "id".into(),
4648                    ty: TypeRef::I32,
4649                    mutable: false,
4650                    doc: None,
4651                }],
4652                returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
4653                    "Contact".into(),
4654                )))),
4655                doc: None,
4656                r#async: false,
4657                cancellable: false,
4658                deprecated: None,
4659                since: None,
4660            }],
4661            structs: vec![StructDef {
4662                name: "Contact".into(),
4663                doc: None,
4664                fields: vec![StructField {
4665                    name: "name".into(),
4666                    ty: TypeRef::StringUtf8,
4667                    doc: None,
4668                    default: None,
4669                }],
4670                builder: false,
4671            }],
4672            enums: vec![],
4673            callbacks: vec![],
4674            listeners: vec![],
4675            errors: None,
4676            modules: vec![],
4677        }]);
4678
4679        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
4680
4681        assert!(
4682            py.contains("if _result is None:\n        return None"),
4683            "optional struct return should null-check before wrap: {py}"
4684        );
4685        let none_check = py
4686            .find("if _result is None:\n        return None")
4687            .expect("null-check block");
4688        let wrap = py
4689            .find("return Contact(_result)")
4690            .expect("Contact(_result) wrap");
4691        assert!(
4692            wrap > none_check,
4693            "Contact(_result) should appear after null check: {py}"
4694        );
4695    }
4696
4697    #[test]
4698    fn python_async_function_is_async_def() {
4699        let api = make_api(vec![simple_module(vec![Function {
4700            name: "fetch_data".into(),
4701            params: vec![Param {
4702                name: "id".into(),
4703                ty: TypeRef::I32,
4704                mutable: false,
4705                doc: None,
4706            }],
4707            returns: Some(TypeRef::StringUtf8),
4708            doc: None,
4709            r#async: true,
4710            cancellable: false,
4711            deprecated: None,
4712            since: None,
4713        }])]);
4714        let code = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
4715        assert!(
4716            code.contains("import asyncio"),
4717            "should import asyncio: {code}"
4718        );
4719        assert!(
4720            code.contains("def _fetch_data_sync(id: int) -> str:"),
4721            "should have sync helper: {code}"
4722        );
4723        assert!(
4724            code.contains("async def fetch_data(id: int) -> str:"),
4725            "should have async wrapper: {code}"
4726        );
4727        assert!(
4728            code.contains("asyncio.get_event_loop()"),
4729            "should use get_event_loop: {code}"
4730        );
4731        assert!(
4732            code.contains("run_in_executor(None, _fetch_data_sync, id)"),
4733            "should use run_in_executor with sync fn and args: {code}"
4734        );
4735    }
4736
4737    #[test]
4738    fn listener_register_unregister_wrappers() {
4739        use weaveffi_ir::ir::{CallbackDef, ListenerDef};
4740        let api = make_api(vec![Module {
4741            callbacks: vec![CallbackDef {
4742                name: "OnMessage".into(),
4743                params: vec![Param {
4744                    name: "message".into(),
4745                    ty: TypeRef::StringUtf8,
4746                    mutable: false,
4747                    doc: None,
4748                }],
4749                doc: None,
4750            }],
4751            listeners: vec![ListenerDef {
4752                name: "message_listener".into(),
4753                event_callback: "OnMessage".into(),
4754                doc: None,
4755            }],
4756            ..simple_module(vec![])
4757        }]);
4758        let code = render_python_module(&api, false, "weaveffi", "weaveffi.yml");
4759        // CFUNCTYPE alias matches the C typedef shape: (const char*, void*).
4760        assert!(
4761            code.contains(
4762                "_CFUNC_weaveffi_math_OnMessage_fn = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_void_p)"
4763            ),
4764            "callback CFUNCTYPE alias: {code}"
4765        );
4766        // Registry pinning keeps the trampoline alive until unregister.
4767        assert!(
4768            code.contains("_listener_refs: Dict[int, object] = {}"),
4769            "listener registry: {code}"
4770        );
4771        assert!(
4772            code.contains(
4773                "def math_register_message_listener(callback: Callable[[str], None]) -> int:"
4774            ),
4775            "register wrapper: {code}"
4776        );
4777        assert!(
4778            code.contains("callback(_bytes_to_string(message))"),
4779            "trampoline converts the C string: {code}"
4780        );
4781        assert!(
4782            code.contains("_listener_refs[_listener_id] = _cfunc"),
4783            "register pins the trampoline: {code}"
4784        );
4785        assert!(
4786            code.contains("def math_unregister_message_listener(listener_id: int) -> None:"),
4787            "unregister wrapper: {code}"
4788        );
4789        assert!(
4790            code.contains("_listener_refs.pop(listener_id, None)"),
4791            "unregister releases the trampoline: {code}"
4792        );
4793    }
4794
4795    #[test]
4796    fn listener_bytes_and_enum_params_convert() {
4797        use weaveffi_ir::ir::{CallbackDef, EnumDef, EnumVariant, ListenerDef};
4798        let api = make_api(vec![Module {
4799            enums: vec![EnumDef {
4800                name: "Level".into(),
4801                doc: None,
4802                variants: vec![EnumVariant {
4803                    name: "Info".into(),
4804                    value: 0,
4805                    doc: None,
4806                }],
4807            }],
4808            callbacks: vec![CallbackDef {
4809                name: "OnChunk".into(),
4810                params: vec![
4811                    Param {
4812                        name: "data".into(),
4813                        ty: TypeRef::Bytes,
4814                        mutable: false,
4815                        doc: None,
4816                    },
4817                    Param {
4818                        name: "level".into(),
4819                        ty: TypeRef::Enum("Level".into()),
4820                        mutable: false,
4821                        doc: None,
4822                    },
4823                ],
4824                doc: None,
4825            }],
4826            listeners: vec![ListenerDef {
4827                name: "chunks".into(),
4828                event_callback: "OnChunk".into(),
4829                doc: None,
4830            }],
4831            ..simple_module(vec![])
4832        }]);
4833        let code = render_python_module(&api, false, "weaveffi", "weaveffi.yml");
4834        // Bytes lower to (ptr, len) slots; the trampoline reconstructs bytes.
4835        assert!(
4836            code.contains("def _trampoline(data_ptr, data_len, level, _context):"),
4837            "trampoline signature has flattened slots: {code}"
4838        );
4839        assert!(
4840            code.contains("bytes(data_ptr[:data_len]) if data_ptr else b\"\""),
4841            "bytes param converts: {code}"
4842        );
4843        assert!(
4844            code.contains("Level(level)"),
4845            "enum param converts to IntEnum: {code}"
4846        );
4847    }
4848
4849    /// `ctypes.CFUNCTYPE` instances pin the C trampoline; `_cb` is held alive
4850    /// in the local frame for the lifetime of the synchronous helper, which
4851    /// blocks on `_ev.wait()` until the C callback fires. The `try/finally`
4852    /// around `_state` mutation ensures `_ev.set()` always runs, releasing
4853    /// the wait and letting `_cb` drop together with the helper frame.
4854    #[test]
4855    fn python_async_pins_callback_for_lifetime() {
4856        let api = make_api(vec![simple_module(vec![Function {
4857            name: "fetch_data".into(),
4858            params: vec![Param {
4859                name: "id".into(),
4860                ty: TypeRef::I32,
4861                mutable: false,
4862                doc: None,
4863            }],
4864            returns: Some(TypeRef::StringUtf8),
4865            doc: None,
4866            r#async: true,
4867            cancellable: false,
4868            deprecated: None,
4869            since: None,
4870        }])]);
4871        let code = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
4872        let pin_count = code.matches("_cb = _cb_type(_cb_impl)").count();
4873        let wait_count = code.matches("_ev.wait()").count();
4874        let set_count = code.matches("_ev.set()").count();
4875        assert_eq!(
4876            pin_count, 1,
4877            "expected one `_cb = _cb_type(_cb_impl)` per async fn, got {pin_count}: {code}"
4878        );
4879        assert_eq!(
4880            wait_count, set_count,
4881            "every `_ev.wait()` must be matched by an `_ev.set()` in finally: wait={wait_count} set={set_count}: {code}"
4882        );
4883        assert!(
4884            code.contains("finally:\n            _ev.set()"),
4885            "_ev.set() must be in a finally block: {code}"
4886        );
4887    }
4888
4889    #[test]
4890    fn python_pyi_async_function() {
4891        let api = make_api(vec![simple_module(vec![Function {
4892            name: "fetch_data".into(),
4893            params: vec![Param {
4894                name: "id".into(),
4895                ty: TypeRef::I32,
4896                mutable: false,
4897                doc: None,
4898            }],
4899            returns: Some(TypeRef::StringUtf8),
4900            doc: None,
4901            r#async: true,
4902            cancellable: false,
4903            deprecated: None,
4904            since: None,
4905        }])]);
4906        let stubs = render_pyi_module(&api, true, "weaveffi.yml");
4907        assert!(
4908            stubs.contains("async def fetch_data(id: int) -> str: ..."),
4909            "pyi should declare async def: {stubs}"
4910        );
4911    }
4912
4913    #[test]
4914    fn python_cross_module_struct() {
4915        let api = make_api(vec![
4916            Module {
4917                name: "types".into(),
4918                functions: vec![],
4919                structs: vec![StructDef {
4920                    name: "Name".into(),
4921                    doc: None,
4922                    fields: vec![StructField {
4923                        name: "value".into(),
4924                        ty: TypeRef::StringUtf8,
4925                        doc: None,
4926                        default: None,
4927                    }],
4928                    builder: false,
4929                }],
4930                enums: vec![],
4931                callbacks: vec![],
4932                listeners: vec![],
4933                errors: None,
4934                modules: vec![],
4935            },
4936            Module {
4937                name: "ops".into(),
4938                functions: vec![Function {
4939                    name: "get_name".into(),
4940                    params: vec![Param {
4941                        name: "id".into(),
4942                        ty: TypeRef::I32,
4943                        mutable: false,
4944                        doc: None,
4945                    }],
4946                    returns: Some(TypeRef::Struct("types.Name".into())),
4947                    doc: None,
4948                    r#async: false,
4949                    cancellable: false,
4950                    deprecated: None,
4951                    since: None,
4952                }],
4953                structs: vec![],
4954                enums: vec![],
4955                callbacks: vec![],
4956                listeners: vec![],
4957                errors: None,
4958                modules: vec![],
4959            },
4960        ]);
4961
4962        let code = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
4963        let stubs = render_pyi_module(&api, true, "weaveffi.yml");
4964
4965        assert!(
4966            code.contains("Name(_result)"),
4967            "cross-module return should construct Name, not types.Name: {code}"
4968        );
4969        assert!(
4970            !code.contains("types.Name"),
4971            "dot-qualified name should not appear in generated Python code: {code}"
4972        );
4973        assert!(
4974            stubs.contains("\"Name\""),
4975            "pyi should use local type name: {stubs}"
4976        );
4977        assert!(
4978            !stubs.contains("types.Name"),
4979            "dot-qualified name should not appear in pyi stubs: {stubs}"
4980        );
4981    }
4982
4983    #[test]
4984    fn python_nested_module_output() {
4985        let api = make_api(vec![Module {
4986            name: "parent".to_string(),
4987            functions: vec![Function {
4988                name: "outer_fn".to_string(),
4989                params: vec![],
4990                returns: Some(TypeRef::I32),
4991                doc: None,
4992                r#async: false,
4993                cancellable: false,
4994                deprecated: None,
4995                since: None,
4996            }],
4997            structs: vec![],
4998            enums: vec![],
4999            callbacks: vec![],
5000            listeners: vec![],
5001            errors: None,
5002            modules: vec![Module {
5003                name: "child".to_string(),
5004                functions: vec![Function {
5005                    name: "inner_fn".to_string(),
5006                    params: vec![],
5007                    returns: Some(TypeRef::I32),
5008                    doc: None,
5009                    r#async: false,
5010                    cancellable: false,
5011                    deprecated: None,
5012                    since: None,
5013                }],
5014                structs: vec![],
5015                enums: vec![],
5016                callbacks: vec![],
5017                listeners: vec![],
5018                errors: None,
5019                modules: vec![],
5020            }],
5021        }]);
5022        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
5023        assert!(
5024            py.contains("# === Module: parent ==="),
5025            "parent module section missing: {py}"
5026        );
5027        assert!(
5028            py.contains("# === Module: parent_child ==="),
5029            "nested child module section missing: {py}"
5030        );
5031        assert!(
5032            py.contains("weaveffi_parent_outer_fn"),
5033            "parent C function missing: {py}"
5034        );
5035        assert!(
5036            py.contains("weaveffi_parent_child_inner_fn"),
5037            "nested child C function missing: {py}"
5038        );
5039        let pyi = render_pyi_module(&api, true, "weaveffi.yml");
5040        assert!(
5041            pyi.contains("def inner_fn"),
5042            "nested child function missing from pyi: {pyi}"
5043        );
5044    }
5045
5046    #[test]
5047    fn python_type_hint_iterator() {
5048        assert_eq!(
5049            py_type_hint(&TypeRef::Iterator(Box::new(TypeRef::I32))),
5050            "Iterator[int]"
5051        );
5052        assert_eq!(
5053            py_type_hint(&TypeRef::Iterator(Box::new(TypeRef::Struct(
5054                "Contact".into()
5055            )))),
5056            "Iterator[\"Contact\"]"
5057        );
5058    }
5059
5060    #[test]
5061    fn python_iterator_return() {
5062        let api = make_api(vec![Module {
5063            name: "data".to_string(),
5064            functions: vec![Function {
5065                name: "list_items".to_string(),
5066                params: vec![],
5067                returns: Some(TypeRef::Iterator(Box::new(TypeRef::I32))),
5068                doc: None,
5069                r#async: false,
5070                cancellable: false,
5071                deprecated: None,
5072                since: None,
5073            }],
5074            structs: vec![],
5075            enums: vec![],
5076            callbacks: vec![],
5077            listeners: vec![],
5078            errors: None,
5079            modules: vec![],
5080        }]);
5081        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
5082        assert!(
5083            py.contains("ListItemsIterator"),
5084            "should reference iterator type name: {py}"
5085        );
5086        assert!(
5087            py.contains("_next"),
5088            "should call _next for iteration: {py}"
5089        );
5090        assert!(
5091            py.contains("_destroy"),
5092            "should call _destroy for cleanup: {py}"
5093        );
5094    }
5095
5096    #[test]
5097    fn deprecated_function_generates_annotation() {
5098        let api = make_api(vec![simple_module(vec![Function {
5099            name: "add_old".into(),
5100            params: vec![
5101                Param {
5102                    name: "a".into(),
5103                    ty: TypeRef::I32,
5104                    mutable: false,
5105                    doc: None,
5106                },
5107                Param {
5108                    name: "b".into(),
5109                    ty: TypeRef::I32,
5110                    mutable: false,
5111                    doc: None,
5112                },
5113            ],
5114            returns: Some(TypeRef::I32),
5115            doc: None,
5116            r#async: false,
5117            cancellable: false,
5118            deprecated: Some("Use add_v2 instead".into()),
5119            since: Some("0.1.0".into()),
5120        }])]);
5121        let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml");
5122        assert!(
5123            py.contains("warnings.warn(\"Use add_v2 instead\", DeprecationWarning, stacklevel=2)"),
5124            "missing deprecation warning: {py}"
5125        );
5126    }
5127
5128    fn doc_api() -> Api {
5129        make_api(vec![Module {
5130            name: "docs".into(),
5131            functions: vec![Function {
5132                name: "do_thing".into(),
5133                params: vec![Param {
5134                    name: "x".into(),
5135                    ty: TypeRef::I32,
5136                    mutable: false,
5137                    doc: Some("the input value".into()),
5138                }],
5139                returns: Some(TypeRef::I32),
5140                doc: Some("Performs a thing.".into()),
5141                r#async: false,
5142                cancellable: false,
5143                deprecated: None,
5144                since: None,
5145            }],
5146            structs: vec![StructDef {
5147                name: "Item".into(),
5148                doc: Some("An item we track.".into()),
5149                fields: vec![StructField {
5150                    name: "id".into(),
5151                    ty: TypeRef::I64,
5152                    doc: Some("Stable id".into()),
5153                    default: None,
5154                }],
5155                builder: false,
5156            }],
5157            enums: vec![EnumDef {
5158                name: "Kind".into(),
5159                doc: Some("Kind of item.".into()),
5160                variants: vec![EnumVariant {
5161                    name: "Small".into(),
5162                    value: 0,
5163                    doc: Some("A small one".into()),
5164                }],
5165            }],
5166            callbacks: vec![],
5167            listeners: vec![],
5168            errors: None,
5169            modules: vec![],
5170        }])
5171    }
5172
5173    #[test]
5174    fn python_emits_doc_on_function() {
5175        let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml");
5176        assert!(py.contains("\"\"\"Performs a thing."), "{py}");
5177    }
5178
5179    #[test]
5180    fn python_emits_doc_on_struct() {
5181        let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml");
5182        assert!(py.contains("\"\"\"An item we track.\"\"\""), "{py}");
5183    }
5184
5185    #[test]
5186    fn python_emits_doc_on_enum_variant() {
5187        let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml");
5188        assert!(py.contains("\"\"\"Kind of item.\"\"\""), "{py}");
5189        assert!(py.contains("# A small one"), "{py}");
5190    }
5191
5192    #[test]
5193    fn python_emits_doc_on_field() {
5194        let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml");
5195        assert!(py.contains("\"\"\"Stable id\"\"\""), "{py}");
5196    }
5197
5198    #[test]
5199    fn python_emits_doc_on_param() {
5200        let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml");
5201        assert!(py.contains("Parameters"), "{py}");
5202        assert!(py.contains("x : the input value"), "{py}");
5203    }
5204
5205    #[test]
5206    fn python_custom_prefix_threads_to_user_symbols() {
5207        let api = make_api(vec![simple_module(vec![Function {
5208            name: "add".into(),
5209            params: vec![
5210                Param {
5211                    name: "a".into(),
5212                    ty: TypeRef::I32,
5213                    mutable: false,
5214                    doc: None,
5215                },
5216                Param {
5217                    name: "b".into(),
5218                    ty: TypeRef::I32,
5219                    mutable: false,
5220                    doc: None,
5221                },
5222            ],
5223            returns: Some(TypeRef::I32),
5224            doc: None,
5225            r#async: false,
5226            cancellable: false,
5227            deprecated: None,
5228            since: None,
5229        }])]);
5230
5231        let py = render_python_module(&api, true, "myffi", "weaveffi.yml");
5232
5233        // User symbols honor the configured ABI prefix.
5234        assert!(
5235            py.contains("_lib.myffi_math_add"),
5236            "user symbol should use the custom prefix: {py}"
5237        );
5238        assert!(
5239            !py.contains("weaveffi_math_add"),
5240            "user symbol must not hard-code the weaveffi_ prefix: {py}"
5241        );
5242
5243        // Runtime ABI helpers stay literal regardless of the user prefix.
5244        assert!(
5245            py.contains("weaveffi_error_clear"),
5246            "runtime ABI helper must remain literal: {py}"
5247        );
5248        assert!(
5249            !py.contains("myffi_error_clear"),
5250            "runtime ABI helper must not be prefixed: {py}"
5251        );
5252    }
5253}