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