Skip to main content

weaveffi_gen_python/
lib.rs

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