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