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