Skip to main content

weaveffi_gen_python/
lib.rs

1use anyhow::Result;
2use camino::Utf8Path;
3use weaveffi_core::codegen::Generator;
4use weaveffi_core::config::GeneratorConfig;
5use weaveffi_core::utils::{c_symbol_name, local_type_name, wrapper_name};
6use weaveffi_ir::ir::{Api, EnumDef, Function, Module, StructDef, StructField, TypeRef};
7
8pub struct PythonGenerator;
9
10impl PythonGenerator {
11    fn generate_impl(
12        &self,
13        api: &Api,
14        out_dir: &Utf8Path,
15        package_name: &str,
16        strip_module_prefix: bool,
17    ) -> Result<()> {
18        let dir = out_dir.join("python");
19        let pkg_dir = dir.join(package_name);
20        std::fs::create_dir_all(&pkg_dir)?;
21        std::fs::write(
22            pkg_dir.join("__init__.py"),
23            "from .weaveffi import *  # noqa: F401,F403\n",
24        )?;
25        std::fs::write(
26            pkg_dir.join("weaveffi.py"),
27            render_python_module(api, strip_module_prefix),
28        )?;
29        std::fs::write(
30            pkg_dir.join("weaveffi.pyi"),
31            render_pyi_module(api, strip_module_prefix),
32        )?;
33        std::fs::write(
34            dir.join("pyproject.toml"),
35            render_pyproject_toml(package_name),
36        )?;
37        std::fs::write(dir.join("setup.py"), render_setup_py(package_name))?;
38        std::fs::write(dir.join("README.md"), render_readme())?;
39        Ok(())
40    }
41}
42
43impl Generator for PythonGenerator {
44    fn name(&self) -> &'static str {
45        "python"
46    }
47
48    fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()> {
49        self.generate_impl(api, out_dir, "weaveffi", true)
50    }
51
52    fn generate_with_config(
53        &self,
54        api: &Api,
55        out_dir: &Utf8Path,
56        config: &GeneratorConfig,
57    ) -> Result<()> {
58        self.generate_impl(
59            api,
60            out_dir,
61            config.python_package_name(),
62            config.strip_module_prefix,
63        )
64    }
65
66    fn output_files(&self, _api: &Api, out_dir: &Utf8Path) -> Vec<String> {
67        let pkg = "weaveffi";
68        vec![
69            out_dir
70                .join(format!("python/{pkg}/__init__.py"))
71                .to_string(),
72            out_dir
73                .join(format!("python/{pkg}/weaveffi.py"))
74                .to_string(),
75            out_dir
76                .join(format!("python/{pkg}/weaveffi.pyi"))
77                .to_string(),
78            out_dir.join("python/pyproject.toml").to_string(),
79            out_dir.join("python/setup.py").to_string(),
80            out_dir.join("python/README.md").to_string(),
81        ]
82    }
83}
84
85// ── Type helpers ──
86
87fn is_c_pointer_type(ty: &TypeRef) -> bool {
88    matches!(
89        ty,
90        TypeRef::StringUtf8
91            | TypeRef::BorrowedStr
92            | TypeRef::Bytes
93            | TypeRef::BorrowedBytes
94            | TypeRef::Struct(_)
95            | TypeRef::TypedHandle(_)
96            | TypeRef::List(_)
97            | TypeRef::Map(_, _)
98            | TypeRef::Iterator(_)
99    )
100}
101
102fn snake_to_pascal(s: &str) -> String {
103    s.split('_')
104        .map(|part| {
105            let mut c = part.chars();
106            match c.next() {
107                None => String::new(),
108                Some(first) => first.to_uppercase().chain(c).collect(),
109            }
110        })
111        .collect()
112}
113
114fn iter_type_name(func_name: &str, module: &str) -> String {
115    let pascal = snake_to_pascal(func_name);
116    format!("weaveffi_{module}_{pascal}Iterator")
117}
118
119fn py_ctypes_scalar(ty: &TypeRef) -> &'static str {
120    match ty {
121        TypeRef::I32 => "ctypes.c_int32",
122        TypeRef::U32 => "ctypes.c_uint32",
123        TypeRef::I64 => "ctypes.c_int64",
124        TypeRef::F64 => "ctypes.c_double",
125        TypeRef::Bool => "ctypes.c_int32",
126        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "ctypes.c_char_p",
127        TypeRef::Handle => "ctypes.c_uint64",
128        TypeRef::TypedHandle(_) => "ctypes.c_void_p",
129        TypeRef::Bytes | TypeRef::BorrowedBytes => "ctypes.c_uint8",
130        TypeRef::Struct(_) => "ctypes.c_void_p",
131        TypeRef::Enum(_) => "ctypes.c_int32",
132        TypeRef::Optional(_) | TypeRef::List(_) | TypeRef::Map(_, _) | TypeRef::Iterator(_) => {
133            "ctypes.c_void_p"
134        }
135        TypeRef::Callback(_) => todo!("callback Python type"),
136    }
137}
138
139fn py_type_hint(ty: &TypeRef) -> String {
140    match ty {
141        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::Handle => "int".into(),
142        TypeRef::TypedHandle(name) => format!("\"{}\"", name),
143        TypeRef::F64 => "float".into(),
144        TypeRef::Bool => "bool".into(),
145        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "str".into(),
146        TypeRef::Bytes | TypeRef::BorrowedBytes => "bytes".into(),
147        TypeRef::Enum(name) => format!("\"{}\"", name),
148        TypeRef::Struct(name) => format!("\"{}\"", local_type_name(name)),
149        TypeRef::Optional(inner) => format!("Optional[{}]", py_type_hint(inner)),
150        TypeRef::List(inner) => format!("List[{}]", py_type_hint(inner)),
151        TypeRef::Map(k, v) => format!("Dict[{}, {}]", py_type_hint(k), py_type_hint(v)),
152        TypeRef::Iterator(inner) => format!("Iterator[{}]", py_type_hint(inner)),
153        TypeRef::Callback(_) => todo!("callback Python type"),
154    }
155}
156
157fn py_param_argtypes(ty: &TypeRef) -> Vec<String> {
158    match ty {
159        TypeRef::Bytes | TypeRef::BorrowedBytes => vec![
160            "ctypes.POINTER(ctypes.c_uint8)".into(),
161            "ctypes.c_size_t".into(),
162        ],
163        TypeRef::Optional(inner) if !is_c_pointer_type(inner) => {
164            vec![format!("ctypes.POINTER({})", py_ctypes_scalar(inner))]
165        }
166        TypeRef::Optional(inner) => py_param_argtypes(inner),
167        TypeRef::List(inner) => vec![
168            format!("ctypes.POINTER({})", py_ctypes_scalar(inner)),
169            "ctypes.c_size_t".into(),
170        ],
171        TypeRef::Map(k, v) => vec![
172            format!("ctypes.POINTER({})", py_ctypes_scalar(k)),
173            format!("ctypes.POINTER({})", py_ctypes_scalar(v)),
174            "ctypes.c_size_t".into(),
175        ],
176        _ => vec![py_ctypes_scalar(ty).into()],
177    }
178}
179
180/// Returns `(restype, out_param_argtypes)` for a return type.
181fn py_return_info(ty: &TypeRef) -> (String, Vec<String>) {
182    match ty {
183        TypeRef::Bytes | TypeRef::BorrowedBytes => (
184            "ctypes.POINTER(ctypes.c_uint8)".into(),
185            vec!["ctypes.POINTER(ctypes.c_size_t)".into()],
186        ),
187        TypeRef::Optional(inner) if !is_c_pointer_type(inner) => (
188            format!("ctypes.POINTER({})", py_ctypes_scalar(inner)),
189            vec![],
190        ),
191        TypeRef::Optional(inner) => py_return_info(inner),
192        TypeRef::List(inner) => (
193            format!("ctypes.POINTER({})", py_ctypes_scalar(inner)),
194            vec!["ctypes.POINTER(ctypes.c_size_t)".into()],
195        ),
196        TypeRef::Map(k, v) => (
197            "None".into(),
198            vec![
199                format!("ctypes.POINTER(ctypes.POINTER({}))", py_ctypes_scalar(k)),
200                format!("ctypes.POINTER(ctypes.POINTER({}))", py_ctypes_scalar(v)),
201                "ctypes.POINTER(ctypes.c_size_t)".into(),
202            ],
203        ),
204        _ => (py_ctypes_scalar(ty).into(), vec![]),
205    }
206}
207
208fn get_map_kv(ty: &TypeRef) -> Option<(&TypeRef, &TypeRef)> {
209    match ty {
210        TypeRef::Map(k, v) => Some((k, v)),
211        TypeRef::Optional(inner) => get_map_kv(inner),
212        _ => None,
213    }
214}
215
216/// `(param_name, ctypes_type)` pairs for async C callback parameters after `(context, err)`.
217fn py_async_cb_trailing_fields(ret: &Option<TypeRef>) -> Vec<(String, String)> {
218    match ret {
219        None => vec![],
220        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => vec![
221            ("result".into(), "ctypes.POINTER(ctypes.c_uint8)".into()),
222            ("result_len".into(), "ctypes.c_size_t".into()),
223        ],
224        Some(TypeRef::List(inner)) => vec![
225            (
226                "result".into(),
227                format!("ctypes.POINTER({})", py_ctypes_scalar(inner)),
228            ),
229            ("result_len".into(), "ctypes.c_size_t".into()),
230        ],
231        Some(TypeRef::Map(k, v)) => vec![
232            (
233                "result_keys".into(),
234                format!("ctypes.POINTER({})", py_ctypes_scalar(k)),
235            ),
236            (
237                "result_values".into(),
238                format!("ctypes.POINTER({})", py_ctypes_scalar(v)),
239            ),
240            ("result_len".into(), "ctypes.c_size_t".into()),
241        ],
242        Some(TypeRef::Optional(inner)) => {
243            if is_c_pointer_type(inner) {
244                py_async_cb_trailing_fields(&Some(*inner.clone()))
245            } else {
246                vec![(
247                    "result".into(),
248                    format!("ctypes.POINTER({})", py_ctypes_scalar(inner)),
249                )]
250            }
251        }
252        Some(TypeRef::Callback(_)) => todo!("async Python callback return type"),
253        Some(ty) => vec![("result".into(), py_ctypes_scalar(ty).to_string())],
254    }
255}
256
257fn append_async_success_handler(out: &mut String, ret: &Option<TypeRef>, ind: &str) {
258    match ret {
259        None => {
260            out.push_str(&format!("{ind}_state[\"val\"] = None\n"));
261        }
262        Some(TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle) => {
263            out.push_str(&format!("{ind}_state[\"val\"] = result\n"));
264        }
265        Some(TypeRef::Bool) => {
266            out.push_str(&format!("{ind}_state[\"val\"] = bool(result)\n"));
267        }
268        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
269            out.push_str(&format!(
270                "{ind}_s = _bytes_to_string(result) or \"\" if result else \"\"\n"
271            ));
272            out.push_str(&format!("{ind}if result:\n"));
273            out.push_str(&format!("{ind}    _lib.weaveffi_free_string(result)\n"));
274            out.push_str(&format!("{ind}_state[\"val\"] = _s\n"));
275        }
276        Some(TypeRef::Enum(name)) => {
277            out.push_str(&format!("{ind}_state[\"val\"] = {name}(result)\n"));
278        }
279        Some(TypeRef::Struct(name)) => {
280            let name = local_type_name(name);
281            out.push_str(&format!("{ind}if result is None:\n"));
282            out.push_str(&format!(
283                "{ind}    _state[\"err\"] = WeaveffiError(-1, \"null pointer\")\n"
284            ));
285            out.push_str(&format!("{ind}else:\n"));
286            out.push_str(&format!("{ind}    _state[\"val\"] = {name}(result)\n"));
287        }
288        Some(TypeRef::TypedHandle(name)) => {
289            out.push_str(&format!("{ind}if result is None:\n"));
290            out.push_str(&format!(
291                "{ind}    _state[\"err\"] = WeaveffiError(-1, \"null pointer\")\n"
292            ));
293            out.push_str(&format!("{ind}else:\n"));
294            out.push_str(&format!("{ind}    _state[\"val\"] = {name}(result)\n"));
295        }
296        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
297            out.push_str(&format!("{ind}if not result:\n"));
298            out.push_str(&format!("{ind}    _state[\"val\"] = b\"\"\n"));
299            out.push_str(&format!("{ind}else:\n"));
300            out.push_str(&format!("{ind}    _n = int(result_len)\n"));
301            out.push_str(&format!("{ind}    _state[\"val\"] = bytes(result[:_n])\n"));
302            out.push_str(&format!(
303                "{ind}    _lib.weaveffi_free_bytes(result, ctypes.c_size_t(_n))\n"
304            ));
305        }
306        Some(TypeRef::List(inner)) => {
307            let elem = py_read_element("result[_i]", inner);
308            out.push_str(&format!("{ind}if not result:\n"));
309            out.push_str(&format!("{ind}    _state[\"val\"] = []\n"));
310            out.push_str(&format!("{ind}else:\n"));
311            out.push_str(&format!("{ind}    _rl = int(result_len)\n"));
312            out.push_str(&format!(
313                "{ind}    _state[\"val\"] = [{elem} for _i in range(_rl)]\n"
314            ));
315        }
316        Some(TypeRef::Map(k, v)) => {
317            let kread = py_read_element("result_keys[_i]", k);
318            let vread = py_read_element("result_values[_i]", v);
319            out.push_str(&format!("{ind}if not result_keys or not result_values:\n"));
320            out.push_str(&format!("{ind}    _state[\"val\"] = {{}}\n"));
321            out.push_str(&format!("{ind}else:\n"));
322            out.push_str(&format!("{ind}    _ml = int(result_len)\n"));
323            out.push_str(&format!(
324                "{ind}    _state[\"val\"] = {{{kread}: {vread} for _i in range(_ml)}}\n"
325            ));
326        }
327        Some(TypeRef::Optional(inner)) => {
328            if is_c_pointer_type(inner) {
329                match inner.as_ref() {
330                    TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
331                        out.push_str(&format!("{ind}if not result:\n"));
332                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
333                        out.push_str(&format!("{ind}else:\n"));
334                        out.push_str(&format!("{ind}    _s = _bytes_to_string(result)\n"));
335                        out.push_str(&format!("{ind}    _lib.weaveffi_free_string(result)\n"));
336                        out.push_str(&format!("{ind}    _state[\"val\"] = _s\n"));
337                    }
338                    TypeRef::Struct(name) => {
339                        let name = local_type_name(name);
340                        out.push_str(&format!("{ind}if not result:\n"));
341                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
342                        out.push_str(&format!("{ind}else:\n"));
343                        out.push_str(&format!("{ind}    _state[\"val\"] = {name}(result)\n"));
344                    }
345                    TypeRef::TypedHandle(name) => {
346                        out.push_str(&format!("{ind}if not result:\n"));
347                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
348                        out.push_str(&format!("{ind}else:\n"));
349                        out.push_str(&format!("{ind}    _state[\"val\"] = {name}(result)\n"));
350                    }
351                    TypeRef::Bytes | TypeRef::BorrowedBytes => {
352                        out.push_str(&format!("{ind}if not result:\n"));
353                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
354                        out.push_str(&format!("{ind}else:\n"));
355                        out.push_str(&format!("{ind}    _n = int(result_len)\n"));
356                        out.push_str(&format!("{ind}    _b = bytes(result[:_n])\n"));
357                        out.push_str(&format!(
358                            "{ind}    _lib.weaveffi_free_bytes(result, ctypes.c_size_t(_n))\n"
359                        ));
360                        out.push_str(&format!("{ind}    _state[\"val\"] = _b\n"));
361                    }
362                    TypeRef::List(elem) => {
363                        let read = py_read_element("result[_i]", elem);
364                        out.push_str(&format!("{ind}if not result:\n"));
365                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
366                        out.push_str(&format!("{ind}else:\n"));
367                        out.push_str(&format!("{ind}    _rl = int(result_len)\n"));
368                        out.push_str(&format!(
369                            "{ind}    _state[\"val\"] = [{read} for _i in range(_rl)]\n"
370                        ));
371                    }
372                    TypeRef::Map(k, v) => {
373                        let kread = py_read_element("result_keys[_i]", k);
374                        let vread = py_read_element("result_values[_i]", v);
375                        out.push_str(&format!("{ind}if not result_keys or not result_values:\n"));
376                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
377                        out.push_str(&format!("{ind}else:\n"));
378                        out.push_str(&format!("{ind}    _ml = int(result_len)\n"));
379                        out.push_str(&format!(
380                            "{ind}    _state[\"val\"] = {{{kread}: {vread} for _i in range(_ml)}}\n"
381                        ));
382                    }
383                    _ => append_async_success_handler(out, &Some(*inner.clone()), ind),
384                }
385            } else {
386                match inner.as_ref() {
387                    TypeRef::Bool => {
388                        out.push_str(&format!("{ind}if not result:\n"));
389                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
390                        out.push_str(&format!("{ind}else:\n"));
391                        out.push_str(&format!("{ind}    _state[\"val\"] = bool(result[0])\n"));
392                    }
393                    TypeRef::Enum(name) => {
394                        out.push_str(&format!("{ind}if not result:\n"));
395                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
396                        out.push_str(&format!("{ind}else:\n"));
397                        out.push_str(&format!("{ind}    _state[\"val\"] = {name}(result[0])\n"));
398                    }
399                    _ => {
400                        out.push_str(&format!("{ind}if not result:\n"));
401                        out.push_str(&format!("{ind}    _state[\"val\"] = None\n"));
402                        out.push_str(&format!("{ind}else:\n"));
403                        out.push_str(&format!("{ind}    _state[\"val\"] = result[0]\n"));
404                    }
405                }
406            }
407        }
408        Some(TypeRef::Iterator(_)) => todo!("async iterator return"),
409        Some(TypeRef::Callback(_)) => todo!("async Python callback return type"),
410    }
411}
412
413fn render_async_ffi_call_body(out: &mut String, module_name: &str, f: &Function) {
414    let c_sym = c_symbol_name(module_name, &f.name);
415    let c_async = format!("{c_sym}_async");
416    let ind = "    ";
417
418    out.push_str(&format!("{ind}_fn = _lib.{c_async}\n"));
419    out.push_str(&format!("{ind}_ev = threading.Event()\n"));
420    out.push_str(&format!("{ind}_state = {{\"err\": None, \"val\": None}}\n"));
421
422    let trailing = py_async_cb_trailing_fields(&f.returns);
423    let mut cb_param_list: Vec<String> = vec!["context".into(), "err".into()];
424    cb_param_list.extend(trailing.iter().map(|(n, _)| n.clone()));
425    let cb_params_joined = cb_param_list.join(", ");
426
427    out.push_str(&format!("{ind}def _cb_impl({cb_params_joined}):\n"));
428    out.push_str(&format!("{ind}    try:\n"));
429    out.push_str(&format!(
430        "{ind}        if err and err.contents.code != 0:\n"
431    ));
432    out.push_str(&format!("{ind}            _code = err.contents.code\n"));
433    out.push_str(&format!(
434        "{ind}            _msg = err.contents.message.decode(\"utf-8\") if err.contents.message else \"\"\n"
435    ));
436    out.push_str(&format!(
437        "{ind}            _lib.weaveffi_error_clear(ctypes.byref(err.contents))\n"
438    ));
439    out.push_str(&format!(
440        "{ind}            _state[\"err\"] = WeaveffiError(_code, _msg)\n"
441    ));
442    out.push_str(&format!("{ind}        else:\n"));
443    append_async_success_handler(out, &f.returns, "            ");
444    out.push_str(&format!("{ind}    finally:\n"));
445    out.push_str(&format!("{ind}        _ev.set()\n"));
446
447    let mut cf_parts: Vec<String> = vec![
448        "ctypes.c_void_p".into(),
449        "ctypes.POINTER(_WeaveffiErrorStruct)".into(),
450    ];
451    cf_parts.extend(trailing.iter().map(|(_, t)| t.clone()));
452    out.push_str(&format!(
453        "{ind}_cb_type = ctypes.CFUNCTYPE(None, {})\n",
454        cf_parts.join(", ")
455    ));
456    out.push_str(&format!("{ind}_cb = _cb_type(_cb_impl)\n"));
457
458    let mut argtypes: Vec<String> = Vec::new();
459    for p in &f.params {
460        argtypes.extend(py_param_argtypes(&p.ty));
461    }
462    if f.cancellable {
463        argtypes.push("ctypes.c_void_p".into());
464    }
465    argtypes.push("_cb_type".into());
466    argtypes.push("ctypes.c_void_p".into());
467
468    out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", ")));
469    out.push_str(&format!("{ind}_fn.restype = None\n"));
470
471    for p in &f.params {
472        for line in py_param_conversion(&p.name, &p.ty, ind) {
473            out.push_str(&line);
474            out.push('\n');
475        }
476    }
477
478    let mut call_args: Vec<String> = Vec::new();
479    for p in &f.params {
480        call_args.extend(py_param_call_args(&p.name, &p.ty));
481    }
482    if f.cancellable {
483        call_args.push("None".into());
484    }
485    call_args.push("_cb".into());
486    call_args.push("None".into());
487
488    out.push_str(&format!("{ind}_fn({})\n", call_args.join(", ")));
489    out.push_str(&format!("{ind}_ev.wait()\n"));
490    out.push_str(&format!("{ind}if _state[\"err\"] is not None:\n"));
491    out.push_str(&format!("{ind}    raise _state[\"err\"]\n"));
492    if f.returns.is_some() {
493        out.push_str(&format!("{ind}return _state[\"val\"]\n"));
494    }
495}
496
497// ── Rendering ──
498
499fn render_python_module(api: &Api, strip_module_prefix: bool) -> String {
500    let mut out = String::new();
501    render_preamble(&mut out);
502    let has_async = collect_all_modules(&api.modules)
503        .iter()
504        .any(|m| m.functions.iter().any(|f| f.r#async));
505    if has_async {
506        out.push_str("\nimport asyncio\nimport threading\n");
507    }
508    for m in &api.modules {
509        render_python_module_content(&mut out, m, &m.name, strip_module_prefix);
510    }
511    out.push('\n');
512    out
513}
514
515fn collect_all_modules(modules: &[Module]) -> Vec<&Module> {
516    let mut all = Vec::new();
517    for m in modules {
518        all.push(m);
519        all.extend(collect_all_modules(&m.modules));
520    }
521    all
522}
523
524fn collect_modules_with_path(modules: &[Module]) -> Vec<(&Module, String)> {
525    let mut result = Vec::new();
526    for m in modules {
527        collect_module_with_path(m, &m.name, &mut result);
528    }
529    result
530}
531
532fn collect_module_with_path<'a>(m: &'a Module, path: &str, out: &mut Vec<(&'a Module, String)>) {
533    out.push((m, path.to_string()));
534    for sub in &m.modules {
535        collect_module_with_path(sub, &format!("{path}_{}", sub.name), out);
536    }
537}
538
539fn render_python_module_content(
540    out: &mut String,
541    m: &Module,
542    module_path: &str,
543    strip_module_prefix: bool,
544) {
545    out.push_str(&format!("\n\n# === Module: {} ===", module_path));
546    for e in &m.enums {
547        render_enum(out, e);
548    }
549    for s in &m.structs {
550        render_struct(out, module_path, s);
551        if s.builder {
552            render_builder(out, s);
553        }
554    }
555    for f in &m.functions {
556        render_function(out, module_path, f, strip_module_prefix);
557    }
558    for sub in &m.modules {
559        let sub_path = format!("{module_path}_{}", sub.name);
560        render_python_module_content(out, sub, &sub_path, strip_module_prefix);
561    }
562}
563
564fn render_preamble(out: &mut String) {
565    out.push_str(
566        r#""""WeaveFFI Python ctypes bindings (auto-generated)"""
567import contextlib
568import ctypes
569import platform
570from enum import IntEnum
571from typing import Dict, Iterator, List, Optional
572
573
574class WeaveffiError(Exception):
575    def __init__(self, code: int, message: str) -> None:
576        self.code = code
577        self.message = message
578        super().__init__(f"({code}) {message}")
579
580
581class _WeaveffiErrorStruct(ctypes.Structure):
582    _fields_ = [
583        ("code", ctypes.c_int32),
584        ("message", ctypes.c_char_p),
585    ]
586
587
588def _load_library() -> ctypes.CDLL:
589    system = platform.system()
590    if system == "Darwin":
591        name = "libweaveffi.dylib"
592    elif system == "Windows":
593        name = "weaveffi.dll"
594    else:
595        name = "libweaveffi.so"
596    return ctypes.CDLL(name)
597
598
599_lib = _load_library()
600_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveffiErrorStruct)]
601_lib.weaveffi_error_clear.restype = None
602_lib.weaveffi_free_string.argtypes = [ctypes.c_char_p]
603_lib.weaveffi_free_string.restype = None
604_lib.weaveffi_free_bytes.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
605_lib.weaveffi_free_bytes.restype = None
606
607
608def _check_error(err: _WeaveffiErrorStruct) -> None:
609    if err.code != 0:
610        code = err.code
611        message = err.message.decode("utf-8") if err.message else ""
612        _lib.weaveffi_error_clear(ctypes.byref(err))
613        raise WeaveffiError(code, message)
614
615
616class _PointerGuard(contextlib.AbstractContextManager):
617    def __init__(self, ptr, free_fn) -> None:
618        self.ptr = ptr
619        self._free_fn = free_fn
620
621    def __exit__(self, *exc) -> bool:
622        if self.ptr is not None:
623            self._free_fn(self.ptr)
624            self.ptr = None
625        return False
626
627
628def _string_to_bytes(s: Optional[str]) -> Optional[bytes]:
629    if s is None:
630        return None
631    return s.encode("utf-8")
632
633
634def _bytes_to_string(ptr) -> Optional[str]:
635    if ptr is None:
636        return None
637    return ptr.decode("utf-8")
638"#,
639    );
640}
641
642fn render_enum(out: &mut String, e: &EnumDef) {
643    out.push_str(&format!("\n\nclass {}(IntEnum):", e.name));
644    if let Some(doc) = &e.doc {
645        out.push_str(&format!("\n    \"\"\"{}\"\"\"", doc));
646    }
647    for v in &e.variants {
648        out.push_str(&format!("\n    {} = {}", v.name, v.value));
649    }
650    out.push('\n');
651}
652
653fn render_struct(out: &mut String, module_name: &str, s: &StructDef) {
654    let prefix = format!("weaveffi_{}_{}", module_name, s.name);
655
656    out.push_str(&format!("\n\nclass {}:", s.name));
657    if let Some(doc) = &s.doc {
658        out.push_str(&format!("\n    \"\"\"{}\"\"\"", doc));
659    }
660
661    out.push_str("\n\n    def __init__(self, _ptr: int) -> None:");
662    out.push_str("\n        self._ptr = _ptr");
663
664    out.push_str("\n\n    def __del__(self) -> None:");
665    out.push_str("\n        if self._ptr is not None:");
666    out.push_str(&format!(
667        "\n            _lib.{prefix}_destroy.argtypes = [ctypes.c_void_p]"
668    ));
669    out.push_str(&format!(
670        "\n            _lib.{prefix}_destroy.restype = None"
671    ));
672    out.push_str(&format!("\n            _lib.{prefix}_destroy(self._ptr)"));
673    out.push_str("\n            self._ptr = None");
674
675    for field in &s.fields {
676        render_getter(out, &prefix, field);
677    }
678    out.push('\n');
679}
680
681fn render_builder(out: &mut String, s: &StructDef) {
682    let builder_name = format!("{}Builder", s.name);
683    out.push_str(&format!("\n\nclass {}:", builder_name));
684    out.push_str("\n    def __init__(self) -> None:");
685    for field in &s.fields {
686        let py_ty = py_type_hint(&field.ty);
687        out.push_str(&format!(
688            "\n        self._{}: Optional[{}] = None",
689            field.name, py_ty
690        ));
691    }
692    for field in &s.fields {
693        let py_ty = py_type_hint(&field.ty);
694        out.push_str(&format!(
695            "\n\n    def with_{}(self, value: {}) -> \"{}\":",
696            field.name, py_ty, builder_name
697        ));
698        out.push_str(&format!("\n        self._{} = value", field.name));
699        out.push_str("\n        return self");
700    }
701    let ret_ty = py_type_hint(&TypeRef::Struct(s.name.clone()));
702    out.push_str(&format!("\n\n    def build(self) -> {}:", ret_ty));
703    for field in &s.fields {
704        out.push_str(&format!(
705            "\n        if self._{} is None:\n            raise ValueError(\"missing field: {}\")",
706            field.name, field.name
707        ));
708    }
709    out.push_str(&format!(
710        "\n        raise NotImplementedError(\"{}Builder.build requires FFI backing\")",
711        s.name
712    ));
713    out.push('\n');
714}
715
716fn render_getter(out: &mut String, prefix: &str, field: &StructField) {
717    let getter = format!("{prefix}_get_{}", field.name);
718    let py_ty = py_type_hint(&field.ty);
719    let ind = "        ";
720
721    out.push_str(&format!(
722        "\n\n    @property\n    def {}(self) -> {}:\n",
723        field.name, py_ty
724    ));
725    out.push_str(&format!("{ind}_fn = _lib.{getter}\n"));
726
727    let (restype, out_argtypes) = py_return_info(&field.ty);
728    let mut argtypes = vec!["ctypes.c_void_p".to_string()];
729    argtypes.extend(out_argtypes.iter().cloned());
730
731    out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", ")));
732    out.push_str(&format!("{ind}_fn.restype = {restype}\n"));
733
734    if out_argtypes.is_empty() {
735        out.push_str(&format!("{ind}_result = _fn(self._ptr)\n"));
736    } else if let Some((k, v)) = get_map_kv(&field.ty) {
737        out.push_str(&format!(
738            "{ind}_out_keys = ctypes.POINTER({})()\n",
739            py_ctypes_scalar(k)
740        ));
741        out.push_str(&format!(
742            "{ind}_out_values = ctypes.POINTER({})()\n",
743            py_ctypes_scalar(v)
744        ));
745        out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
746        out.push_str(&format!("{ind}_fn(self._ptr, ctypes.byref(_out_keys), ctypes.byref(_out_values), ctypes.byref(_out_len))\n"));
747    } else {
748        out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
749        out.push_str(&format!(
750            "{ind}_result = _fn(self._ptr, ctypes.byref(_out_len))\n"
751        ));
752    }
753
754    render_return_value(out, &field.ty, ind);
755}
756
757fn render_function(out: &mut String, module_name: &str, f: &Function, strip_module_prefix: bool) {
758    let func_name = wrapper_name(module_name, &f.name, strip_module_prefix);
759    let params_sig: Vec<String> = f
760        .params
761        .iter()
762        .map(|p| format!("{}: {}", p.name, py_type_hint(&p.ty)))
763        .collect();
764    let ret_hint = f
765        .returns
766        .as_ref()
767        .map(py_type_hint)
768        .unwrap_or_else(|| "None".to_string());
769
770    let def_name = if f.r#async {
771        format!("_{func_name}_sync")
772    } else {
773        func_name.clone()
774    };
775
776    if let Some(TypeRef::Iterator(inner)) = &f.returns {
777        render_iterator_class(out, module_name, &f.name, inner);
778    }
779
780    out.push_str(&format!(
781        "\n\ndef {}({}) -> {}:\n",
782        def_name,
783        params_sig.join(", "),
784        ret_hint
785    ));
786
787    let ind = "    ";
788
789    if let Some(msg) = &f.deprecated {
790        out.push_str(&format!(
791            "{ind}import warnings\n{ind}warnings.warn(\"{}\", DeprecationWarning, stacklevel=2)\n",
792            msg.replace('"', "\\\"")
793        ));
794    }
795
796    if f.r#async {
797        render_async_ffi_call_body(out, module_name, f);
798    } else {
799        let c_sym = c_symbol_name(module_name, &f.name);
800        out.push_str(&format!("{ind}_fn = _lib.{c_sym}\n"));
801
802        let mut argtypes: Vec<String> = Vec::new();
803        for p in &f.params {
804            argtypes.extend(py_param_argtypes(&p.ty));
805        }
806        let mut out_ret_argtypes = Vec::new();
807        let restype;
808        if let Some(ret_ty) = &f.returns {
809            let (rt, oat) = py_return_info(ret_ty);
810            argtypes.extend(oat.iter().cloned());
811            restype = rt;
812            out_ret_argtypes = oat;
813        } else {
814            restype = "None".to_string();
815        }
816        argtypes.push("ctypes.POINTER(_WeaveffiErrorStruct)".into());
817
818        out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", ")));
819        out.push_str(&format!("{ind}_fn.restype = {restype}\n"));
820
821        for p in &f.params {
822            for line in py_param_conversion(&p.name, &p.ty, ind) {
823                out.push_str(&line);
824                out.push('\n');
825            }
826        }
827
828        out.push_str(&format!("{ind}_err = _WeaveffiErrorStruct()\n"));
829
830        let is_map_ret = f.returns.as_ref().and_then(get_map_kv).is_some();
831        let has_out_len = !out_ret_argtypes.is_empty() && !is_map_ret;
832
833        if let Some((k, v)) = f.returns.as_ref().and_then(get_map_kv) {
834            out.push_str(&format!(
835                "{ind}_out_keys = ctypes.POINTER({})()\n",
836                py_ctypes_scalar(k)
837            ));
838            out.push_str(&format!(
839                "{ind}_out_values = ctypes.POINTER({})()\n",
840                py_ctypes_scalar(v)
841            ));
842            out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
843        } else if has_out_len {
844            out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
845        }
846
847        let mut call_args: Vec<String> = Vec::new();
848        for p in &f.params {
849            call_args.extend(py_param_call_args(&p.name, &p.ty));
850        }
851        if is_map_ret {
852            call_args.push("ctypes.byref(_out_keys)".into());
853            call_args.push("ctypes.byref(_out_values)".into());
854            call_args.push("ctypes.byref(_out_len)".into());
855        } else if has_out_len {
856            call_args.push("ctypes.byref(_out_len)".into());
857        }
858        call_args.push("ctypes.byref(_err)".into());
859
860        let call_expr = format!("_fn({})", call_args.join(", "));
861        if f.returns.is_some() && !is_map_ret {
862            out.push_str(&format!("{ind}_result = {call_expr}\n"));
863        } else {
864            out.push_str(&format!("{ind}{call_expr}\n"));
865        }
866
867        out.push_str(&format!("{ind}_check_error(_err)\n"));
868
869        if let Some(ret_ty) = &f.returns {
870            if let TypeRef::Iterator(inner) = ret_ty {
871                render_iterator_return(out, module_name, &f.name, inner, ind);
872            } else {
873                render_return_value(out, ret_ty, ind);
874            }
875        }
876    }
877
878    if f.r#async {
879        let params_joined = params_sig.join(", ");
880        out.push_str(&format!(
881            "\n\nasync def {}({}) -> {}:\n",
882            func_name, params_joined, ret_hint
883        ));
884        out.push_str("    _loop = asyncio.get_event_loop()\n");
885        let arg_names: Vec<&str> = f.params.iter().map(|p| p.name.as_str()).collect();
886        let executor_args = if arg_names.is_empty() {
887            def_name
888        } else {
889            format!("{def_name}, {}", arg_names.join(", "))
890        };
891        if f.returns.is_some() {
892            out.push_str(&format!(
893                "    return await _loop.run_in_executor(None, {executor_args})\n"
894            ));
895        } else {
896            out.push_str(&format!(
897                "    await _loop.run_in_executor(None, {executor_args})\n"
898            ));
899        }
900    }
901}
902
903// ── Param helpers ──
904
905fn py_list_convert_expr(name: &str, elem: &TypeRef) -> String {
906    match elem {
907        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
908            format!("*[_string_to_bytes(v) for v in {name}]")
909        }
910        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => format!("*[v._ptr for v in {name}]"),
911        TypeRef::Enum(_) => format!("*[v.value for v in {name}]"),
912        TypeRef::Bool => format!("*[1 if v else 0 for v in {name}]"),
913        _ => format!("*{name}"),
914    }
915}
916
917fn py_map_elem_convert(list_name: &str, ty: &TypeRef, var: &str) -> String {
918    match ty {
919        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
920            format!("*[_string_to_bytes({var}) for {var} in {list_name}]")
921        }
922        TypeRef::Enum(_) => format!("*[{var}.value for {var} in {list_name}]"),
923        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
924            format!("*[{var}._ptr for {var} in {list_name}]")
925        }
926        TypeRef::Bool => format!("*[1 if {var} else 0 for {var} in {list_name}]"),
927        _ => format!("*{list_name}"),
928    }
929}
930
931fn py_param_conversion(name: &str, ty: &TypeRef, ind: &str) -> Vec<String> {
932    match ty {
933        TypeRef::Bytes | TypeRef::BorrowedBytes => {
934            let s = py_ctypes_scalar(&TypeRef::Bytes);
935            vec![format!("{ind}_{name}_arr = ({s} * len({name}))(*{name})")]
936        }
937        TypeRef::Optional(inner) => match inner.as_ref() {
938            TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
939                let s = py_ctypes_scalar(inner);
940                vec![format!(
941                    "{ind}_{name}_c = ctypes.byref({s}({name})) if {name} is not None else None"
942                )]
943            }
944            TypeRef::Bool => {
945                vec![format!(
946                    "{ind}_{name}_c = ctypes.byref(ctypes.c_int32(1 if {name} else 0)) if {name} is not None else None"
947                )]
948            }
949            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
950                vec![format!("{ind}_{name}_c = _string_to_bytes({name})")]
951            }
952            TypeRef::Enum(_) => {
953                vec![format!(
954                    "{ind}_{name}_c = ctypes.byref(ctypes.c_int32({name}.value)) if {name} is not None else None"
955                )]
956            }
957            TypeRef::Bytes | TypeRef::BorrowedBytes => {
958                let s = py_ctypes_scalar(&TypeRef::Bytes);
959                vec![
960                    format!("{ind}if {name} is not None:"),
961                    format!("{ind}    _{name}_arr = ({s} * len({name}))(*{name})"),
962                    format!("{ind}    _{name}_len = len({name})"),
963                    format!("{ind}else:"),
964                    format!("{ind}    _{name}_arr = None"),
965                    format!("{ind}    _{name}_len = 0"),
966                ]
967            }
968            TypeRef::List(elem) => {
969                let s = py_ctypes_scalar(elem);
970                let convert = py_list_convert_expr(name, elem);
971                vec![
972                    format!("{ind}if {name} is not None:"),
973                    format!("{ind}    _{name}_arr = ({s} * len({name}))({convert})"),
974                    format!("{ind}    _{name}_len = len({name})"),
975                    format!("{ind}else:"),
976                    format!("{ind}    _{name}_arr = None"),
977                    format!("{ind}    _{name}_len = 0"),
978                ]
979            }
980            _ => vec![],
981        },
982        TypeRef::List(inner) => {
983            let s = py_ctypes_scalar(inner);
984            let convert = py_list_convert_expr(name, inner);
985            vec![format!("{ind}_{name}_arr = ({s} * len({name}))({convert})")]
986        }
987        TypeRef::Map(k, v) => {
988            let ks = py_ctypes_scalar(k);
989            let vs = py_ctypes_scalar(v);
990            let kconv = py_map_elem_convert(&format!("_{name}_keys"), k, "_k");
991            let vconv = py_map_elem_convert(&format!("_{name}_vals"), v, "_v");
992            vec![
993                format!("{ind}_{name}_keys = list({name}.keys())"),
994                format!("{ind}_{name}_vals = [{name}[_k] for _k in _{name}_keys]"),
995                format!("{ind}_{name}_ka = ({ks} * len(_{name}_keys))({kconv})"),
996                format!("{ind}_{name}_va = ({vs} * len(_{name}_vals))({vconv})"),
997            ]
998        }
999        _ => vec![],
1000    }
1001}
1002
1003fn py_param_call_args(name: &str, ty: &TypeRef) -> Vec<String> {
1004    match ty {
1005        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
1006            vec![name.to_string()]
1007        }
1008        TypeRef::Bool => vec![format!("1 if {name} else 0")],
1009        TypeRef::StringUtf8 | TypeRef::BorrowedStr => vec![format!("_string_to_bytes({name})")],
1010        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1011            vec![format!("_{name}_arr"), format!("len({name})")]
1012        }
1013        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => vec![format!("{name}._ptr")],
1014        TypeRef::Enum(_) => vec![format!("{name}.value")],
1015        TypeRef::Optional(inner) => match inner.as_ref() {
1016            TypeRef::StringUtf8 | TypeRef::BorrowedStr => vec![format!("_{name}_c")],
1017            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
1018                vec![format!("{name}._ptr if {name} is not None else None")]
1019            }
1020            TypeRef::Bytes | TypeRef::BorrowedBytes | TypeRef::List(_) => {
1021                vec![format!("_{name}_arr"), format!("_{name}_len")]
1022            }
1023            TypeRef::Map(_, _) => vec![
1024                format!("_{name}_ka"),
1025                format!("_{name}_va"),
1026                format!("_{name}_len"),
1027            ],
1028            _ if !is_c_pointer_type(inner) => vec![format!("_{name}_c")],
1029            _ => py_param_call_args(name, inner),
1030        },
1031        TypeRef::List(_) => vec![format!("_{name}_arr"), format!("len({name})")],
1032        TypeRef::Map(_, _) => vec![
1033            format!("_{name}_ka"),
1034            format!("_{name}_va"),
1035            format!("len(_{name}_keys)"),
1036        ],
1037        TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"),
1038        TypeRef::Callback(_) => todo!("callback Python param call args"),
1039    }
1040}
1041
1042// ── Return helpers ──
1043
1044fn py_read_element(expr: &str, ty: &TypeRef) -> String {
1045    match ty {
1046        TypeRef::StringUtf8 | TypeRef::BorrowedStr => format!("_bytes_to_string({expr})"),
1047        TypeRef::Struct(name) => {
1048            let name = local_type_name(name);
1049            format!("{name}({expr})")
1050        }
1051        TypeRef::TypedHandle(name) => format!("{name}({expr})"),
1052        TypeRef::Enum(name) => format!("{name}({expr})"),
1053        TypeRef::Bool => format!("bool({expr})"),
1054        _ => expr.to_string(),
1055    }
1056}
1057
1058fn render_return_value(out: &mut String, ty: &TypeRef, ind: &str) {
1059    match ty {
1060        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
1061            out.push_str(&format!("{ind}return _result\n"));
1062        }
1063        TypeRef::Bool => {
1064            out.push_str(&format!("{ind}return bool(_result)\n"));
1065        }
1066        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1067            out.push_str(&format!("{ind}return _bytes_to_string(_result) or \"\"\n"));
1068        }
1069        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1070            out.push_str(&format!("{ind}if not _result:\n"));
1071            out.push_str(&format!("{ind}    return b\"\"\n"));
1072            out.push_str(&format!("{ind}return bytes(_result[:_out_len.value])\n"));
1073        }
1074        TypeRef::Struct(name) => {
1075            let name = local_type_name(name);
1076            out.push_str(&format!("{ind}if _result is None:\n"));
1077            out.push_str(&format!(
1078                "{ind}    raise WeaveffiError(-1, \"null pointer\")\n"
1079            ));
1080            out.push_str(&format!("{ind}return {name}(_result)\n"));
1081        }
1082        TypeRef::TypedHandle(name) => {
1083            out.push_str(&format!("{ind}if _result is None:\n"));
1084            out.push_str(&format!(
1085                "{ind}    raise WeaveffiError(-1, \"null pointer\")\n"
1086            ));
1087            out.push_str(&format!("{ind}return {name}(_result)\n"));
1088        }
1089        TypeRef::Enum(name) => {
1090            out.push_str(&format!("{ind}return {name}(_result)\n"));
1091        }
1092        TypeRef::Optional(inner) => render_optional_return(out, inner, ind),
1093        TypeRef::List(inner) => render_list_return(out, inner, ind),
1094        TypeRef::Map(k, v) => render_map_return(out, k, v, ind),
1095        TypeRef::Iterator(_) => unreachable!("iterator return handled in render_function"),
1096        TypeRef::Callback(_) => todo!("callback Python return"),
1097    }
1098}
1099
1100fn render_optional_return(out: &mut String, inner: &TypeRef, ind: &str) {
1101    match inner {
1102        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1103            out.push_str(&format!("{ind}return _bytes_to_string(_result)\n"));
1104        }
1105        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1106            out.push_str(&format!("{ind}if not _result:\n"));
1107            out.push_str(&format!("{ind}    return None\n"));
1108            out.push_str(&format!("{ind}return bytes(_result[:_out_len.value])\n"));
1109        }
1110        TypeRef::Struct(name) => {
1111            let name = local_type_name(name);
1112            out.push_str(&format!("{ind}if _result is None:\n"));
1113            out.push_str(&format!("{ind}    return None\n"));
1114            out.push_str(&format!("{ind}return {name}(_result)\n"));
1115        }
1116        TypeRef::TypedHandle(name) => {
1117            out.push_str(&format!("{ind}if _result is None:\n"));
1118            out.push_str(&format!("{ind}    return None\n"));
1119            out.push_str(&format!("{ind}return {name}(_result)\n"));
1120        }
1121        TypeRef::Enum(name) => {
1122            out.push_str(&format!("{ind}if not _result:\n"));
1123            out.push_str(&format!("{ind}    return None\n"));
1124            out.push_str(&format!("{ind}return {name}(_result[0])\n"));
1125        }
1126        TypeRef::Bool => {
1127            out.push_str(&format!("{ind}if not _result:\n"));
1128            out.push_str(&format!("{ind}    return None\n"));
1129            out.push_str(&format!("{ind}return bool(_result[0])\n"));
1130        }
1131        _ if !is_c_pointer_type(inner) => {
1132            out.push_str(&format!("{ind}if not _result:\n"));
1133            out.push_str(&format!("{ind}    return None\n"));
1134            out.push_str(&format!("{ind}return _result[0]\n"));
1135        }
1136        _ => {
1137            out.push_str(&format!("{ind}return _result\n"));
1138        }
1139    }
1140}
1141
1142fn render_list_return(out: &mut String, inner: &TypeRef, ind: &str) {
1143    out.push_str(&format!("{ind}if not _result:\n"));
1144    out.push_str(&format!("{ind}    return []\n"));
1145    let elem = py_read_element("_result[_i]", inner);
1146    out.push_str(&format!(
1147        "{ind}return [{elem} for _i in range(_out_len.value)]\n"
1148    ));
1149}
1150
1151fn render_map_return(out: &mut String, k: &TypeRef, v: &TypeRef, ind: &str) {
1152    out.push_str(&format!("{ind}if not _out_keys or not _out_values:\n"));
1153    out.push_str(&format!("{ind}    return {{}}\n"));
1154    let key_read = py_read_element("_out_keys[_i]", k);
1155    let val_read = py_read_element("_out_values[_i]", v);
1156    out.push_str(&format!(
1157        "{ind}return {{{key_read}: {val_read} for _i in range(_out_len.value)}}\n"
1158    ));
1159}
1160
1161// ── Iterator helpers ──
1162
1163fn py_read_iter_item(inner: &TypeRef) -> String {
1164    match inner {
1165        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "_bytes_to_string(_out_item.value)".into(),
1166        TypeRef::Struct(name) => {
1167            let name = local_type_name(name);
1168            format!("{name}(_out_item.value)")
1169        }
1170        TypeRef::TypedHandle(name) => format!("{name}(_out_item.value)"),
1171        TypeRef::Enum(name) => format!("{name}(_out_item.value)"),
1172        TypeRef::Bool => "bool(_out_item.value)".into(),
1173        _ => "_out_item.value".into(),
1174    }
1175}
1176
1177fn render_iterator_class(out: &mut String, module_name: &str, func_name: &str, inner: &TypeRef) {
1178    let iter_tag = iter_type_name(func_name, module_name);
1179    let pascal = snake_to_pascal(func_name);
1180    let class_name = format!("_{pascal}Iterator");
1181    let item_scalar = py_ctypes_scalar(inner);
1182    let read_expr = py_read_iter_item(inner);
1183
1184    out.push_str(&format!("\n\nclass {class_name}:"));
1185    out.push_str("\n    def __init__(self, ptr):");
1186    out.push_str("\n        self._ptr = ptr");
1187    out.push_str("\n        self._done = False");
1188
1189    out.push_str("\n\n    def __iter__(self):");
1190    out.push_str("\n        return self");
1191
1192    out.push_str("\n\n    def __next__(self):");
1193    out.push_str("\n        if self._done:");
1194    out.push_str("\n            raise StopIteration");
1195    out.push_str(&format!("\n        _next_fn = _lib.{iter_tag}_next"));
1196    out.push_str(&format!(
1197        "\n        _next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER({item_scalar}), ctypes.POINTER(_WeaveffiErrorStruct)]"
1198    ));
1199    out.push_str("\n        _next_fn.restype = ctypes.c_int32");
1200    out.push_str(&format!("\n        _out_item = {item_scalar}()"));
1201    out.push_str("\n        _err = _WeaveffiErrorStruct()");
1202    out.push_str(
1203        "\n        _has = _next_fn(self._ptr, ctypes.byref(_out_item), ctypes.byref(_err))",
1204    );
1205    out.push_str("\n        _check_error(_err)");
1206    out.push_str("\n        if not _has:");
1207    out.push_str("\n            self._done = True");
1208    out.push_str("\n            self._destroy()");
1209    out.push_str("\n            raise StopIteration");
1210    out.push_str(&format!("\n        return {read_expr}"));
1211
1212    out.push_str("\n\n    def _destroy(self):");
1213    out.push_str("\n        if self._ptr is not None:");
1214    out.push_str(&format!(
1215        "\n            _destroy_fn = _lib.{iter_tag}_destroy"
1216    ));
1217    out.push_str("\n            _destroy_fn.argtypes = [ctypes.c_void_p]");
1218    out.push_str("\n            _destroy_fn.restype = None");
1219    out.push_str("\n            _destroy_fn(self._ptr)");
1220    out.push_str("\n            self._ptr = None");
1221
1222    out.push_str("\n\n    def __del__(self):");
1223    out.push_str("\n        self._destroy()");
1224    out.push('\n');
1225}
1226
1227fn render_iterator_return(
1228    out: &mut String,
1229    module_name: &str,
1230    func_name: &str,
1231    inner: &TypeRef,
1232    ind: &str,
1233) {
1234    let iter_tag = iter_type_name(func_name, module_name);
1235    let item_scalar = py_ctypes_scalar(inner);
1236    let read_expr = py_read_iter_item(inner);
1237
1238    out.push_str(&format!("{ind}_next_fn = _lib.{iter_tag}_next\n"));
1239    out.push_str(&format!(
1240        "{ind}_next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER({item_scalar}), ctypes.POINTER(_WeaveffiErrorStruct)]\n"
1241    ));
1242    out.push_str(&format!("{ind}_next_fn.restype = ctypes.c_int32\n"));
1243
1244    out.push_str(&format!("{ind}_destroy_fn = _lib.{iter_tag}_destroy\n"));
1245    out.push_str(&format!("{ind}_destroy_fn.argtypes = [ctypes.c_void_p]\n"));
1246    out.push_str(&format!("{ind}_destroy_fn.restype = None\n"));
1247
1248    out.push_str(&format!("{ind}_items = []\n"));
1249    out.push_str(&format!("{ind}while True:\n"));
1250    out.push_str(&format!("{ind}    _out_item = {item_scalar}()\n"));
1251    out.push_str(&format!("{ind}    _item_err = _WeaveffiErrorStruct()\n"));
1252    out.push_str(&format!(
1253        "{ind}    _has = _next_fn(_result, ctypes.byref(_out_item), ctypes.byref(_item_err))\n"
1254    ));
1255    out.push_str(&format!("{ind}    _check_error(_item_err)\n"));
1256    out.push_str(&format!("{ind}    if not _has:\n"));
1257    out.push_str(&format!("{ind}        break\n"));
1258    out.push_str(&format!("{ind}    _items.append({read_expr})\n"));
1259
1260    out.push_str(&format!("{ind}_destroy_fn(_result)\n"));
1261    out.push_str(&format!("{ind}return _items\n"));
1262}
1263
1264// ── Packaging ──
1265
1266fn render_pyproject_toml(package_name: &str) -> String {
1267    format!(
1268        r#"[build-system]
1269requires = ["setuptools>=61.0"]
1270build-backend = "setuptools.build_meta"
1271
1272[project]
1273name = "{package_name}"
1274version = "0.1.0"
1275description = "Python bindings for WeaveFFI (auto-generated)"
1276requires-python = ">=3.8"
1277
1278[tool.setuptools]
1279packages = ["{package_name}"]
1280"#,
1281    )
1282}
1283
1284fn render_setup_py(package_name: &str) -> String {
1285    format!(
1286        r#"from setuptools import setup
1287
1288setup(
1289    name="{package_name}",
1290    version="0.1.0",
1291    packages=["{package_name}"],
1292)
1293"#,
1294    )
1295}
1296
1297fn render_readme() -> &'static str {
1298    r#"# WeaveFFI Python Bindings
1299
1300Auto-generated Python bindings using ctypes.
1301
1302## Prerequisites
1303
1304- Python >= 3.8
1305- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path.
1306
1307## Install
1308
1309```bash
1310pip install .
1311```
1312
1313## Development install
1314
1315```bash
1316pip install -e .
1317```
1318
1319## Usage
1320
1321```python
1322from weaveffi import *
1323```
1324"#
1325}
1326
1327// ── Type stub (.pyi) rendering ──
1328
1329fn render_pyi_module(api: &Api, strip_module_prefix: bool) -> String {
1330    let mut out = String::from(
1331        "from enum import IntEnum\nfrom typing import Dict, Iterator, List, Optional\n",
1332    );
1333    for (m, path) in collect_modules_with_path(&api.modules) {
1334        for e in &m.enums {
1335            render_pyi_enum(&mut out, e);
1336        }
1337        for s in &m.structs {
1338            render_pyi_struct(&mut out, s);
1339        }
1340        for f in &m.functions {
1341            render_pyi_function(&mut out, &path, f, strip_module_prefix);
1342        }
1343    }
1344    out
1345}
1346
1347fn render_pyi_enum(out: &mut String, e: &EnumDef) {
1348    out.push_str(&format!("\nclass {}(IntEnum):\n", e.name));
1349    for v in &e.variants {
1350        out.push_str(&format!("    {}: int\n", v.name));
1351    }
1352}
1353
1354fn render_pyi_struct(out: &mut String, s: &StructDef) {
1355    out.push_str(&format!("\nclass {}:\n", s.name));
1356    for field in &s.fields {
1357        let py_ty = py_type_hint(&field.ty);
1358        out.push_str(&format!(
1359            "    @property\n    def {}(self) -> {}: ...\n",
1360            field.name, py_ty
1361        ));
1362    }
1363}
1364
1365fn render_pyi_function(
1366    out: &mut String,
1367    module_name: &str,
1368    f: &Function,
1369    strip_module_prefix: bool,
1370) {
1371    let func_name = wrapper_name(module_name, &f.name, strip_module_prefix);
1372    let params: Vec<String> = f
1373        .params
1374        .iter()
1375        .map(|p| format!("{}: {}", p.name, py_type_hint(&p.ty)))
1376        .collect();
1377    let ret = f
1378        .returns
1379        .as_ref()
1380        .map(py_type_hint)
1381        .unwrap_or_else(|| "None".into());
1382    let async_kw = if f.r#async { "async " } else { "" };
1383    out.push_str(&format!(
1384        "\n{async_kw}def {}({}) -> {}: ...\n",
1385        func_name,
1386        params.join(", "),
1387        ret
1388    ));
1389}
1390
1391#[cfg(test)]
1392mod tests {
1393    use super::*;
1394    use camino::Utf8Path;
1395    use weaveffi_core::config::GeneratorConfig;
1396    use weaveffi_ir::ir::{
1397        Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef,
1398    };
1399
1400    fn make_api(modules: Vec<Module>) -> Api {
1401        Api {
1402            version: "0.1.0".into(),
1403            modules,
1404            generators: None,
1405        }
1406    }
1407
1408    fn simple_module(functions: Vec<Function>) -> Module {
1409        Module {
1410            name: "math".into(),
1411            functions,
1412            structs: vec![],
1413            enums: vec![],
1414            callbacks: vec![],
1415            listeners: vec![],
1416            errors: None,
1417            modules: vec![],
1418        }
1419    }
1420
1421    #[test]
1422    fn generator_name_is_python() {
1423        assert_eq!(PythonGenerator.name(), "python");
1424    }
1425
1426    #[test]
1427    fn generate_creates_output_files() {
1428        let api = make_api(vec![simple_module(vec![Function {
1429            name: "add".into(),
1430            params: vec![
1431                Param {
1432                    name: "a".into(),
1433                    ty: TypeRef::I32,
1434                    mutable: false,
1435                },
1436                Param {
1437                    name: "b".into(),
1438                    ty: TypeRef::I32,
1439                    mutable: false,
1440                },
1441            ],
1442            returns: Some(TypeRef::I32),
1443            doc: None,
1444            r#async: false,
1445            cancellable: false,
1446            deprecated: None,
1447            since: None,
1448        }])]);
1449
1450        let tmp = std::env::temp_dir().join("weaveffi_test_python_gen_output");
1451        let _ = std::fs::remove_dir_all(&tmp);
1452        std::fs::create_dir_all(&tmp).unwrap();
1453        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
1454
1455        PythonGenerator.generate(&api, out_dir).unwrap();
1456
1457        let init = std::fs::read_to_string(tmp.join("python/weaveffi/__init__.py")).unwrap();
1458        assert!(init.contains("from .weaveffi import *"));
1459
1460        let weaveffi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
1461        assert!(weaveffi.contains("WeaveFFI"));
1462        assert!(weaveffi.contains("def add("));
1463
1464        let _ = std::fs::remove_dir_all(&tmp);
1465    }
1466
1467    #[test]
1468    fn output_files_lists_all() {
1469        let api = make_api(vec![]);
1470        let out = Utf8Path::new("/tmp/out");
1471        let files = PythonGenerator.output_files(&api, out);
1472        assert_eq!(
1473            files,
1474            vec![
1475                out.join("python/weaveffi/__init__.py").to_string(),
1476                out.join("python/weaveffi/weaveffi.py").to_string(),
1477                out.join("python/weaveffi/weaveffi.pyi").to_string(),
1478                out.join("python/pyproject.toml").to_string(),
1479                out.join("python/setup.py").to_string(),
1480                out.join("python/README.md").to_string(),
1481            ]
1482        );
1483    }
1484
1485    #[test]
1486    fn preamble_has_load_library() {
1487        let api = make_api(vec![]);
1488        let py = render_python_module(&api, true);
1489        assert!(py.contains("def _load_library()"), "missing _load_library");
1490        assert!(
1491            py.contains("libweaveffi.dylib"),
1492            "missing macOS library name"
1493        );
1494        assert!(py.contains("libweaveffi.so"), "missing Linux library name");
1495        assert!(py.contains("weaveffi.dll"), "missing Windows library name");
1496        assert!(py.contains("ctypes.CDLL(name)"), "missing CDLL call");
1497    }
1498
1499    #[test]
1500    fn preamble_has_error_handling() {
1501        let api = make_api(vec![]);
1502        let py = render_python_module(&api, true);
1503        assert!(
1504            py.contains("class WeaveffiError(Exception):"),
1505            "missing error class"
1506        );
1507        assert!(
1508            py.contains("class _WeaveffiErrorStruct(ctypes.Structure):"),
1509            "missing error struct"
1510        );
1511        assert!(py.contains("def _check_error("), "missing _check_error");
1512        assert!(
1513            py.contains("weaveffi_error_clear"),
1514            "missing error_clear setup"
1515        );
1516    }
1517
1518    #[test]
1519    fn simple_i32_function() {
1520        let api = make_api(vec![simple_module(vec![Function {
1521            name: "add".into(),
1522            params: vec![
1523                Param {
1524                    name: "a".into(),
1525                    ty: TypeRef::I32,
1526                    mutable: false,
1527                },
1528                Param {
1529                    name: "b".into(),
1530                    ty: TypeRef::I32,
1531                    mutable: false,
1532                },
1533            ],
1534            returns: Some(TypeRef::I32),
1535            doc: None,
1536            r#async: false,
1537            cancellable: false,
1538            deprecated: None,
1539            since: None,
1540        }])]);
1541
1542        let py = render_python_module(&api, true);
1543        assert!(
1544            py.contains("def add(a: int, b: int) -> int:"),
1545            "missing function signature: {py}"
1546        );
1547        assert!(
1548            py.contains("_lib.weaveffi_math_add"),
1549            "missing C symbol: {py}"
1550        );
1551        assert!(
1552            py.contains("ctypes.c_int32, ctypes.c_int32"),
1553            "missing argtypes: {py}"
1554        );
1555        assert!(
1556            py.contains("_fn.restype = ctypes.c_int32"),
1557            "missing restype: {py}"
1558        );
1559        assert!(
1560            py.contains("_check_error(_err)"),
1561            "missing error check: {py}"
1562        );
1563        assert!(py.contains("return _result"), "missing return: {py}");
1564    }
1565
1566    #[test]
1567    fn string_function_encode_decode() {
1568        let api = make_api(vec![Module {
1569            name: "text".into(),
1570            functions: vec![Function {
1571                name: "echo".into(),
1572                params: vec![Param {
1573                    name: "msg".into(),
1574                    ty: TypeRef::StringUtf8,
1575                    mutable: false,
1576                }],
1577                returns: Some(TypeRef::StringUtf8),
1578                doc: None,
1579                r#async: false,
1580                cancellable: false,
1581                deprecated: None,
1582                since: None,
1583            }],
1584            structs: vec![],
1585            enums: vec![],
1586            callbacks: vec![],
1587            listeners: vec![],
1588            errors: None,
1589            modules: vec![],
1590        }]);
1591
1592        let py = render_python_module(&api, true);
1593        assert!(
1594            py.contains("def echo(msg: str) -> str:"),
1595            "missing signature: {py}"
1596        );
1597        assert!(py.contains("ctypes.c_char_p"), "missing c_char_p: {py}");
1598        assert!(
1599            py.contains("_string_to_bytes(msg)"),
1600            "missing _string_to_bytes call: {py}"
1601        );
1602        assert!(
1603            py.contains("_bytes_to_string(_result)"),
1604            "missing _bytes_to_string call: {py}"
1605        );
1606    }
1607
1608    #[test]
1609    fn void_function() {
1610        let api = make_api(vec![simple_module(vec![Function {
1611            name: "reset".into(),
1612            params: vec![],
1613            returns: None,
1614            doc: None,
1615            r#async: false,
1616            cancellable: false,
1617            deprecated: None,
1618            since: None,
1619        }])]);
1620
1621        let py = render_python_module(&api, true);
1622        assert!(
1623            py.contains("def reset() -> None:"),
1624            "missing void signature: {py}"
1625        );
1626        assert!(
1627            py.contains("_fn.restype = None"),
1628            "missing None restype: {py}"
1629        );
1630        assert!(
1631            !py.contains("_result ="),
1632            "void function should not assign _result: {py}"
1633        );
1634    }
1635
1636    #[test]
1637    fn enum_intenum_class() {
1638        let api = make_api(vec![Module {
1639            name: "paint".into(),
1640            functions: vec![],
1641            structs: vec![],
1642            enums: vec![EnumDef {
1643                name: "Color".into(),
1644                doc: Some("Primary colors".into()),
1645                variants: vec![
1646                    EnumVariant {
1647                        name: "Red".into(),
1648                        value: 0,
1649                        doc: None,
1650                    },
1651                    EnumVariant {
1652                        name: "Green".into(),
1653                        value: 1,
1654                        doc: None,
1655                    },
1656                    EnumVariant {
1657                        name: "Blue".into(),
1658                        value: 2,
1659                        doc: None,
1660                    },
1661                ],
1662            }],
1663            callbacks: vec![],
1664            listeners: vec![],
1665            errors: None,
1666            modules: vec![],
1667        }]);
1668
1669        let py = render_python_module(&api, true);
1670        assert!(
1671            py.contains("class Color(IntEnum):"),
1672            "missing IntEnum class: {py}"
1673        );
1674        assert!(
1675            py.contains("\"\"\"Primary colors\"\"\""),
1676            "missing doc: {py}"
1677        );
1678        assert!(py.contains("Red = 0"), "missing Red: {py}");
1679        assert!(py.contains("Green = 1"), "missing Green: {py}");
1680        assert!(py.contains("Blue = 2"), "missing Blue: {py}");
1681    }
1682
1683    #[test]
1684    fn enum_param_and_return() {
1685        let api = make_api(vec![Module {
1686            name: "paint".into(),
1687            functions: vec![Function {
1688                name: "mix".into(),
1689                params: vec![Param {
1690                    name: "a".into(),
1691                    ty: TypeRef::Enum("Color".into()),
1692                    mutable: false,
1693                }],
1694                returns: Some(TypeRef::Enum("Color".into())),
1695                doc: None,
1696                r#async: false,
1697                cancellable: false,
1698                deprecated: None,
1699                since: None,
1700            }],
1701            structs: vec![],
1702            enums: vec![],
1703            callbacks: vec![],
1704            listeners: vec![],
1705            errors: None,
1706            modules: vec![],
1707        }]);
1708
1709        let py = render_python_module(&api, true);
1710        assert!(py.contains("a: \"Color\""), "missing enum param hint: {py}");
1711        assert!(
1712            py.contains("-> \"Color\":"),
1713            "missing enum return hint: {py}"
1714        );
1715        assert!(py.contains("a.value"), "missing .value conversion: {py}");
1716        assert!(
1717            py.contains("return Color(_result)"),
1718            "missing enum return wrap: {py}"
1719        );
1720    }
1721
1722    #[test]
1723    fn struct_class_with_getters() {
1724        let api = make_api(vec![Module {
1725            name: "contacts".into(),
1726            functions: vec![],
1727            structs: vec![StructDef {
1728                name: "Contact".into(),
1729                doc: None,
1730                fields: vec![
1731                    StructField {
1732                        name: "name".into(),
1733                        ty: TypeRef::StringUtf8,
1734                        doc: None,
1735                        default: None,
1736                    },
1737                    StructField {
1738                        name: "age".into(),
1739                        ty: TypeRef::I32,
1740                        doc: None,
1741                        default: None,
1742                    },
1743                ],
1744                builder: false,
1745            }],
1746            enums: vec![],
1747            callbacks: vec![],
1748            listeners: vec![],
1749            errors: None,
1750            modules: vec![],
1751        }]);
1752
1753        let py = render_python_module(&api, true);
1754        assert!(py.contains("class Contact:"), "missing class: {py}");
1755        assert!(
1756            py.contains("def __init__(self, _ptr: int)"),
1757            "missing __init__: {py}"
1758        );
1759        assert!(
1760            py.contains("self._ptr = _ptr"),
1761            "missing _ptr assignment: {py}"
1762        );
1763        assert!(py.contains("def __del__(self)"), "missing __del__: {py}");
1764        assert!(
1765            py.contains("weaveffi_contacts_Contact_destroy"),
1766            "missing destroy call: {py}"
1767        );
1768        assert!(
1769            py.contains("def name(self) -> str:"),
1770            "missing name getter: {py}"
1771        );
1772        assert!(
1773            py.contains("weaveffi_contacts_Contact_get_name"),
1774            "missing name getter C call: {py}"
1775        );
1776        assert!(
1777            py.contains("_bytes_to_string(_result)"),
1778            "missing _bytes_to_string in getter: {py}"
1779        );
1780        assert!(
1781            py.contains("def age(self) -> int:"),
1782            "missing age getter: {py}"
1783        );
1784        assert!(
1785            py.contains("weaveffi_contacts_Contact_get_age"),
1786            "missing age getter C call: {py}"
1787        );
1788    }
1789
1790    #[test]
1791    fn python_builder_generated() {
1792        let api = Api {
1793            version: "0.1.0".into(),
1794            modules: vec![Module {
1795                name: "contacts".into(),
1796                functions: vec![],
1797                structs: vec![StructDef {
1798                    name: "Contact".into(),
1799                    doc: None,
1800                    fields: vec![
1801                        StructField {
1802                            name: "name".into(),
1803                            ty: TypeRef::StringUtf8,
1804                            doc: None,
1805                            default: None,
1806                        },
1807                        StructField {
1808                            name: "age".into(),
1809                            ty: TypeRef::I32,
1810                            doc: None,
1811                            default: None,
1812                        },
1813                    ],
1814                    builder: true,
1815                }],
1816                enums: vec![],
1817                callbacks: vec![],
1818                listeners: vec![],
1819                errors: None,
1820                modules: vec![],
1821            }],
1822            generators: None,
1823        };
1824        let dir = tempfile::tempdir().unwrap();
1825        let out = Utf8Path::from_path(dir.path()).unwrap();
1826        PythonGenerator.generate(&api, out).unwrap();
1827        let py = std::fs::read_to_string(out.join("python/weaveffi/weaveffi.py")).unwrap();
1828        assert!(
1829            py.contains("class ContactBuilder"),
1830            "missing builder class: {py}"
1831        );
1832        assert!(py.contains("def with_name("), "missing with_name: {py}");
1833        assert!(py.contains("def with_age("), "missing with_age: {py}");
1834        assert!(py.contains("def build("), "missing build: {py}");
1835    }
1836
1837    #[test]
1838    fn struct_return() {
1839        let api = make_api(vec![Module {
1840            name: "contacts".into(),
1841            functions: vec![Function {
1842                name: "get_contact".into(),
1843                params: vec![Param {
1844                    name: "id".into(),
1845                    ty: TypeRef::Handle,
1846                    mutable: false,
1847                }],
1848                returns: Some(TypeRef::Struct("Contact".into())),
1849                doc: None,
1850                r#async: false,
1851                cancellable: false,
1852                deprecated: None,
1853                since: None,
1854            }],
1855            structs: vec![],
1856            enums: vec![],
1857            callbacks: vec![],
1858            listeners: vec![],
1859            errors: None,
1860            modules: vec![],
1861        }]);
1862
1863        let py = render_python_module(&api, true);
1864        assert!(
1865            py.contains("-> \"Contact\":"),
1866            "missing struct return hint: {py}"
1867        );
1868        assert!(
1869            py.contains("ctypes.c_void_p"),
1870            "missing void_p for struct: {py}"
1871        );
1872        assert!(
1873            py.contains("return Contact(_result)"),
1874            "missing struct wrapping: {py}"
1875        );
1876    }
1877
1878    #[test]
1879    fn bool_uses_c_int32() {
1880        let api = make_api(vec![simple_module(vec![Function {
1881            name: "is_valid".into(),
1882            params: vec![Param {
1883                name: "flag".into(),
1884                ty: TypeRef::Bool,
1885                mutable: false,
1886            }],
1887            returns: Some(TypeRef::Bool),
1888            doc: None,
1889            r#async: false,
1890            cancellable: false,
1891            deprecated: None,
1892            since: None,
1893        }])]);
1894
1895        let py = render_python_module(&api, true);
1896        assert!(py.contains("flag: bool"), "missing bool param: {py}");
1897        assert!(py.contains("-> bool:"), "missing bool return: {py}");
1898        assert!(
1899            py.contains("ctypes.c_int32"),
1900            "missing c_int32 for Bool: {py}"
1901        );
1902        assert!(
1903            py.contains("1 if flag else 0"),
1904            "missing bool-to-int conversion: {py}"
1905        );
1906        assert!(
1907            py.contains("return bool(_result)"),
1908            "missing int-to-bool conversion: {py}"
1909        );
1910    }
1911
1912    #[test]
1913    fn handle_uses_c_uint64() {
1914        let api = make_api(vec![simple_module(vec![Function {
1915            name: "create".into(),
1916            params: vec![],
1917            returns: Some(TypeRef::Handle),
1918            doc: None,
1919            r#async: false,
1920            cancellable: false,
1921            deprecated: None,
1922            since: None,
1923        }])]);
1924
1925        let py = render_python_module(&api, true);
1926        assert!(
1927            py.contains("ctypes.c_uint64"),
1928            "missing c_uint64 for Handle: {py}"
1929        );
1930    }
1931
1932    #[test]
1933    fn bytes_param_and_return() {
1934        let api = make_api(vec![Module {
1935            name: "store".into(),
1936            functions: vec![Function {
1937                name: "process".into(),
1938                params: vec![Param {
1939                    name: "data".into(),
1940                    ty: TypeRef::Bytes,
1941                    mutable: false,
1942                }],
1943                returns: Some(TypeRef::Bytes),
1944                doc: None,
1945                r#async: false,
1946                cancellable: false,
1947                deprecated: None,
1948                since: None,
1949            }],
1950            structs: vec![],
1951            enums: vec![],
1952            callbacks: vec![],
1953            listeners: vec![],
1954            errors: None,
1955            modules: vec![],
1956        }]);
1957
1958        let py = render_python_module(&api, true);
1959        assert!(py.contains("data: bytes"), "missing bytes param: {py}");
1960        assert!(py.contains("-> bytes:"), "missing bytes return: {py}");
1961        assert!(
1962            py.contains("ctypes.POINTER(ctypes.c_uint8)"),
1963            "missing uint8 pointer: {py}"
1964        );
1965        assert!(py.contains("ctypes.c_size_t"), "missing size_t: {py}");
1966        assert!(py.contains("_out_len"), "missing out_len: {py}");
1967    }
1968
1969    #[test]
1970    fn optional_value_param_and_return() {
1971        let api = make_api(vec![Module {
1972            name: "store".into(),
1973            functions: vec![Function {
1974                name: "find".into(),
1975                params: vec![Param {
1976                    name: "id".into(),
1977                    ty: TypeRef::Optional(Box::new(TypeRef::I32)),
1978                    mutable: false,
1979                }],
1980                returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))),
1981                doc: None,
1982                r#async: false,
1983                cancellable: false,
1984                deprecated: None,
1985                since: None,
1986            }],
1987            structs: vec![],
1988            enums: vec![],
1989            callbacks: vec![],
1990            listeners: vec![],
1991            errors: None,
1992            modules: vec![],
1993        }]);
1994
1995        let py = render_python_module(&api, true);
1996        assert!(
1997            py.contains("id: Optional[int]"),
1998            "missing optional param: {py}"
1999        );
2000        assert!(
2001            py.contains("-> Optional[int]:"),
2002            "missing optional return: {py}"
2003        );
2004        assert!(
2005            py.contains("ctypes.POINTER(ctypes.c_int32)"),
2006            "missing POINTER for optional: {py}"
2007        );
2008        assert!(
2009            py.contains("ctypes.byref(ctypes.c_int32(id)) if id is not None else None"),
2010            "missing optional param conversion: {py}"
2011        );
2012        assert!(py.contains("return None"), "missing None return path: {py}");
2013        assert!(
2014            py.contains("return _result[0]"),
2015            "missing pointer deref: {py}"
2016        );
2017    }
2018
2019    #[test]
2020    fn optional_string_return() {
2021        let api = make_api(vec![Module {
2022            name: "store".into(),
2023            functions: vec![Function {
2024                name: "get_name".into(),
2025                params: vec![],
2026                returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))),
2027                doc: None,
2028                r#async: false,
2029                cancellable: false,
2030                deprecated: None,
2031                since: None,
2032            }],
2033            structs: vec![],
2034            enums: vec![],
2035            callbacks: vec![],
2036            listeners: vec![],
2037            errors: None,
2038            modules: vec![],
2039        }]);
2040
2041        let py = render_python_module(&api, true);
2042        assert!(
2043            py.contains("-> Optional[str]:"),
2044            "missing optional str return: {py}"
2045        );
2046        assert!(
2047            py.contains("return _bytes_to_string(_result)"),
2048            "missing _bytes_to_string for optional string: {py}"
2049        );
2050    }
2051
2052    #[test]
2053    fn list_param_and_return() {
2054        let api = make_api(vec![Module {
2055            name: "batch".into(),
2056            functions: vec![
2057                Function {
2058                    name: "process".into(),
2059                    params: vec![Param {
2060                        name: "ids".into(),
2061                        ty: TypeRef::List(Box::new(TypeRef::I32)),
2062                        mutable: false,
2063                    }],
2064                    returns: None,
2065                    doc: None,
2066                    r#async: false,
2067                    cancellable: false,
2068                    deprecated: None,
2069                    since: None,
2070                },
2071                Function {
2072                    name: "get_ids".into(),
2073                    params: vec![],
2074                    returns: Some(TypeRef::List(Box::new(TypeRef::I32))),
2075                    doc: None,
2076                    r#async: false,
2077                    cancellable: false,
2078                    deprecated: None,
2079                    since: None,
2080                },
2081            ],
2082            structs: vec![],
2083            enums: vec![],
2084            callbacks: vec![],
2085            listeners: vec![],
2086            errors: None,
2087            modules: vec![],
2088        }]);
2089
2090        let py = render_python_module(&api, true);
2091        assert!(py.contains("ids: List[int]"), "missing list param: {py}");
2092        assert!(py.contains("-> List[int]:"), "missing list return: {py}");
2093        assert!(
2094            py.contains("ctypes.c_int32 * len(ids)"),
2095            "missing ctypes array creation: {py}"
2096        );
2097        assert!(
2098            py.contains("_out_len"),
2099            "missing out_len for list return: {py}"
2100        );
2101        assert!(
2102            py.contains("for _i in range(_out_len.value)"),
2103            "missing list iteration: {py}"
2104        );
2105    }
2106
2107    #[test]
2108    fn map_param_and_return() {
2109        let api = make_api(vec![Module {
2110            name: "store".into(),
2111            functions: vec![
2112                Function {
2113                    name: "update".into(),
2114                    params: vec![Param {
2115                        name: "scores".into(),
2116                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
2117                        mutable: false,
2118                    }],
2119                    returns: None,
2120                    doc: None,
2121                    r#async: false,
2122                    cancellable: false,
2123                    deprecated: None,
2124                    since: None,
2125                },
2126                Function {
2127                    name: "get_scores".into(),
2128                    params: vec![],
2129                    returns: Some(TypeRef::Map(
2130                        Box::new(TypeRef::StringUtf8),
2131                        Box::new(TypeRef::I32),
2132                    )),
2133                    doc: None,
2134                    r#async: false,
2135                    cancellable: false,
2136                    deprecated: None,
2137                    since: None,
2138                },
2139            ],
2140            structs: vec![],
2141            enums: vec![],
2142            callbacks: vec![],
2143            listeners: vec![],
2144            errors: None,
2145            modules: vec![],
2146        }]);
2147
2148        let py = render_python_module(&api, true);
2149        assert!(
2150            py.contains("scores: Dict[str, int]"),
2151            "missing map param: {py}"
2152        );
2153        assert!(
2154            py.contains("-> Dict[str, int]:"),
2155            "missing map return: {py}"
2156        );
2157        assert!(
2158            py.contains("list(scores.keys())"),
2159            "missing keys extraction: {py}"
2160        );
2161        assert!(py.contains("_out_keys"), "missing out_keys: {py}");
2162        assert!(py.contains("_out_values"), "missing out_values: {py}");
2163    }
2164
2165    #[test]
2166    fn struct_optional_string_getter() {
2167        let api = make_api(vec![Module {
2168            name: "contacts".into(),
2169            functions: vec![],
2170            structs: vec![StructDef {
2171                name: "Contact".into(),
2172                doc: None,
2173                fields: vec![StructField {
2174                    name: "email".into(),
2175                    ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2176                    doc: None,
2177                    default: None,
2178                }],
2179                builder: false,
2180            }],
2181            enums: vec![],
2182            callbacks: vec![],
2183            listeners: vec![],
2184            errors: None,
2185            modules: vec![],
2186        }]);
2187
2188        let py = render_python_module(&api, true);
2189        assert!(
2190            py.contains("def email(self) -> Optional[str]:"),
2191            "missing optional getter: {py}"
2192        );
2193        assert!(
2194            py.contains("_bytes_to_string(_result)"),
2195            "missing _bytes_to_string in optional getter: {py}"
2196        );
2197    }
2198
2199    #[test]
2200    fn struct_enum_field_getter() {
2201        let api = make_api(vec![Module {
2202            name: "contacts".into(),
2203            functions: vec![],
2204            structs: vec![StructDef {
2205                name: "Contact".into(),
2206                doc: None,
2207                fields: vec![StructField {
2208                    name: "role".into(),
2209                    ty: TypeRef::Enum("Role".into()),
2210                    doc: None,
2211                    default: None,
2212                }],
2213                builder: false,
2214            }],
2215            enums: vec![],
2216            callbacks: vec![],
2217            listeners: vec![],
2218            errors: None,
2219            modules: vec![],
2220        }]);
2221
2222        let py = render_python_module(&api, true);
2223        assert!(
2224            py.contains("def role(self) -> \"Role\":"),
2225            "missing enum getter: {py}"
2226        );
2227        assert!(
2228            py.contains("return Role(_result)"),
2229            "missing enum wrapping in getter: {py}"
2230        );
2231    }
2232
2233    #[test]
2234    fn comprehensive_contacts_api() {
2235        let api = make_api(vec![Module {
2236            name: "contacts".into(),
2237            enums: vec![EnumDef {
2238                name: "ContactType".into(),
2239                doc: None,
2240                variants: vec![
2241                    EnumVariant {
2242                        name: "Personal".into(),
2243                        value: 0,
2244                        doc: None,
2245                    },
2246                    EnumVariant {
2247                        name: "Work".into(),
2248                        value: 1,
2249                        doc: None,
2250                    },
2251                ],
2252            }],
2253            callbacks: vec![],
2254            listeners: vec![],
2255            structs: vec![StructDef {
2256                name: "Contact".into(),
2257                doc: None,
2258                fields: vec![
2259                    StructField {
2260                        name: "id".into(),
2261                        ty: TypeRef::I64,
2262                        doc: None,
2263                        default: None,
2264                    },
2265                    StructField {
2266                        name: "first_name".into(),
2267                        ty: TypeRef::StringUtf8,
2268                        doc: None,
2269                        default: None,
2270                    },
2271                    StructField {
2272                        name: "email".into(),
2273                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2274                        doc: None,
2275                        default: None,
2276                    },
2277                    StructField {
2278                        name: "contact_type".into(),
2279                        ty: TypeRef::Enum("ContactType".into()),
2280                        doc: None,
2281                        default: None,
2282                    },
2283                ],
2284                builder: false,
2285            }],
2286            functions: vec![
2287                Function {
2288                    name: "create_contact".into(),
2289                    params: vec![
2290                        Param {
2291                            name: "first_name".into(),
2292                            ty: TypeRef::StringUtf8,
2293                            mutable: false,
2294                        },
2295                        Param {
2296                            name: "email".into(),
2297                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2298                            mutable: false,
2299                        },
2300                        Param {
2301                            name: "contact_type".into(),
2302                            ty: TypeRef::Enum("ContactType".into()),
2303                            mutable: false,
2304                        },
2305                    ],
2306                    returns: Some(TypeRef::Handle),
2307                    doc: None,
2308                    r#async: false,
2309                    cancellable: false,
2310                    deprecated: None,
2311                    since: None,
2312                },
2313                Function {
2314                    name: "get_contact".into(),
2315                    params: vec![Param {
2316                        name: "id".into(),
2317                        ty: TypeRef::Handle,
2318                        mutable: false,
2319                    }],
2320                    returns: Some(TypeRef::Struct("Contact".into())),
2321                    doc: None,
2322                    r#async: false,
2323                    cancellable: false,
2324                    deprecated: None,
2325                    since: None,
2326                },
2327                Function {
2328                    name: "list_contacts".into(),
2329                    params: vec![],
2330                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
2331                    doc: None,
2332                    r#async: false,
2333                    cancellable: false,
2334                    deprecated: None,
2335                    since: None,
2336                },
2337                Function {
2338                    name: "count_contacts".into(),
2339                    params: vec![],
2340                    returns: Some(TypeRef::I32),
2341                    doc: None,
2342                    r#async: false,
2343                    cancellable: false,
2344                    deprecated: None,
2345                    since: None,
2346                },
2347            ],
2348            errors: None,
2349            modules: vec![],
2350        }]);
2351
2352        let tmp = std::env::temp_dir().join("weaveffi_test_python_gen_contacts");
2353        let _ = std::fs::remove_dir_all(&tmp);
2354        std::fs::create_dir_all(&tmp).unwrap();
2355        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2356
2357        PythonGenerator.generate(&api, out_dir).unwrap();
2358
2359        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
2360
2361        assert!(py.contains("class ContactType(IntEnum):"));
2362        assert!(py.contains("Personal = 0"));
2363        assert!(py.contains("Work = 1"));
2364
2365        assert!(py.contains("class Contact:"));
2366        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
2367        assert!(py.contains("def id(self) -> int:"));
2368        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
2369        assert!(py.contains("def first_name(self) -> str:"));
2370        assert!(py.contains("def email(self) -> Optional[str]:"));
2371        assert!(py.contains("def contact_type(self) -> \"ContactType\":"));
2372
2373        assert!(py.contains("def create_contact("));
2374        assert!(py.contains("weaveffi_contacts_create_contact"));
2375        assert!(py.contains("def get_contact(id: int) -> \"Contact\":"));
2376        assert!(py.contains("def list_contacts() -> List[\"Contact\"]:"));
2377        assert!(py.contains("def count_contacts() -> int:"));
2378
2379        let _ = std::fs::remove_dir_all(&tmp);
2380    }
2381
2382    #[test]
2383    fn type_hint_mapping() {
2384        assert_eq!(py_type_hint(&TypeRef::I32), "int");
2385        assert_eq!(py_type_hint(&TypeRef::U32), "int");
2386        assert_eq!(py_type_hint(&TypeRef::I64), "int");
2387        assert_eq!(py_type_hint(&TypeRef::F64), "float");
2388        assert_eq!(py_type_hint(&TypeRef::Bool), "bool");
2389        assert_eq!(py_type_hint(&TypeRef::StringUtf8), "str");
2390        assert_eq!(py_type_hint(&TypeRef::Bytes), "bytes");
2391        assert_eq!(py_type_hint(&TypeRef::Handle), "int");
2392        assert_eq!(py_type_hint(&TypeRef::Struct("Foo".into())), "\"Foo\"");
2393        assert_eq!(py_type_hint(&TypeRef::Enum("Bar".into())), "\"Bar\"");
2394        assert_eq!(
2395            py_type_hint(&TypeRef::Optional(Box::new(TypeRef::I32))),
2396            "Optional[int]"
2397        );
2398        assert_eq!(
2399            py_type_hint(&TypeRef::List(Box::new(TypeRef::I32))),
2400            "List[int]"
2401        );
2402        assert_eq!(
2403            py_type_hint(&TypeRef::Map(
2404                Box::new(TypeRef::StringUtf8),
2405                Box::new(TypeRef::I32)
2406            )),
2407            "Dict[str, int]"
2408        );
2409    }
2410
2411    #[test]
2412    fn ctypes_scalar_mapping() {
2413        assert_eq!(py_ctypes_scalar(&TypeRef::I32), "ctypes.c_int32");
2414        assert_eq!(py_ctypes_scalar(&TypeRef::U32), "ctypes.c_uint32");
2415        assert_eq!(py_ctypes_scalar(&TypeRef::I64), "ctypes.c_int64");
2416        assert_eq!(py_ctypes_scalar(&TypeRef::F64), "ctypes.c_double");
2417        assert_eq!(py_ctypes_scalar(&TypeRef::Bool), "ctypes.c_int32");
2418        assert_eq!(py_ctypes_scalar(&TypeRef::StringUtf8), "ctypes.c_char_p");
2419        assert_eq!(py_ctypes_scalar(&TypeRef::Handle), "ctypes.c_uint64");
2420        assert_eq!(py_ctypes_scalar(&TypeRef::Bytes), "ctypes.c_uint8");
2421        assert_eq!(
2422            py_ctypes_scalar(&TypeRef::Struct("X".into())),
2423            "ctypes.c_void_p"
2424        );
2425        assert_eq!(
2426            py_ctypes_scalar(&TypeRef::Enum("X".into())),
2427            "ctypes.c_int32"
2428        );
2429    }
2430
2431    #[test]
2432    fn list_struct_return() {
2433        let api = make_api(vec![Module {
2434            name: "store".into(),
2435            functions: vec![Function {
2436                name: "list_items".into(),
2437                params: vec![],
2438                returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Item".into())))),
2439                doc: None,
2440                r#async: false,
2441                cancellable: false,
2442                deprecated: None,
2443                since: None,
2444            }],
2445            structs: vec![],
2446            enums: vec![],
2447            callbacks: vec![],
2448            listeners: vec![],
2449            errors: None,
2450            modules: vec![],
2451        }]);
2452
2453        let py = render_python_module(&api, true);
2454        assert!(
2455            py.contains("-> List[\"Item\"]:"),
2456            "missing list struct return: {py}"
2457        );
2458        assert!(
2459            py.contains("Item(_result[_i])"),
2460            "missing struct wrapping in list: {py}"
2461        );
2462    }
2463
2464    #[test]
2465    fn struct_bytes_field_getter() {
2466        let api = make_api(vec![Module {
2467            name: "storage".into(),
2468            functions: vec![],
2469            structs: vec![StructDef {
2470                name: "Blob".into(),
2471                doc: None,
2472                fields: vec![StructField {
2473                    name: "data".into(),
2474                    ty: TypeRef::Bytes,
2475                    doc: None,
2476                    default: None,
2477                }],
2478                builder: false,
2479            }],
2480            enums: vec![],
2481            callbacks: vec![],
2482            listeners: vec![],
2483            errors: None,
2484            modules: vec![],
2485        }]);
2486
2487        let py = render_python_module(&api, true);
2488        assert!(
2489            py.contains("def data(self) -> bytes:"),
2490            "missing bytes getter: {py}"
2491        );
2492        assert!(
2493            py.contains("_out_len = ctypes.c_size_t(0)"),
2494            "missing out_len in bytes getter: {py}"
2495        );
2496        assert!(
2497            py.contains("_result[:_out_len.value]"),
2498            "missing bytes slice: {py}"
2499        );
2500    }
2501
2502    #[test]
2503    fn python_generates_type_stubs() {
2504        let api = make_api(vec![Module {
2505            name: "contacts".into(),
2506            enums: vec![EnumDef {
2507                name: "ContactType".into(),
2508                doc: None,
2509                variants: vec![
2510                    EnumVariant {
2511                        name: "Personal".into(),
2512                        value: 0,
2513                        doc: None,
2514                    },
2515                    EnumVariant {
2516                        name: "Work".into(),
2517                        value: 1,
2518                        doc: None,
2519                    },
2520                ],
2521            }],
2522            callbacks: vec![],
2523            listeners: vec![],
2524            structs: vec![StructDef {
2525                name: "Contact".into(),
2526                doc: None,
2527                fields: vec![
2528                    StructField {
2529                        name: "id".into(),
2530                        ty: TypeRef::I64,
2531                        doc: None,
2532                        default: None,
2533                    },
2534                    StructField {
2535                        name: "name".into(),
2536                        ty: TypeRef::StringUtf8,
2537                        doc: None,
2538                        default: None,
2539                    },
2540                    StructField {
2541                        name: "email".into(),
2542                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2543                        doc: None,
2544                        default: None,
2545                    },
2546                    StructField {
2547                        name: "tags".into(),
2548                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
2549                        doc: None,
2550                        default: None,
2551                    },
2552                    StructField {
2553                        name: "metadata".into(),
2554                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
2555                        doc: None,
2556                        default: None,
2557                    },
2558                ],
2559                builder: false,
2560            }],
2561            functions: vec![
2562                Function {
2563                    name: "create_contact".into(),
2564                    params: vec![
2565                        Param {
2566                            name: "name".into(),
2567                            ty: TypeRef::StringUtf8,
2568                            mutable: false,
2569                        },
2570                        Param {
2571                            name: "email".into(),
2572                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2573                            mutable: false,
2574                        },
2575                    ],
2576                    returns: Some(TypeRef::Handle),
2577                    doc: None,
2578                    r#async: false,
2579                    cancellable: false,
2580                    deprecated: None,
2581                    since: None,
2582                },
2583                Function {
2584                    name: "get_contact".into(),
2585                    params: vec![Param {
2586                        name: "id".into(),
2587                        ty: TypeRef::Handle,
2588                        mutable: false,
2589                    }],
2590                    returns: Some(TypeRef::Struct("Contact".into())),
2591                    doc: None,
2592                    r#async: false,
2593                    cancellable: false,
2594                    deprecated: None,
2595                    since: None,
2596                },
2597                Function {
2598                    name: "delete_contact".into(),
2599                    params: vec![Param {
2600                        name: "id".into(),
2601                        ty: TypeRef::Handle,
2602                        mutable: false,
2603                    }],
2604                    returns: None,
2605                    doc: None,
2606                    r#async: false,
2607                    cancellable: false,
2608                    deprecated: None,
2609                    since: None,
2610                },
2611            ],
2612            errors: None,
2613            modules: vec![],
2614        }]);
2615
2616        let tmp = std::env::temp_dir().join("weaveffi_test_python_pyi");
2617        let _ = std::fs::remove_dir_all(&tmp);
2618        std::fs::create_dir_all(&tmp).unwrap();
2619        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2620
2621        PythonGenerator.generate(&api, out_dir).unwrap();
2622
2623        let pyi_path = tmp.join("python/weaveffi/weaveffi.pyi");
2624        assert!(pyi_path.exists(), ".pyi file must exist");
2625
2626        let pyi = std::fs::read_to_string(&pyi_path).unwrap();
2627
2628        assert!(
2629            pyi.contains("from enum import IntEnum"),
2630            "missing IntEnum import"
2631        );
2632        assert!(
2633            pyi.contains("from typing import Dict, Iterator, List, Optional"),
2634            "missing typing imports"
2635        );
2636
2637        assert!(
2638            pyi.contains("class ContactType(IntEnum):"),
2639            "missing enum stub"
2640        );
2641        assert!(
2642            pyi.contains("    Personal: int"),
2643            "missing enum variant Personal"
2644        );
2645        assert!(pyi.contains("    Work: int"), "missing enum variant Work");
2646
2647        assert!(pyi.contains("class Contact:"), "missing struct stub");
2648        assert!(
2649            pyi.contains("    def id(self) -> int: ..."),
2650            "missing id property: {pyi}"
2651        );
2652        assert!(
2653            pyi.contains("    def name(self) -> str: ..."),
2654            "missing name property: {pyi}"
2655        );
2656        assert!(
2657            pyi.contains("    def email(self) -> Optional[str]: ..."),
2658            "missing email property: {pyi}"
2659        );
2660        assert!(
2661            pyi.contains("    def tags(self) -> List[str]: ..."),
2662            "missing tags property: {pyi}"
2663        );
2664        assert!(
2665            pyi.contains("    def metadata(self) -> Dict[str, int]: ..."),
2666            "missing metadata property: {pyi}"
2667        );
2668
2669        assert!(
2670            pyi.contains("def create_contact(name: str, email: Optional[str]) -> int: ..."),
2671            "missing create_contact stub: {pyi}"
2672        );
2673        assert!(
2674            pyi.contains("def get_contact(id: int) -> \"Contact\": ..."),
2675            "missing get_contact stub: {pyi}"
2676        );
2677        assert!(
2678            pyi.contains("def delete_contact(id: int) -> None: ..."),
2679            "missing delete_contact stub: {pyi}"
2680        );
2681
2682        let _ = std::fs::remove_dir_all(&tmp);
2683    }
2684
2685    #[test]
2686    fn generate_python_basic() {
2687        let api = make_api(vec![simple_module(vec![Function {
2688            name: "add".into(),
2689            params: vec![
2690                Param {
2691                    name: "a".into(),
2692                    ty: TypeRef::I32,
2693                    mutable: false,
2694                },
2695                Param {
2696                    name: "b".into(),
2697                    ty: TypeRef::I32,
2698                    mutable: false,
2699                },
2700            ],
2701            returns: Some(TypeRef::I32),
2702            doc: None,
2703            r#async: false,
2704            cancellable: false,
2705            deprecated: None,
2706            since: None,
2707        }])]);
2708
2709        let tmp = std::env::temp_dir().join("weaveffi_test_py_basic");
2710        let _ = std::fs::remove_dir_all(&tmp);
2711        std::fs::create_dir_all(&tmp).unwrap();
2712        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2713
2714        PythonGenerator.generate(&api, out_dir).unwrap();
2715
2716        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
2717
2718        assert!(py.contains("def add(a: int, b: int) -> int:"));
2719        assert!(py.contains("_fn = _lib.weaveffi_math_add"));
2720        assert!(py.contains("ctypes.c_int32, ctypes.c_int32"));
2721        assert!(py.contains("_fn.restype = ctypes.c_int32"));
2722        assert!(py.contains("_err = _WeaveffiErrorStruct()"));
2723        assert!(py.contains("_check_error(_err)"));
2724        assert!(py.contains("return _result"));
2725
2726        assert!(py.contains("import ctypes"));
2727        assert!(py.contains("from enum import IntEnum"));
2728        assert!(py.contains("from typing import Dict, Iterator, List, Optional"));
2729        assert!(py.contains("class WeaveffiError(Exception):"));
2730        assert!(py.contains("def _load_library()"));
2731        assert!(py.contains("_lib = _load_library()"));
2732
2733        let _ = std::fs::remove_dir_all(&tmp);
2734    }
2735
2736    #[test]
2737    fn generate_python_with_structs() {
2738        let api = make_api(vec![Module {
2739            name: "contacts".into(),
2740            functions: vec![],
2741            structs: vec![StructDef {
2742                name: "Contact".into(),
2743                doc: Some("A contact record".into()),
2744                fields: vec![
2745                    StructField {
2746                        name: "id".into(),
2747                        ty: TypeRef::I64,
2748                        doc: None,
2749                        default: None,
2750                    },
2751                    StructField {
2752                        name: "first_name".into(),
2753                        ty: TypeRef::StringUtf8,
2754                        doc: None,
2755                        default: None,
2756                    },
2757                    StructField {
2758                        name: "last_name".into(),
2759                        ty: TypeRef::StringUtf8,
2760                        doc: None,
2761                        default: None,
2762                    },
2763                    StructField {
2764                        name: "email".into(),
2765                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2766                        doc: None,
2767                        default: None,
2768                    },
2769                ],
2770                builder: false,
2771            }],
2772            enums: vec![],
2773            callbacks: vec![],
2774            listeners: vec![],
2775            errors: None,
2776            modules: vec![],
2777        }]);
2778
2779        let py = render_python_module(&api, true);
2780
2781        assert!(py.contains("class Contact:"), "missing class decl");
2782        assert!(
2783            py.contains("\"\"\"A contact record\"\"\""),
2784            "missing doc: {py}"
2785        );
2786        assert!(py.contains("def __init__(self, _ptr: int) -> None:"));
2787        assert!(py.contains("self._ptr = _ptr"));
2788        assert!(py.contains("def __del__(self) -> None:"));
2789        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
2790
2791        assert!(py.contains("@property\n    def id(self) -> int:"));
2792        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
2793        assert!(py.contains("_fn.restype = ctypes.c_int64"));
2794
2795        assert!(py.contains("@property\n    def first_name(self) -> str:"));
2796        assert!(py.contains("weaveffi_contacts_Contact_get_first_name"));
2797
2798        assert!(py.contains("@property\n    def last_name(self) -> str:"));
2799        assert!(py.contains("weaveffi_contacts_Contact_get_last_name"));
2800
2801        assert!(py.contains("@property\n    def email(self) -> Optional[str]:"));
2802        assert!(py.contains("weaveffi_contacts_Contact_get_email"));
2803    }
2804
2805    #[test]
2806    fn generate_python_with_enums() {
2807        let api = make_api(vec![Module {
2808            name: "contacts".into(),
2809            functions: vec![Function {
2810                name: "get_type".into(),
2811                params: vec![Param {
2812                    name: "ct".into(),
2813                    ty: TypeRef::Enum("ContactType".into()),
2814                    mutable: false,
2815                }],
2816                returns: Some(TypeRef::Enum("ContactType".into())),
2817                doc: None,
2818                r#async: false,
2819                cancellable: false,
2820                deprecated: None,
2821                since: None,
2822            }],
2823            structs: vec![],
2824            enums: vec![EnumDef {
2825                name: "ContactType".into(),
2826                doc: Some("Type of contact".into()),
2827                variants: vec![
2828                    EnumVariant {
2829                        name: "Personal".into(),
2830                        value: 0,
2831                        doc: None,
2832                    },
2833                    EnumVariant {
2834                        name: "Work".into(),
2835                        value: 1,
2836                        doc: None,
2837                    },
2838                    EnumVariant {
2839                        name: "Other".into(),
2840                        value: 2,
2841                        doc: None,
2842                    },
2843                ],
2844            }],
2845            callbacks: vec![],
2846            listeners: vec![],
2847            errors: None,
2848            modules: vec![],
2849        }]);
2850
2851        let py = render_python_module(&api, true);
2852
2853        assert!(py.contains("class ContactType(IntEnum):"));
2854        assert!(py.contains("\"\"\"Type of contact\"\"\""));
2855        assert!(py.contains("Personal = 0"));
2856        assert!(py.contains("Work = 1"));
2857        assert!(py.contains("Other = 2"));
2858
2859        assert!(
2860            py.contains("ct: \"ContactType\""),
2861            "missing enum param hint"
2862        );
2863        assert!(
2864            py.contains("-> \"ContactType\":"),
2865            "missing enum return hint"
2866        );
2867        assert!(py.contains("ct.value"), "missing .value for enum param");
2868        assert!(
2869            py.contains("return ContactType(_result)"),
2870            "missing enum return wrap"
2871        );
2872        assert!(py.contains("ctypes.c_int32"), "enum should use c_int32 ABI");
2873    }
2874
2875    #[test]
2876    fn generate_python_with_optionals() {
2877        let api = make_api(vec![Module {
2878            name: "store".into(),
2879            functions: vec![
2880                Function {
2881                    name: "find_int".into(),
2882                    params: vec![Param {
2883                        name: "key".into(),
2884                        ty: TypeRef::Optional(Box::new(TypeRef::I32)),
2885                        mutable: false,
2886                    }],
2887                    returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))),
2888                    doc: None,
2889                    r#async: false,
2890                    cancellable: false,
2891                    deprecated: None,
2892                    since: None,
2893                },
2894                Function {
2895                    name: "find_name".into(),
2896                    params: vec![Param {
2897                        name: "prefix".into(),
2898                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2899                        mutable: false,
2900                    }],
2901                    returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))),
2902                    doc: None,
2903                    r#async: false,
2904                    cancellable: false,
2905                    deprecated: None,
2906                    since: None,
2907                },
2908                Function {
2909                    name: "find_contact".into(),
2910                    params: vec![],
2911                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
2912                        "Contact".into(),
2913                    )))),
2914                    doc: None,
2915                    r#async: false,
2916                    cancellable: false,
2917                    deprecated: None,
2918                    since: None,
2919                },
2920                Function {
2921                    name: "find_flag".into(),
2922                    params: vec![],
2923                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Bool))),
2924                    doc: None,
2925                    r#async: false,
2926                    cancellable: false,
2927                    deprecated: None,
2928                    since: None,
2929                },
2930            ],
2931            structs: vec![],
2932            enums: vec![],
2933            callbacks: vec![],
2934            listeners: vec![],
2935            errors: None,
2936            modules: vec![],
2937        }]);
2938
2939        let py = render_python_module(&api, true);
2940
2941        assert!(
2942            py.contains("key: Optional[int]"),
2943            "missing Optional[int] param"
2944        );
2945        assert!(
2946            py.contains("-> Optional[int]:"),
2947            "missing Optional[int] return"
2948        );
2949        assert!(
2950            py.contains("ctypes.byref(ctypes.c_int32(key)) if key is not None else None"),
2951            "missing optional i32 conversion"
2952        );
2953        assert!(
2954            py.contains("ctypes.POINTER(ctypes.c_int32)"),
2955            "missing POINTER for optional i32"
2956        );
2957
2958        assert!(
2959            py.contains("prefix: Optional[str]"),
2960            "missing Optional[str] param"
2961        );
2962        assert!(
2963            py.contains("-> Optional[str]:"),
2964            "missing Optional[str] return"
2965        );
2966        assert!(
2967            py.contains("_string_to_bytes(prefix)"),
2968            "missing optional _string_to_bytes"
2969        );
2970
2971        assert!(
2972            py.contains("-> Optional[\"Contact\"]:"),
2973            "missing Optional struct return"
2974        );
2975        assert!(
2976            py.contains("if _result is None:\n        return None\n    return Contact(_result)"),
2977            "missing optional struct None check"
2978        );
2979
2980        assert!(
2981            py.contains("-> Optional[bool]:"),
2982            "missing Optional[bool] return"
2983        );
2984        assert!(
2985            py.contains("return bool(_result[0])"),
2986            "missing optional bool deref"
2987        );
2988    }
2989
2990    #[test]
2991    fn generate_python_with_lists() {
2992        let api = make_api(vec![Module {
2993            name: "batch".into(),
2994            functions: vec![
2995                Function {
2996                    name: "process_ids".into(),
2997                    params: vec![Param {
2998                        name: "ids".into(),
2999                        ty: TypeRef::List(Box::new(TypeRef::I32)),
3000                        mutable: false,
3001                    }],
3002                    returns: None,
3003                    doc: None,
3004                    r#async: false,
3005                    cancellable: false,
3006                    deprecated: None,
3007                    since: None,
3008                },
3009                Function {
3010                    name: "get_names".into(),
3011                    params: vec![],
3012                    returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
3013                    doc: None,
3014                    r#async: false,
3015                    cancellable: false,
3016                    deprecated: None,
3017                    since: None,
3018                },
3019                Function {
3020                    name: "get_items".into(),
3021                    params: vec![],
3022                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Item".into())))),
3023                    doc: None,
3024                    r#async: false,
3025                    cancellable: false,
3026                    deprecated: None,
3027                    since: None,
3028                },
3029            ],
3030            structs: vec![],
3031            enums: vec![],
3032            callbacks: vec![],
3033            listeners: vec![],
3034            errors: None,
3035            modules: vec![],
3036        }]);
3037
3038        let py = render_python_module(&api, true);
3039
3040        assert!(py.contains("ids: List[int]"), "missing List[int] param");
3041        assert!(
3042            py.contains("(ctypes.c_int32 * len(ids))(*ids)"),
3043            "missing list-to-array conversion"
3044        );
3045        assert!(
3046            py.contains("ctypes.POINTER(ctypes.c_int32)"),
3047            "missing POINTER for list param"
3048        );
3049        assert!(py.contains("ctypes.c_size_t"), "missing size_t for length");
3050
3051        assert!(
3052            py.contains("-> List[str]:"),
3053            "missing List[str] return: {py}"
3054        );
3055        assert!(
3056            py.contains("_bytes_to_string(_result[_i]) for _i in range(_out_len.value)"),
3057            "missing string list _bytes_to_string: {py}"
3058        );
3059
3060        assert!(
3061            py.contains("-> List[\"Item\"]:"),
3062            "missing List struct return"
3063        );
3064        assert!(
3065            py.contains("Item(_result[_i]) for _i in range(_out_len.value)"),
3066            "missing struct wrapping in list"
3067        );
3068    }
3069
3070    #[test]
3071    fn generate_python_with_maps() {
3072        let api = make_api(vec![Module {
3073            name: "config".into(),
3074            functions: vec![
3075                Function {
3076                    name: "set_config".into(),
3077                    params: vec![Param {
3078                        name: "settings".into(),
3079                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
3080                        mutable: false,
3081                    }],
3082                    returns: None,
3083                    doc: None,
3084                    r#async: false,
3085                    cancellable: false,
3086                    deprecated: None,
3087                    since: None,
3088                },
3089                Function {
3090                    name: "get_config".into(),
3091                    params: vec![],
3092                    returns: Some(TypeRef::Map(
3093                        Box::new(TypeRef::StringUtf8),
3094                        Box::new(TypeRef::I32),
3095                    )),
3096                    doc: None,
3097                    r#async: false,
3098                    cancellable: false,
3099                    deprecated: None,
3100                    since: None,
3101                },
3102            ],
3103            structs: vec![],
3104            enums: vec![],
3105            callbacks: vec![],
3106            listeners: vec![],
3107            errors: None,
3108            modules: vec![],
3109        }]);
3110
3111        let py = render_python_module(&api, true);
3112
3113        assert!(
3114            py.contains("settings: Dict[str, int]"),
3115            "missing Dict param hint"
3116        );
3117        assert!(
3118            py.contains("list(settings.keys())"),
3119            "missing keys extraction"
3120        );
3121        assert!(
3122            py.contains("_settings_vals = [settings[_k] for _k in _settings_keys]"),
3123            "missing values extraction"
3124        );
3125        assert!(
3126            py.contains("ctypes.c_char_p * len(_settings_keys)"),
3127            "missing key array creation"
3128        );
3129        assert!(
3130            py.contains("ctypes.c_int32 * len(_settings_vals)"),
3131            "missing value array creation"
3132        );
3133
3134        assert!(
3135            py.contains("-> Dict[str, int]:"),
3136            "missing Dict return hint"
3137        );
3138        assert!(
3139            py.contains("_out_keys = ctypes.POINTER(ctypes.c_char_p)()"),
3140            "missing out_keys init"
3141        );
3142        assert!(
3143            py.contains("_out_values = ctypes.POINTER(ctypes.c_int32)()"),
3144            "missing out_values init"
3145        );
3146        assert!(
3147            py.contains("_out_len = ctypes.c_size_t(0)"),
3148            "missing out_len init"
3149        );
3150        assert!(
3151            py.contains("if not _out_keys or not _out_values:"),
3152            "missing empty map check"
3153        );
3154        assert!(
3155            py.contains("_bytes_to_string(_out_keys[_i]): _out_values[_i]"),
3156            "missing map comprehension"
3157        );
3158    }
3159
3160    #[test]
3161    fn generate_python_pyi_types() {
3162        let api = make_api(vec![Module {
3163            name: "contacts".into(),
3164            enums: vec![EnumDef {
3165                name: "ContactType".into(),
3166                doc: None,
3167                variants: vec![
3168                    EnumVariant {
3169                        name: "Personal".into(),
3170                        value: 0,
3171                        doc: None,
3172                    },
3173                    EnumVariant {
3174                        name: "Work".into(),
3175                        value: 1,
3176                        doc: None,
3177                    },
3178                    EnumVariant {
3179                        name: "Other".into(),
3180                        value: 2,
3181                        doc: None,
3182                    },
3183                ],
3184            }],
3185            callbacks: vec![],
3186            listeners: vec![],
3187            structs: vec![StructDef {
3188                name: "Contact".into(),
3189                doc: None,
3190                fields: vec![
3191                    StructField {
3192                        name: "id".into(),
3193                        ty: TypeRef::I64,
3194                        doc: None,
3195                        default: None,
3196                    },
3197                    StructField {
3198                        name: "first_name".into(),
3199                        ty: TypeRef::StringUtf8,
3200                        doc: None,
3201                        default: None,
3202                    },
3203                    StructField {
3204                        name: "email".into(),
3205                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3206                        doc: None,
3207                        default: None,
3208                    },
3209                    StructField {
3210                        name: "tags".into(),
3211                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
3212                        doc: None,
3213                        default: None,
3214                    },
3215                    StructField {
3216                        name: "scores".into(),
3217                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
3218                        doc: None,
3219                        default: None,
3220                    },
3221                ],
3222                builder: false,
3223            }],
3224            functions: vec![
3225                Function {
3226                    name: "create_contact".into(),
3227                    params: vec![
3228                        Param {
3229                            name: "name".into(),
3230                            ty: TypeRef::StringUtf8,
3231                            mutable: false,
3232                        },
3233                        Param {
3234                            name: "email".into(),
3235                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3236                            mutable: false,
3237                        },
3238                    ],
3239                    returns: Some(TypeRef::Handle),
3240                    doc: None,
3241                    r#async: false,
3242                    cancellable: false,
3243                    deprecated: None,
3244                    since: None,
3245                },
3246                Function {
3247                    name: "get_contact".into(),
3248                    params: vec![Param {
3249                        name: "id".into(),
3250                        ty: TypeRef::Handle,
3251                        mutable: false,
3252                    }],
3253                    returns: Some(TypeRef::Struct("Contact".into())),
3254                    doc: None,
3255                    r#async: false,
3256                    cancellable: false,
3257                    deprecated: None,
3258                    since: None,
3259                },
3260                Function {
3261                    name: "list_contacts".into(),
3262                    params: vec![],
3263                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
3264                    doc: None,
3265                    r#async: false,
3266                    cancellable: false,
3267                    deprecated: None,
3268                    since: None,
3269                },
3270                Function {
3271                    name: "delete_contact".into(),
3272                    params: vec![Param {
3273                        name: "id".into(),
3274                        ty: TypeRef::Handle,
3275                        mutable: false,
3276                    }],
3277                    returns: None,
3278                    doc: None,
3279                    r#async: false,
3280                    cancellable: false,
3281                    deprecated: None,
3282                    since: None,
3283                },
3284            ],
3285            errors: None,
3286            modules: vec![],
3287        }]);
3288
3289        let pyi = render_pyi_module(&api, true);
3290
3291        assert!(pyi.contains("from enum import IntEnum"));
3292        assert!(pyi.contains("from typing import Dict, Iterator, List, Optional"));
3293
3294        assert!(pyi.contains("class ContactType(IntEnum):"));
3295        assert!(pyi.contains("    Personal: int"));
3296        assert!(pyi.contains("    Work: int"));
3297        assert!(pyi.contains("    Other: int"));
3298
3299        assert!(pyi.contains("class Contact:"));
3300        assert!(pyi.contains("    def id(self) -> int: ..."));
3301        assert!(pyi.contains("    def first_name(self) -> str: ..."));
3302        assert!(pyi.contains("    def email(self) -> Optional[str]: ..."));
3303        assert!(pyi.contains("    def tags(self) -> List[str]: ..."));
3304        assert!(pyi.contains("    def scores(self) -> Dict[str, int]: ..."));
3305
3306        assert!(pyi.contains("def create_contact(name: str, email: Optional[str]) -> int: ..."));
3307        assert!(pyi.contains("def get_contact(id: int) -> \"Contact\": ..."));
3308        assert!(pyi.contains("def list_contacts() -> List[\"Contact\"]: ..."));
3309        assert!(pyi.contains("def delete_contact(id: int) -> None: ..."));
3310    }
3311
3312    #[test]
3313    fn generate_python_full_contacts() {
3314        let api = make_api(vec![Module {
3315            name: "contacts".into(),
3316            enums: vec![EnumDef {
3317                name: "ContactType".into(),
3318                doc: None,
3319                variants: vec![
3320                    EnumVariant {
3321                        name: "Personal".into(),
3322                        value: 0,
3323                        doc: None,
3324                    },
3325                    EnumVariant {
3326                        name: "Work".into(),
3327                        value: 1,
3328                        doc: None,
3329                    },
3330                    EnumVariant {
3331                        name: "Other".into(),
3332                        value: 2,
3333                        doc: None,
3334                    },
3335                ],
3336            }],
3337            callbacks: vec![],
3338            listeners: vec![],
3339            structs: vec![StructDef {
3340                name: "Contact".into(),
3341                doc: None,
3342                fields: vec![
3343                    StructField {
3344                        name: "id".into(),
3345                        ty: TypeRef::I64,
3346                        doc: None,
3347                        default: None,
3348                    },
3349                    StructField {
3350                        name: "first_name".into(),
3351                        ty: TypeRef::StringUtf8,
3352                        doc: None,
3353                        default: None,
3354                    },
3355                    StructField {
3356                        name: "last_name".into(),
3357                        ty: TypeRef::StringUtf8,
3358                        doc: None,
3359                        default: None,
3360                    },
3361                    StructField {
3362                        name: "email".into(),
3363                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3364                        doc: None,
3365                        default: None,
3366                    },
3367                    StructField {
3368                        name: "contact_type".into(),
3369                        ty: TypeRef::Enum("ContactType".into()),
3370                        doc: None,
3371                        default: None,
3372                    },
3373                ],
3374                builder: false,
3375            }],
3376            functions: vec![
3377                Function {
3378                    name: "create_contact".into(),
3379                    params: vec![
3380                        Param {
3381                            name: "first_name".into(),
3382                            ty: TypeRef::StringUtf8,
3383                            mutable: false,
3384                        },
3385                        Param {
3386                            name: "last_name".into(),
3387                            ty: TypeRef::StringUtf8,
3388                            mutable: false,
3389                        },
3390                        Param {
3391                            name: "email".into(),
3392                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3393                            mutable: false,
3394                        },
3395                        Param {
3396                            name: "contact_type".into(),
3397                            ty: TypeRef::Enum("ContactType".into()),
3398                            mutable: false,
3399                        },
3400                    ],
3401                    returns: Some(TypeRef::Handle),
3402                    doc: None,
3403                    r#async: false,
3404                    cancellable: false,
3405                    deprecated: None,
3406                    since: None,
3407                },
3408                Function {
3409                    name: "get_contact".into(),
3410                    params: vec![Param {
3411                        name: "id".into(),
3412                        ty: TypeRef::Handle,
3413                        mutable: false,
3414                    }],
3415                    returns: Some(TypeRef::Struct("Contact".into())),
3416                    doc: None,
3417                    r#async: false,
3418                    cancellable: false,
3419                    deprecated: None,
3420                    since: None,
3421                },
3422                Function {
3423                    name: "list_contacts".into(),
3424                    params: vec![],
3425                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
3426                    doc: None,
3427                    r#async: false,
3428                    cancellable: false,
3429                    deprecated: None,
3430                    since: None,
3431                },
3432                Function {
3433                    name: "delete_contact".into(),
3434                    params: vec![Param {
3435                        name: "id".into(),
3436                        ty: TypeRef::Handle,
3437                        mutable: false,
3438                    }],
3439                    returns: Some(TypeRef::Bool),
3440                    doc: None,
3441                    r#async: false,
3442                    cancellable: false,
3443                    deprecated: None,
3444                    since: None,
3445                },
3446                Function {
3447                    name: "count_contacts".into(),
3448                    params: vec![],
3449                    returns: Some(TypeRef::I32),
3450                    doc: None,
3451                    r#async: false,
3452                    cancellable: false,
3453                    deprecated: None,
3454                    since: None,
3455                },
3456            ],
3457            errors: None,
3458            modules: vec![],
3459        }]);
3460
3461        let tmp = std::env::temp_dir().join("weaveffi_test_py_full_contacts");
3462        let _ = std::fs::remove_dir_all(&tmp);
3463        std::fs::create_dir_all(&tmp).unwrap();
3464        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
3465
3466        PythonGenerator.generate(&api, out_dir).unwrap();
3467
3468        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
3469        let pyi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.pyi")).unwrap();
3470
3471        assert!(py.contains("class ContactType(IntEnum):"));
3472        assert!(py.contains("Personal = 0"));
3473        assert!(py.contains("Work = 1"));
3474        assert!(py.contains("Other = 2"));
3475
3476        assert!(py.contains("class Contact:"));
3477        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
3478        assert!(py.contains("@property\n    def id(self) -> int:"));
3479        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
3480        assert!(py.contains("@property\n    def first_name(self) -> str:"));
3481        assert!(py.contains("weaveffi_contacts_Contact_get_first_name"));
3482        assert!(py.contains("@property\n    def last_name(self) -> str:"));
3483        assert!(py.contains("weaveffi_contacts_Contact_get_last_name"));
3484        assert!(py.contains("@property\n    def email(self) -> Optional[str]:"));
3485        assert!(py.contains("weaveffi_contacts_Contact_get_email"));
3486        assert!(py.contains("@property\n    def contact_type(self) -> \"ContactType\":"));
3487        assert!(py.contains("weaveffi_contacts_Contact_get_contact_type"));
3488        assert!(py.contains("return ContactType(_result)"));
3489
3490        assert!(py.contains("def create_contact("));
3491        assert!(py.contains("first_name: str"));
3492        assert!(py.contains("last_name: str"));
3493        assert!(py.contains("email: Optional[str]"));
3494        assert!(py.contains("contact_type: \"ContactType\""));
3495        assert!(py.contains("-> int:"));
3496        assert!(py.contains("weaveffi_contacts_create_contact"));
3497        assert!(py.contains("_string_to_bytes(first_name)"));
3498        assert!(py.contains("contact_type.value"));
3499
3500        assert!(py.contains("def get_contact(id: int) -> \"Contact\":"));
3501        assert!(py.contains("weaveffi_contacts_get_contact"));
3502        assert!(py.contains("return Contact(_result)"));
3503
3504        assert!(py.contains("def list_contacts() -> List[\"Contact\"]:"));
3505        assert!(py.contains("weaveffi_contacts_list_contacts"));
3506        assert!(py.contains("Contact(_result[_i]) for _i in range(_out_len.value)"));
3507
3508        assert!(py.contains("def delete_contact(id: int) -> bool:"));
3509        assert!(py.contains("weaveffi_contacts_delete_contact"));
3510        assert!(py.contains("return bool(_result)"));
3511
3512        assert!(py.contains("def count_contacts() -> int:"));
3513        assert!(py.contains("weaveffi_contacts_count_contacts"));
3514
3515        assert!(pyi.contains("class ContactType(IntEnum):"));
3516        assert!(pyi.contains("    Personal: int"));
3517        assert!(pyi.contains("    Work: int"));
3518        assert!(pyi.contains("    Other: int"));
3519        assert!(pyi.contains("class Contact:"));
3520        assert!(pyi.contains("def create_contact("));
3521        assert!(pyi.contains("def get_contact(id: int) -> \"Contact\": ..."));
3522        assert!(pyi.contains("def list_contacts() -> List[\"Contact\"]: ..."));
3523        assert!(pyi.contains("def delete_contact(id: int) -> bool: ..."));
3524        assert!(pyi.contains("def count_contacts() -> int: ..."));
3525
3526        let _ = std::fs::remove_dir_all(&tmp);
3527    }
3528
3529    #[test]
3530    fn python_generates_packaging() {
3531        let api = make_api(vec![simple_module(vec![Function {
3532            name: "add".into(),
3533            params: vec![
3534                Param {
3535                    name: "a".into(),
3536                    ty: TypeRef::I32,
3537                    mutable: false,
3538                },
3539                Param {
3540                    name: "b".into(),
3541                    ty: TypeRef::I32,
3542                    mutable: false,
3543                },
3544            ],
3545            returns: Some(TypeRef::I32),
3546            doc: None,
3547            r#async: false,
3548            cancellable: false,
3549            deprecated: None,
3550            since: None,
3551        }])]);
3552
3553        let tmp = std::env::temp_dir().join("weaveffi_test_python_packaging");
3554        let _ = std::fs::remove_dir_all(&tmp);
3555        std::fs::create_dir_all(&tmp).unwrap();
3556        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
3557
3558        PythonGenerator.generate(&api, out_dir).unwrap();
3559
3560        let pyproject = std::fs::read_to_string(tmp.join("python/pyproject.toml")).unwrap();
3561        assert!(
3562            pyproject.contains("[build-system]"),
3563            "missing build-system: {pyproject}"
3564        );
3565        assert!(
3566            pyproject.contains("setuptools"),
3567            "missing setuptools: {pyproject}"
3568        );
3569        assert!(
3570            pyproject.contains("[project]"),
3571            "missing project section: {pyproject}"
3572        );
3573        assert!(
3574            pyproject.contains("name = \"weaveffi\""),
3575            "missing project name: {pyproject}"
3576        );
3577        assert!(
3578            pyproject.contains("version = \"0.1.0\""),
3579            "missing version: {pyproject}"
3580        );
3581        assert!(
3582            pyproject.contains("[tool.setuptools]"),
3583            "missing tool.setuptools: {pyproject}"
3584        );
3585        assert!(
3586            pyproject.contains("packages = [\"weaveffi\"]"),
3587            "missing packages list: {pyproject}"
3588        );
3589
3590        let setup = std::fs::read_to_string(tmp.join("python/setup.py")).unwrap();
3591        assert!(
3592            setup.contains("from setuptools import setup"),
3593            "missing setuptools import: {setup}"
3594        );
3595        assert!(
3596            setup.contains("name=\"weaveffi\""),
3597            "missing package name: {setup}"
3598        );
3599
3600        let readme = std::fs::read_to_string(tmp.join("python/README.md")).unwrap();
3601        assert!(
3602            readme.contains("pip install"),
3603            "missing install instructions: {readme}"
3604        );
3605
3606        let _ = std::fs::remove_dir_all(&tmp);
3607    }
3608
3609    #[test]
3610    fn python_has_memory_helpers() {
3611        let api = make_api(vec![]);
3612        let py = render_python_module(&api, true);
3613        assert!(
3614            py.contains("import contextlib"),
3615            "missing contextlib import"
3616        );
3617        assert!(
3618            py.contains("class _PointerGuard(contextlib.AbstractContextManager):"),
3619            "missing _PointerGuard class"
3620        );
3621        assert!(
3622            py.contains("def __exit__(self, *exc)"),
3623            "missing _PointerGuard.__exit__"
3624        );
3625        assert!(
3626            py.contains("def _string_to_bytes("),
3627            "missing _string_to_bytes helper"
3628        );
3629        assert!(
3630            py.contains("def _bytes_to_string("),
3631            "missing _bytes_to_string helper"
3632        );
3633    }
3634
3635    #[test]
3636    fn python_custom_package_name() {
3637        let api = make_api(vec![simple_module(vec![Function {
3638            name: "add".into(),
3639            params: vec![
3640                Param {
3641                    name: "a".into(),
3642                    ty: TypeRef::I32,
3643                    mutable: false,
3644                },
3645                Param {
3646                    name: "b".into(),
3647                    ty: TypeRef::I32,
3648                    mutable: false,
3649                },
3650            ],
3651            returns: Some(TypeRef::I32),
3652            doc: None,
3653            r#async: false,
3654            cancellable: false,
3655            deprecated: None,
3656            since: None,
3657        }])]);
3658
3659        let config = GeneratorConfig {
3660            python_package_name: Some("my_bindings".into()),
3661            ..Default::default()
3662        };
3663
3664        let tmp = std::env::temp_dir().join("weaveffi_test_py_custom_pkg");
3665        let _ = std::fs::remove_dir_all(&tmp);
3666        std::fs::create_dir_all(&tmp).unwrap();
3667        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
3668
3669        PythonGenerator
3670            .generate_with_config(&api, out_dir, &config)
3671            .unwrap();
3672
3673        assert!(
3674            tmp.join("python/my_bindings/__init__.py").exists(),
3675            "package dir should use custom name"
3676        );
3677        assert!(
3678            tmp.join("python/my_bindings/weaveffi.py").exists(),
3679            "module file should be inside custom package dir"
3680        );
3681
3682        let pyproject = std::fs::read_to_string(tmp.join("python/pyproject.toml")).unwrap();
3683        assert!(
3684            pyproject.contains("name = \"my_bindings\""),
3685            "pyproject.toml should use custom name: {pyproject}"
3686        );
3687        assert!(
3688            pyproject.contains("packages = [\"my_bindings\"]"),
3689            "pyproject.toml packages should use custom name: {pyproject}"
3690        );
3691
3692        let setup = std::fs::read_to_string(tmp.join("python/setup.py")).unwrap();
3693        assert!(
3694            setup.contains("name=\"my_bindings\""),
3695            "setup.py should use custom name: {setup}"
3696        );
3697
3698        let _ = std::fs::remove_dir_all(&tmp);
3699    }
3700
3701    #[test]
3702    fn python_strip_module_prefix() {
3703        let api = make_api(vec![Module {
3704            name: "contacts".into(),
3705            functions: vec![Function {
3706                name: "create_contact".into(),
3707                params: vec![Param {
3708                    name: "name".into(),
3709                    ty: TypeRef::StringUtf8,
3710                    mutable: false,
3711                }],
3712                returns: Some(TypeRef::I32),
3713                doc: None,
3714                r#async: false,
3715                cancellable: false,
3716                deprecated: None,
3717                since: None,
3718            }],
3719            structs: vec![],
3720            enums: vec![],
3721            callbacks: vec![],
3722            listeners: vec![],
3723            errors: None,
3724            modules: vec![],
3725        }]);
3726
3727        let config = GeneratorConfig {
3728            strip_module_prefix: true,
3729            ..Default::default()
3730        };
3731
3732        let tmp = std::env::temp_dir().join("weaveffi_test_python_strip_prefix");
3733        let _ = std::fs::remove_dir_all(&tmp);
3734        std::fs::create_dir_all(&tmp).unwrap();
3735        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
3736
3737        PythonGenerator
3738            .generate_with_config(&api, out_dir, &config)
3739            .unwrap();
3740
3741        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
3742        assert!(
3743            py.contains("def create_contact("),
3744            "stripped name should be create_contact: {py}"
3745        );
3746        assert!(
3747            !py.contains("def contacts_create_contact("),
3748            "should not contain module-prefixed name: {py}"
3749        );
3750        assert!(
3751            py.contains("weaveffi_contacts_create_contact"),
3752            "C ABI call should still use full name: {py}"
3753        );
3754
3755        let pyi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.pyi")).unwrap();
3756        assert!(
3757            pyi.contains("def create_contact("),
3758            "pyi stripped name should be create_contact: {pyi}"
3759        );
3760
3761        let no_strip = GeneratorConfig::default();
3762        let tmp2 = std::env::temp_dir().join("weaveffi_test_python_no_strip_prefix");
3763        let _ = std::fs::remove_dir_all(&tmp2);
3764        std::fs::create_dir_all(&tmp2).unwrap();
3765        let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8");
3766
3767        PythonGenerator
3768            .generate_with_config(&api, out_dir2, &no_strip)
3769            .unwrap();
3770
3771        let py2 = std::fs::read_to_string(tmp2.join("python/weaveffi/weaveffi.py")).unwrap();
3772        assert!(
3773            py2.contains("def contacts_create_contact("),
3774            "default should use module-prefixed name: {py2}"
3775        );
3776
3777        let pyi2 = std::fs::read_to_string(tmp2.join("python/weaveffi/weaveffi.pyi")).unwrap();
3778        assert!(
3779            pyi2.contains("def contacts_create_contact("),
3780            "pyi default should use module-prefixed name: {pyi2}"
3781        );
3782
3783        let _ = std::fs::remove_dir_all(&tmp);
3784        let _ = std::fs::remove_dir_all(&tmp2);
3785    }
3786
3787    #[test]
3788    fn python_deeply_nested_optional() {
3789        let api = make_api(vec![Module {
3790            name: "edge".into(),
3791            functions: vec![Function {
3792                name: "process".into(),
3793                params: vec![Param {
3794                    name: "data".into(),
3795                    ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
3796                        Box::new(TypeRef::Struct("Contact".into())),
3797                    ))))),
3798                    mutable: false,
3799                }],
3800                returns: None,
3801                doc: None,
3802                r#async: false,
3803                cancellable: false,
3804                deprecated: None,
3805                since: None,
3806            }],
3807            structs: vec![StructDef {
3808                name: "Contact".into(),
3809                doc: None,
3810                fields: vec![StructField {
3811                    name: "name".into(),
3812                    ty: TypeRef::StringUtf8,
3813                    doc: None,
3814                    default: None,
3815                }],
3816                builder: false,
3817            }],
3818            enums: vec![],
3819            callbacks: vec![],
3820            listeners: vec![],
3821            errors: None,
3822            modules: vec![],
3823        }]);
3824        let pyi = render_pyi_module(&api, true);
3825        assert!(
3826            pyi.contains("Optional[List[Optional["),
3827            "should contain deeply nested optional type: {pyi}"
3828        );
3829    }
3830
3831    #[test]
3832    fn python_map_of_lists() {
3833        let api = make_api(vec![Module {
3834            name: "edge".into(),
3835            functions: vec![Function {
3836                name: "process".into(),
3837                params: vec![Param {
3838                    name: "scores".into(),
3839                    ty: TypeRef::Map(
3840                        Box::new(TypeRef::StringUtf8),
3841                        Box::new(TypeRef::List(Box::new(TypeRef::I32))),
3842                    ),
3843                    mutable: false,
3844                }],
3845                returns: None,
3846                doc: None,
3847                r#async: false,
3848                cancellable: false,
3849                deprecated: None,
3850                since: None,
3851            }],
3852            structs: vec![],
3853            enums: vec![],
3854            callbacks: vec![],
3855            listeners: vec![],
3856            errors: None,
3857            modules: vec![],
3858        }]);
3859        let pyi = render_pyi_module(&api, true);
3860        assert!(
3861            pyi.contains("Dict[str, List[int]]"),
3862            "should contain map of lists type: {pyi}"
3863        );
3864    }
3865
3866    #[test]
3867    fn python_enum_keyed_map() {
3868        let api = make_api(vec![Module {
3869            name: "edge".into(),
3870            functions: vec![Function {
3871                name: "process".into(),
3872                params: vec![Param {
3873                    name: "contacts".into(),
3874                    ty: TypeRef::Map(
3875                        Box::new(TypeRef::Enum("Color".into())),
3876                        Box::new(TypeRef::Struct("Contact".into())),
3877                    ),
3878                    mutable: false,
3879                }],
3880                returns: None,
3881                doc: None,
3882                r#async: false,
3883                cancellable: false,
3884                deprecated: None,
3885                since: None,
3886            }],
3887            structs: vec![StructDef {
3888                name: "Contact".into(),
3889                doc: None,
3890                fields: vec![StructField {
3891                    name: "name".into(),
3892                    ty: TypeRef::StringUtf8,
3893                    doc: None,
3894                    default: None,
3895                }],
3896                builder: false,
3897            }],
3898            enums: vec![EnumDef {
3899                name: "Color".into(),
3900                doc: None,
3901                variants: vec![
3902                    EnumVariant {
3903                        name: "Red".into(),
3904                        value: 0,
3905                        doc: None,
3906                    },
3907                    EnumVariant {
3908                        name: "Green".into(),
3909                        value: 1,
3910                        doc: None,
3911                    },
3912                    EnumVariant {
3913                        name: "Blue".into(),
3914                        value: 2,
3915                        doc: None,
3916                    },
3917                ],
3918            }],
3919            callbacks: vec![],
3920            listeners: vec![],
3921            errors: None,
3922            modules: vec![],
3923        }]);
3924        let pyi = render_pyi_module(&api, true);
3925        assert!(
3926            pyi.contains("Dict[\"Color\", \"Contact\"]"),
3927            "should contain enum-keyed map type: {pyi}"
3928        );
3929    }
3930
3931    #[test]
3932    fn python_typed_handle_type() {
3933        let api = Api {
3934            version: "0.1.0".into(),
3935            modules: vec![Module {
3936                name: "contacts".into(),
3937                functions: vec![Function {
3938                    name: "get_info".into(),
3939                    params: vec![Param {
3940                        name: "contact".into(),
3941                        ty: TypeRef::TypedHandle("Contact".into()),
3942                        mutable: false,
3943                    }],
3944                    returns: None,
3945                    doc: None,
3946                    r#async: false,
3947                    cancellable: false,
3948                    deprecated: None,
3949                    since: None,
3950                }],
3951                structs: vec![StructDef {
3952                    name: "Contact".into(),
3953                    doc: None,
3954                    fields: vec![StructField {
3955                        name: "name".into(),
3956                        ty: TypeRef::StringUtf8,
3957                        doc: None,
3958                        default: None,
3959                    }],
3960                    builder: false,
3961                }],
3962                enums: vec![],
3963                callbacks: vec![],
3964                listeners: vec![],
3965                errors: None,
3966                modules: vec![],
3967            }],
3968            generators: None,
3969        };
3970        let py = render_python_module(&api, true);
3971        assert!(
3972            py.contains("contact: \"Contact\""),
3973            "TypedHandle should use class type not int: {py}"
3974        );
3975        assert!(
3976            py.contains("contact._ptr"),
3977            "TypedHandle call arg should extract ._ptr: {py}"
3978        );
3979        assert!(
3980            py.contains("ctypes.c_void_p"),
3981            "TypedHandle ctypes type should be c_void_p: {py}"
3982        );
3983    }
3984
3985    #[test]
3986    fn python_no_double_free_on_error() {
3987        let api = make_api(vec![Module {
3988            name: "contacts".into(),
3989            functions: vec![Function {
3990                name: "find_contact".into(),
3991                params: vec![Param {
3992                    name: "name".into(),
3993                    ty: TypeRef::StringUtf8,
3994                    mutable: false,
3995                }],
3996                returns: Some(TypeRef::Struct("Contact".into())),
3997                doc: None,
3998                r#async: false,
3999                cancellable: false,
4000                deprecated: None,
4001                since: None,
4002            }],
4003            structs: vec![StructDef {
4004                name: "Contact".into(),
4005                doc: None,
4006                fields: vec![StructField {
4007                    name: "name".into(),
4008                    ty: TypeRef::StringUtf8,
4009                    doc: None,
4010                    default: None,
4011                }],
4012                builder: false,
4013            }],
4014            enums: vec![],
4015            callbacks: vec![],
4016            listeners: vec![],
4017            errors: None,
4018            modules: vec![],
4019        }]);
4020
4021        let py = render_python_module(&api, true);
4022
4023        assert!(
4024            py.contains("_string_to_bytes(name)"),
4025            "string param should use _string_to_bytes(name): {py}"
4026        );
4027        assert!(
4028            !py.contains("weaveffi_free_string(name"),
4029            "input string param must not be freed with weaveffi_free_string(name): {py}"
4030        );
4031        assert!(
4032            !py.contains("free(name"),
4033            "input string param must not be passed to free(name: {py}"
4034        );
4035
4036        let fn_sig = "def find_contact(name: str) -> \"Contact\":";
4037        let start = py
4038            .find(fn_sig)
4039            .unwrap_or_else(|| panic!("missing find_contact signature: {py}"));
4040        let rest = &py[start..];
4041        let end_offset = rest[1..]
4042            .find("\n\ndef ")
4043            .or_else(|| rest[1..].find("\n\nclass "))
4044            .map(|i| i + 1)
4045            .unwrap_or(rest.len());
4046        let body = &rest[..end_offset];
4047        let err_pos = body
4048            .find("_check_error(_err)")
4049            .expect("_check_error should appear in find_contact");
4050        let contact_pos = body
4051            .find("return Contact(_result)")
4052            .expect("return Contact(_result) should appear in find_contact");
4053        assert!(
4054            err_pos < contact_pos,
4055            "_check_error(_err) should precede return Contact(_result): {body}"
4056        );
4057
4058        let class_start = py
4059            .find("class Contact:")
4060            .expect("Contact class should be defined");
4061        let after_class = &py[class_start..];
4062        let class_end = after_class[1..]
4063            .find("\n\nclass ")
4064            .or_else(|| after_class[1..].find("\n\ndef "))
4065            .map(|i| i + 1)
4066            .unwrap_or(after_class.len());
4067        let contact_class = &after_class[..class_end];
4068        assert!(
4069            contact_class.contains("def __del__(self)"),
4070            "Contact should define __del__: {contact_class}"
4071        );
4072        assert!(
4073            contact_class.contains("_destroy"),
4074            "Contact.__del__ should call _destroy: {contact_class}"
4075        );
4076    }
4077
4078    #[test]
4079    fn python_null_check_on_optional_return() {
4080        let api = make_api(vec![Module {
4081            name: "contacts".into(),
4082            functions: vec![Function {
4083                name: "find_contact".into(),
4084                params: vec![Param {
4085                    name: "id".into(),
4086                    ty: TypeRef::I32,
4087                    mutable: false,
4088                }],
4089                returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
4090                    "Contact".into(),
4091                )))),
4092                doc: None,
4093                r#async: false,
4094                cancellable: false,
4095                deprecated: None,
4096                since: None,
4097            }],
4098            structs: vec![StructDef {
4099                name: "Contact".into(),
4100                doc: None,
4101                fields: vec![StructField {
4102                    name: "name".into(),
4103                    ty: TypeRef::StringUtf8,
4104                    doc: None,
4105                    default: None,
4106                }],
4107                builder: false,
4108            }],
4109            enums: vec![],
4110            callbacks: vec![],
4111            listeners: vec![],
4112            errors: None,
4113            modules: vec![],
4114        }]);
4115
4116        let py = render_python_module(&api, true);
4117
4118        assert!(
4119            py.contains("if _result is None:\n        return None"),
4120            "optional struct return should null-check before wrap: {py}"
4121        );
4122        let none_check = py
4123            .find("if _result is None:\n        return None")
4124            .expect("null-check block");
4125        let wrap = py
4126            .find("return Contact(_result)")
4127            .expect("Contact(_result) wrap");
4128        assert!(
4129            wrap > none_check,
4130            "Contact(_result) should appear after null check: {py}"
4131        );
4132    }
4133
4134    #[test]
4135    fn python_async_function_is_async_def() {
4136        let api = make_api(vec![simple_module(vec![Function {
4137            name: "fetch_data".into(),
4138            params: vec![Param {
4139                name: "id".into(),
4140                ty: TypeRef::I32,
4141                mutable: false,
4142            }],
4143            returns: Some(TypeRef::StringUtf8),
4144            doc: None,
4145            r#async: true,
4146            cancellable: false,
4147            deprecated: None,
4148            since: None,
4149        }])]);
4150        let code = render_python_module(&api, true);
4151        assert!(
4152            code.contains("import asyncio"),
4153            "should import asyncio: {code}"
4154        );
4155        assert!(
4156            code.contains("def _fetch_data_sync(id: int) -> str:"),
4157            "should have sync helper: {code}"
4158        );
4159        assert!(
4160            code.contains("async def fetch_data(id: int) -> str:"),
4161            "should have async wrapper: {code}"
4162        );
4163        assert!(
4164            code.contains("asyncio.get_event_loop()"),
4165            "should use get_event_loop: {code}"
4166        );
4167        assert!(
4168            code.contains("run_in_executor(None, _fetch_data_sync, id)"),
4169            "should use run_in_executor with sync fn and args: {code}"
4170        );
4171    }
4172
4173    #[test]
4174    fn python_pyi_async_function() {
4175        let api = make_api(vec![simple_module(vec![Function {
4176            name: "fetch_data".into(),
4177            params: vec![Param {
4178                name: "id".into(),
4179                ty: TypeRef::I32,
4180                mutable: false,
4181            }],
4182            returns: Some(TypeRef::StringUtf8),
4183            doc: None,
4184            r#async: true,
4185            cancellable: false,
4186            deprecated: None,
4187            since: None,
4188        }])]);
4189        let stubs = render_pyi_module(&api, true);
4190        assert!(
4191            stubs.contains("async def fetch_data(id: int) -> str: ..."),
4192            "pyi should declare async def: {stubs}"
4193        );
4194    }
4195
4196    #[test]
4197    fn python_cross_module_struct() {
4198        let api = make_api(vec![
4199            Module {
4200                name: "types".into(),
4201                functions: vec![],
4202                structs: vec![StructDef {
4203                    name: "Name".into(),
4204                    doc: None,
4205                    fields: vec![StructField {
4206                        name: "value".into(),
4207                        ty: TypeRef::StringUtf8,
4208                        doc: None,
4209                        default: None,
4210                    }],
4211                    builder: false,
4212                }],
4213                enums: vec![],
4214                callbacks: vec![],
4215                listeners: vec![],
4216                errors: None,
4217                modules: vec![],
4218            },
4219            Module {
4220                name: "ops".into(),
4221                functions: vec![Function {
4222                    name: "get_name".into(),
4223                    params: vec![Param {
4224                        name: "id".into(),
4225                        ty: TypeRef::I32,
4226                        mutable: false,
4227                    }],
4228                    returns: Some(TypeRef::Struct("types.Name".into())),
4229                    doc: None,
4230                    r#async: false,
4231                    cancellable: false,
4232                    deprecated: None,
4233                    since: None,
4234                }],
4235                structs: vec![],
4236                enums: vec![],
4237                callbacks: vec![],
4238                listeners: vec![],
4239                errors: None,
4240                modules: vec![],
4241            },
4242        ]);
4243
4244        let code = render_python_module(&api, true);
4245        let stubs = render_pyi_module(&api, true);
4246
4247        assert!(
4248            code.contains("Name(_result)"),
4249            "cross-module return should construct Name, not types.Name: {code}"
4250        );
4251        assert!(
4252            !code.contains("types.Name"),
4253            "dot-qualified name should not appear in generated Python code: {code}"
4254        );
4255        assert!(
4256            stubs.contains("\"Name\""),
4257            "pyi should use local type name: {stubs}"
4258        );
4259        assert!(
4260            !stubs.contains("types.Name"),
4261            "dot-qualified name should not appear in pyi stubs: {stubs}"
4262        );
4263    }
4264
4265    #[test]
4266    fn python_nested_module_output() {
4267        let api = make_api(vec![Module {
4268            name: "parent".to_string(),
4269            functions: vec![Function {
4270                name: "outer_fn".to_string(),
4271                params: vec![],
4272                returns: Some(TypeRef::I32),
4273                doc: None,
4274                r#async: false,
4275                cancellable: false,
4276                deprecated: None,
4277                since: None,
4278            }],
4279            structs: vec![],
4280            enums: vec![],
4281            callbacks: vec![],
4282            listeners: vec![],
4283            errors: None,
4284            modules: vec![Module {
4285                name: "child".to_string(),
4286                functions: vec![Function {
4287                    name: "inner_fn".to_string(),
4288                    params: vec![],
4289                    returns: Some(TypeRef::I32),
4290                    doc: None,
4291                    r#async: false,
4292                    cancellable: false,
4293                    deprecated: None,
4294                    since: None,
4295                }],
4296                structs: vec![],
4297                enums: vec![],
4298                callbacks: vec![],
4299                listeners: vec![],
4300                errors: None,
4301                modules: vec![],
4302            }],
4303        }]);
4304        let py = render_python_module(&api, true);
4305        assert!(
4306            py.contains("# === Module: parent ==="),
4307            "parent module section missing: {py}"
4308        );
4309        assert!(
4310            py.contains("# === Module: parent_child ==="),
4311            "nested child module section missing: {py}"
4312        );
4313        assert!(
4314            py.contains("weaveffi_parent_outer_fn"),
4315            "parent C function missing: {py}"
4316        );
4317        assert!(
4318            py.contains("weaveffi_parent_child_inner_fn"),
4319            "nested child C function missing: {py}"
4320        );
4321        let pyi = render_pyi_module(&api, true);
4322        assert!(
4323            pyi.contains("def inner_fn"),
4324            "nested child function missing from pyi: {pyi}"
4325        );
4326    }
4327
4328    #[test]
4329    fn python_type_hint_iterator() {
4330        assert_eq!(
4331            py_type_hint(&TypeRef::Iterator(Box::new(TypeRef::I32))),
4332            "Iterator[int]"
4333        );
4334        assert_eq!(
4335            py_type_hint(&TypeRef::Iterator(Box::new(TypeRef::Struct(
4336                "Contact".into()
4337            )))),
4338            "Iterator[\"Contact\"]"
4339        );
4340    }
4341
4342    #[test]
4343    fn python_iterator_return() {
4344        let api = make_api(vec![Module {
4345            name: "data".to_string(),
4346            functions: vec![Function {
4347                name: "list_items".to_string(),
4348                params: vec![],
4349                returns: Some(TypeRef::Iterator(Box::new(TypeRef::I32))),
4350                doc: None,
4351                r#async: false,
4352                cancellable: false,
4353                deprecated: None,
4354                since: None,
4355            }],
4356            structs: vec![],
4357            enums: vec![],
4358            callbacks: vec![],
4359            listeners: vec![],
4360            errors: None,
4361            modules: vec![],
4362        }]);
4363        let py = render_python_module(&api, true);
4364        assert!(
4365            py.contains("ListItemsIterator"),
4366            "should reference iterator type name: {py}"
4367        );
4368        assert!(
4369            py.contains("_next"),
4370            "should call _next for iteration: {py}"
4371        );
4372        assert!(
4373            py.contains("_destroy"),
4374            "should call _destroy for cleanup: {py}"
4375        );
4376    }
4377
4378    #[test]
4379    fn deprecated_function_generates_annotation() {
4380        let api = make_api(vec![simple_module(vec![Function {
4381            name: "add_old".into(),
4382            params: vec![
4383                Param {
4384                    name: "a".into(),
4385                    ty: TypeRef::I32,
4386                    mutable: false,
4387                },
4388                Param {
4389                    name: "b".into(),
4390                    ty: TypeRef::I32,
4391                    mutable: false,
4392                },
4393            ],
4394            returns: Some(TypeRef::I32),
4395            doc: None,
4396            r#async: false,
4397            cancellable: false,
4398            deprecated: Some("Use add_v2 instead".into()),
4399            since: Some("0.1.0".into()),
4400        }])]);
4401        let py = render_python_module(&api, true);
4402        assert!(
4403            py.contains("warnings.warn(\"Use add_v2 instead\", DeprecationWarning, stacklevel=2)"),
4404            "missing deprecation warning: {py}"
4405        );
4406    }
4407}