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