Skip to main content

weaveffi_gen_python/
lib.rs

1use anyhow::Result;
2use camino::Utf8Path;
3use weaveffi_core::codegen::Generator;
4use weaveffi_core::utils::c_symbol_name;
5use weaveffi_ir::ir::{Api, EnumDef, Function, StructDef, StructField, TypeRef};
6
7pub struct PythonGenerator;
8
9impl PythonGenerator {
10    fn generate_impl(&self, api: &Api, out_dir: &Utf8Path, package_name: &str) -> Result<()> {
11        let dir = out_dir.join("python");
12        let pkg_dir = dir.join(package_name);
13        std::fs::create_dir_all(&pkg_dir)?;
14        std::fs::write(
15            pkg_dir.join("__init__.py"),
16            "from .weaveffi import *  # noqa: F401,F403\n",
17        )?;
18        std::fs::write(pkg_dir.join("weaveffi.py"), render_python_module(api))?;
19        std::fs::write(pkg_dir.join("weaveffi.pyi"), render_pyi_module(api))?;
20        std::fs::write(
21            dir.join("pyproject.toml"),
22            render_pyproject_toml(package_name),
23        )?;
24        std::fs::write(dir.join("setup.py"), render_setup_py(package_name))?;
25        std::fs::write(dir.join("README.md"), render_readme())?;
26        Ok(())
27    }
28}
29
30impl Generator for PythonGenerator {
31    fn name(&self) -> &'static str {
32        "python"
33    }
34
35    fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()> {
36        self.generate_impl(api, out_dir, "weaveffi")
37    }
38
39    fn output_files(&self, _api: &Api, out_dir: &Utf8Path) -> Vec<String> {
40        let pkg = "weaveffi";
41        vec![
42            out_dir
43                .join(format!("python/{pkg}/__init__.py"))
44                .to_string(),
45            out_dir
46                .join(format!("python/{pkg}/weaveffi.py"))
47                .to_string(),
48            out_dir
49                .join(format!("python/{pkg}/weaveffi.pyi"))
50                .to_string(),
51            out_dir.join("python/pyproject.toml").to_string(),
52            out_dir.join("python/setup.py").to_string(),
53            out_dir.join("python/README.md").to_string(),
54        ]
55    }
56}
57
58// ── Type helpers ──
59
60fn is_c_pointer_type(ty: &TypeRef) -> bool {
61    matches!(
62        ty,
63        TypeRef::StringUtf8
64            | TypeRef::Bytes
65            | TypeRef::Struct(_)
66            | TypeRef::List(_)
67            | TypeRef::Map(_, _)
68    )
69}
70
71fn py_ctypes_scalar(ty: &TypeRef) -> &'static str {
72    match ty {
73        TypeRef::I32 => "ctypes.c_int32",
74        TypeRef::U32 => "ctypes.c_uint32",
75        TypeRef::I64 => "ctypes.c_int64",
76        TypeRef::F64 => "ctypes.c_double",
77        TypeRef::Bool => "ctypes.c_int32",
78        TypeRef::StringUtf8 => "ctypes.c_char_p",
79        TypeRef::Handle => "ctypes.c_uint64",
80        TypeRef::Bytes => "ctypes.c_uint8",
81        TypeRef::Struct(_) => "ctypes.c_void_p",
82        TypeRef::Enum(_) => "ctypes.c_int32",
83        TypeRef::Optional(_) | TypeRef::List(_) | TypeRef::Map(_, _) => "ctypes.c_void_p",
84    }
85}
86
87fn py_type_hint(ty: &TypeRef) -> String {
88    match ty {
89        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::Handle => "int".into(),
90        TypeRef::F64 => "float".into(),
91        TypeRef::Bool => "bool".into(),
92        TypeRef::StringUtf8 => "str".into(),
93        TypeRef::Bytes => "bytes".into(),
94        TypeRef::Struct(name) | TypeRef::Enum(name) => format!("\"{}\"", name),
95        TypeRef::Optional(inner) => format!("Optional[{}]", py_type_hint(inner)),
96        TypeRef::List(inner) => format!("List[{}]", py_type_hint(inner)),
97        TypeRef::Map(k, v) => format!("Dict[{}, {}]", py_type_hint(k), py_type_hint(v)),
98    }
99}
100
101fn py_param_argtypes(ty: &TypeRef) -> Vec<String> {
102    match ty {
103        TypeRef::Bytes => vec![
104            "ctypes.POINTER(ctypes.c_uint8)".into(),
105            "ctypes.c_size_t".into(),
106        ],
107        TypeRef::Optional(inner) if !is_c_pointer_type(inner) => {
108            vec![format!("ctypes.POINTER({})", py_ctypes_scalar(inner))]
109        }
110        TypeRef::Optional(inner) => py_param_argtypes(inner),
111        TypeRef::List(inner) => vec![
112            format!("ctypes.POINTER({})", py_ctypes_scalar(inner)),
113            "ctypes.c_size_t".into(),
114        ],
115        TypeRef::Map(k, v) => vec![
116            format!("ctypes.POINTER({})", py_ctypes_scalar(k)),
117            format!("ctypes.POINTER({})", py_ctypes_scalar(v)),
118            "ctypes.c_size_t".into(),
119        ],
120        _ => vec![py_ctypes_scalar(ty).into()],
121    }
122}
123
124/// Returns `(restype, out_param_argtypes)` for a return type.
125fn py_return_info(ty: &TypeRef) -> (String, Vec<String>) {
126    match ty {
127        TypeRef::Bytes => (
128            "ctypes.POINTER(ctypes.c_uint8)".into(),
129            vec!["ctypes.POINTER(ctypes.c_size_t)".into()],
130        ),
131        TypeRef::Optional(inner) if !is_c_pointer_type(inner) => (
132            format!("ctypes.POINTER({})", py_ctypes_scalar(inner)),
133            vec![],
134        ),
135        TypeRef::Optional(inner) => py_return_info(inner),
136        TypeRef::List(inner) => (
137            format!("ctypes.POINTER({})", py_ctypes_scalar(inner)),
138            vec!["ctypes.POINTER(ctypes.c_size_t)".into()],
139        ),
140        TypeRef::Map(k, v) => (
141            "None".into(),
142            vec![
143                format!("ctypes.POINTER(ctypes.POINTER({}))", py_ctypes_scalar(k)),
144                format!("ctypes.POINTER(ctypes.POINTER({}))", py_ctypes_scalar(v)),
145                "ctypes.POINTER(ctypes.c_size_t)".into(),
146            ],
147        ),
148        _ => (py_ctypes_scalar(ty).into(), vec![]),
149    }
150}
151
152fn get_map_kv(ty: &TypeRef) -> Option<(&TypeRef, &TypeRef)> {
153    match ty {
154        TypeRef::Map(k, v) => Some((k, v)),
155        TypeRef::Optional(inner) => get_map_kv(inner),
156        _ => None,
157    }
158}
159
160// ── Rendering ──
161
162fn render_python_module(api: &Api) -> String {
163    let mut out = String::new();
164    render_preamble(&mut out);
165    for m in &api.modules {
166        out.push_str(&format!("\n\n# === Module: {} ===", m.name));
167        for e in &m.enums {
168            render_enum(&mut out, e);
169        }
170        for s in &m.structs {
171            render_struct(&mut out, &m.name, s);
172        }
173        for f in &m.functions {
174            render_function(&mut out, &m.name, f);
175        }
176    }
177    out.push('\n');
178    out
179}
180
181fn render_preamble(out: &mut String) {
182    out.push_str(
183        r#""""WeaveFFI Python ctypes bindings (auto-generated)"""
184import contextlib
185import ctypes
186import platform
187from enum import IntEnum
188from typing import Dict, List, Optional
189
190
191class WeaveffiError(Exception):
192    def __init__(self, code: int, message: str) -> None:
193        self.code = code
194        self.message = message
195        super().__init__(f"({code}) {message}")
196
197
198class _WeaveffiErrorStruct(ctypes.Structure):
199    _fields_ = [
200        ("code", ctypes.c_int32),
201        ("message", ctypes.c_char_p),
202    ]
203
204
205def _load_library() -> ctypes.CDLL:
206    system = platform.system()
207    if system == "Darwin":
208        name = "libweaveffi.dylib"
209    elif system == "Windows":
210        name = "weaveffi.dll"
211    else:
212        name = "libweaveffi.so"
213    return ctypes.CDLL(name)
214
215
216_lib = _load_library()
217_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveffiErrorStruct)]
218_lib.weaveffi_error_clear.restype = None
219_lib.weaveffi_free_string.argtypes = [ctypes.c_char_p]
220_lib.weaveffi_free_string.restype = None
221_lib.weaveffi_free_bytes.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t]
222_lib.weaveffi_free_bytes.restype = None
223
224
225def _check_error(err: _WeaveffiErrorStruct) -> None:
226    if err.code != 0:
227        code = err.code
228        message = err.message.decode("utf-8") if err.message else ""
229        _lib.weaveffi_error_clear(ctypes.byref(err))
230        raise WeaveffiError(code, message)
231
232
233class _PointerGuard(contextlib.AbstractContextManager):
234    def __init__(self, ptr, free_fn) -> None:
235        self.ptr = ptr
236        self._free_fn = free_fn
237
238    def __exit__(self, *exc) -> bool:
239        if self.ptr is not None:
240            self._free_fn(self.ptr)
241            self.ptr = None
242        return False
243
244
245def _string_to_bytes(s: Optional[str]) -> Optional[bytes]:
246    if s is None:
247        return None
248    return s.encode("utf-8")
249
250
251def _bytes_to_string(ptr) -> Optional[str]:
252    if ptr is None:
253        return None
254    return ptr.decode("utf-8")
255"#,
256    );
257}
258
259fn render_enum(out: &mut String, e: &EnumDef) {
260    out.push_str(&format!("\n\nclass {}(IntEnum):", e.name));
261    if let Some(doc) = &e.doc {
262        out.push_str(&format!("\n    \"\"\"{}\"\"\"", doc));
263    }
264    for v in &e.variants {
265        out.push_str(&format!("\n    {} = {}", v.name, v.value));
266    }
267    out.push('\n');
268}
269
270fn render_struct(out: &mut String, module_name: &str, s: &StructDef) {
271    let prefix = format!("weaveffi_{}_{}", module_name, s.name);
272
273    out.push_str(&format!("\n\nclass {}:", s.name));
274    if let Some(doc) = &s.doc {
275        out.push_str(&format!("\n    \"\"\"{}\"\"\"", doc));
276    }
277
278    out.push_str("\n\n    def __init__(self, _ptr: int) -> None:");
279    out.push_str("\n        self._ptr = _ptr");
280
281    out.push_str("\n\n    def __del__(self) -> None:");
282    out.push_str("\n        if self._ptr is not None:");
283    out.push_str(&format!(
284        "\n            _lib.{prefix}_destroy.argtypes = [ctypes.c_void_p]"
285    ));
286    out.push_str(&format!(
287        "\n            _lib.{prefix}_destroy.restype = None"
288    ));
289    out.push_str(&format!("\n            _lib.{prefix}_destroy(self._ptr)"));
290    out.push_str("\n            self._ptr = None");
291
292    for field in &s.fields {
293        render_getter(out, &prefix, field);
294    }
295    out.push('\n');
296}
297
298fn render_getter(out: &mut String, prefix: &str, field: &StructField) {
299    let getter = format!("{prefix}_get_{}", field.name);
300    let py_ty = py_type_hint(&field.ty);
301    let ind = "        ";
302
303    out.push_str(&format!(
304        "\n\n    @property\n    def {}(self) -> {}:\n",
305        field.name, py_ty
306    ));
307    out.push_str(&format!("{ind}_fn = _lib.{getter}\n"));
308
309    let (restype, out_argtypes) = py_return_info(&field.ty);
310    let mut argtypes = vec!["ctypes.c_void_p".to_string()];
311    argtypes.extend(out_argtypes.iter().cloned());
312
313    out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", ")));
314    out.push_str(&format!("{ind}_fn.restype = {restype}\n"));
315
316    if out_argtypes.is_empty() {
317        out.push_str(&format!("{ind}_result = _fn(self._ptr)\n"));
318    } else if let Some((k, v)) = get_map_kv(&field.ty) {
319        out.push_str(&format!(
320            "{ind}_out_keys = ctypes.POINTER({})()\n",
321            py_ctypes_scalar(k)
322        ));
323        out.push_str(&format!(
324            "{ind}_out_values = ctypes.POINTER({})()\n",
325            py_ctypes_scalar(v)
326        ));
327        out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
328        out.push_str(&format!("{ind}_fn(self._ptr, ctypes.byref(_out_keys), ctypes.byref(_out_values), ctypes.byref(_out_len))\n"));
329    } else {
330        out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
331        out.push_str(&format!(
332            "{ind}_result = _fn(self._ptr, ctypes.byref(_out_len))\n"
333        ));
334    }
335
336    render_return_value(out, &field.ty, ind);
337}
338
339fn render_function(out: &mut String, module_name: &str, f: &Function) {
340    let params_sig: Vec<String> = f
341        .params
342        .iter()
343        .map(|p| format!("{}: {}", p.name, py_type_hint(&p.ty)))
344        .collect();
345    let ret_hint = f
346        .returns
347        .as_ref()
348        .map(py_type_hint)
349        .unwrap_or_else(|| "None".to_string());
350
351    out.push_str(&format!(
352        "\n\ndef {}({}) -> {}:\n",
353        f.name,
354        params_sig.join(", "),
355        ret_hint
356    ));
357
358    let c_sym = c_symbol_name(module_name, &f.name);
359    let ind = "    ";
360
361    out.push_str(&format!("{ind}_fn = _lib.{c_sym}\n"));
362
363    let mut argtypes: Vec<String> = Vec::new();
364    for p in &f.params {
365        argtypes.extend(py_param_argtypes(&p.ty));
366    }
367    let mut out_ret_argtypes = Vec::new();
368    let restype;
369    if let Some(ret_ty) = &f.returns {
370        let (rt, oat) = py_return_info(ret_ty);
371        argtypes.extend(oat.iter().cloned());
372        restype = rt;
373        out_ret_argtypes = oat;
374    } else {
375        restype = "None".to_string();
376    }
377    argtypes.push("ctypes.POINTER(_WeaveffiErrorStruct)".into());
378
379    out.push_str(&format!("{ind}_fn.argtypes = [{}]\n", argtypes.join(", ")));
380    out.push_str(&format!("{ind}_fn.restype = {restype}\n"));
381
382    for p in &f.params {
383        for line in py_param_conversion(&p.name, &p.ty, ind) {
384            out.push_str(&line);
385            out.push('\n');
386        }
387    }
388
389    out.push_str(&format!("{ind}_err = _WeaveffiErrorStruct()\n"));
390
391    let is_map_ret = f.returns.as_ref().and_then(get_map_kv).is_some();
392    let has_out_len = !out_ret_argtypes.is_empty() && !is_map_ret;
393
394    if let Some((k, v)) = f.returns.as_ref().and_then(get_map_kv) {
395        out.push_str(&format!(
396            "{ind}_out_keys = ctypes.POINTER({})()\n",
397            py_ctypes_scalar(k)
398        ));
399        out.push_str(&format!(
400            "{ind}_out_values = ctypes.POINTER({})()\n",
401            py_ctypes_scalar(v)
402        ));
403        out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
404    } else if has_out_len {
405        out.push_str(&format!("{ind}_out_len = ctypes.c_size_t(0)\n"));
406    }
407
408    let mut call_args: Vec<String> = Vec::new();
409    for p in &f.params {
410        call_args.extend(py_param_call_args(&p.name, &p.ty));
411    }
412    if is_map_ret {
413        call_args.push("ctypes.byref(_out_keys)".into());
414        call_args.push("ctypes.byref(_out_values)".into());
415        call_args.push("ctypes.byref(_out_len)".into());
416    } else if has_out_len {
417        call_args.push("ctypes.byref(_out_len)".into());
418    }
419    call_args.push("ctypes.byref(_err)".into());
420
421    let call_expr = format!("_fn({})", call_args.join(", "));
422    if f.returns.is_some() && !is_map_ret {
423        out.push_str(&format!("{ind}_result = {call_expr}\n"));
424    } else {
425        out.push_str(&format!("{ind}{call_expr}\n"));
426    }
427
428    out.push_str(&format!("{ind}_check_error(_err)\n"));
429
430    if let Some(ret_ty) = &f.returns {
431        render_return_value(out, ret_ty, ind);
432    }
433}
434
435// ── Param helpers ──
436
437fn py_list_convert_expr(name: &str, elem: &TypeRef) -> String {
438    match elem {
439        TypeRef::StringUtf8 => format!("*[_string_to_bytes(v) for v in {name}]"),
440        TypeRef::Struct(_) => format!("*[v._ptr for v in {name}]"),
441        TypeRef::Enum(_) => format!("*[v.value for v in {name}]"),
442        TypeRef::Bool => format!("*[1 if v else 0 for v in {name}]"),
443        _ => format!("*{name}"),
444    }
445}
446
447fn py_map_elem_convert(list_name: &str, ty: &TypeRef, var: &str) -> String {
448    match ty {
449        TypeRef::StringUtf8 => format!("*[_string_to_bytes({var}) for {var} in {list_name}]"),
450        TypeRef::Enum(_) => format!("*[{var}.value for {var} in {list_name}]"),
451        TypeRef::Struct(_) => format!("*[{var}._ptr for {var} in {list_name}]"),
452        TypeRef::Bool => format!("*[1 if {var} else 0 for {var} in {list_name}]"),
453        _ => format!("*{list_name}"),
454    }
455}
456
457fn py_param_conversion(name: &str, ty: &TypeRef, ind: &str) -> Vec<String> {
458    match ty {
459        TypeRef::Bytes => {
460            let s = py_ctypes_scalar(&TypeRef::Bytes);
461            vec![format!("{ind}_{name}_arr = ({s} * len({name}))(*{name})")]
462        }
463        TypeRef::Optional(inner) => match inner.as_ref() {
464            TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
465                let s = py_ctypes_scalar(inner);
466                vec![format!(
467                    "{ind}_{name}_c = ctypes.byref({s}({name})) if {name} is not None else None"
468                )]
469            }
470            TypeRef::Bool => {
471                vec![format!(
472                    "{ind}_{name}_c = ctypes.byref(ctypes.c_int32(1 if {name} else 0)) if {name} is not None else None"
473                )]
474            }
475            TypeRef::StringUtf8 => {
476                vec![format!("{ind}_{name}_c = _string_to_bytes({name})")]
477            }
478            TypeRef::Enum(_) => {
479                vec![format!(
480                    "{ind}_{name}_c = ctypes.byref(ctypes.c_int32({name}.value)) if {name} is not None else None"
481                )]
482            }
483            TypeRef::Bytes => {
484                let s = py_ctypes_scalar(&TypeRef::Bytes);
485                vec![
486                    format!("{ind}if {name} is not None:"),
487                    format!("{ind}    _{name}_arr = ({s} * len({name}))(*{name})"),
488                    format!("{ind}    _{name}_len = len({name})"),
489                    format!("{ind}else:"),
490                    format!("{ind}    _{name}_arr = None"),
491                    format!("{ind}    _{name}_len = 0"),
492                ]
493            }
494            TypeRef::List(elem) => {
495                let s = py_ctypes_scalar(elem);
496                let convert = py_list_convert_expr(name, elem);
497                vec![
498                    format!("{ind}if {name} is not None:"),
499                    format!("{ind}    _{name}_arr = ({s} * len({name}))({convert})"),
500                    format!("{ind}    _{name}_len = len({name})"),
501                    format!("{ind}else:"),
502                    format!("{ind}    _{name}_arr = None"),
503                    format!("{ind}    _{name}_len = 0"),
504                ]
505            }
506            _ => vec![],
507        },
508        TypeRef::List(inner) => {
509            let s = py_ctypes_scalar(inner);
510            let convert = py_list_convert_expr(name, inner);
511            vec![format!("{ind}_{name}_arr = ({s} * len({name}))({convert})")]
512        }
513        TypeRef::Map(k, v) => {
514            let ks = py_ctypes_scalar(k);
515            let vs = py_ctypes_scalar(v);
516            let kconv = py_map_elem_convert(&format!("_{name}_keys"), k, "_k");
517            let vconv = py_map_elem_convert(&format!("_{name}_vals"), v, "_v");
518            vec![
519                format!("{ind}_{name}_keys = list({name}.keys())"),
520                format!("{ind}_{name}_vals = [{name}[_k] for _k in _{name}_keys]"),
521                format!("{ind}_{name}_ka = ({ks} * len(_{name}_keys))({kconv})"),
522                format!("{ind}_{name}_va = ({vs} * len(_{name}_vals))({vconv})"),
523            ]
524        }
525        _ => vec![],
526    }
527}
528
529fn py_param_call_args(name: &str, ty: &TypeRef) -> Vec<String> {
530    match ty {
531        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
532            vec![name.to_string()]
533        }
534        TypeRef::Bool => vec![format!("1 if {name} else 0")],
535        TypeRef::StringUtf8 => vec![format!("_string_to_bytes({name})")],
536        TypeRef::Bytes => vec![format!("_{name}_arr"), format!("len({name})")],
537        TypeRef::Struct(_) => vec![format!("{name}._ptr")],
538        TypeRef::Enum(_) => vec![format!("{name}.value")],
539        TypeRef::Optional(inner) => match inner.as_ref() {
540            TypeRef::StringUtf8 => vec![format!("_{name}_c")],
541            TypeRef::Struct(_) => {
542                vec![format!("{name}._ptr if {name} is not None else None")]
543            }
544            TypeRef::Bytes | TypeRef::List(_) => {
545                vec![format!("_{name}_arr"), format!("_{name}_len")]
546            }
547            TypeRef::Map(_, _) => vec![
548                format!("_{name}_ka"),
549                format!("_{name}_va"),
550                format!("_{name}_len"),
551            ],
552            _ if !is_c_pointer_type(inner) => vec![format!("_{name}_c")],
553            _ => py_param_call_args(name, inner),
554        },
555        TypeRef::List(_) => vec![format!("_{name}_arr"), format!("len({name})")],
556        TypeRef::Map(_, _) => vec![
557            format!("_{name}_ka"),
558            format!("_{name}_va"),
559            format!("len(_{name}_keys)"),
560        ],
561    }
562}
563
564// ── Return helpers ──
565
566fn py_read_element(expr: &str, ty: &TypeRef) -> String {
567    match ty {
568        TypeRef::StringUtf8 => format!("_bytes_to_string({expr})"),
569        TypeRef::Struct(name) => format!("{name}({expr})"),
570        TypeRef::Enum(name) => format!("{name}({expr})"),
571        TypeRef::Bool => format!("bool({expr})"),
572        _ => expr.to_string(),
573    }
574}
575
576fn render_return_value(out: &mut String, ty: &TypeRef, ind: &str) {
577    match ty {
578        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Handle => {
579            out.push_str(&format!("{ind}return _result\n"));
580        }
581        TypeRef::Bool => {
582            out.push_str(&format!("{ind}return bool(_result)\n"));
583        }
584        TypeRef::StringUtf8 => {
585            out.push_str(&format!("{ind}return _bytes_to_string(_result) or \"\"\n"));
586        }
587        TypeRef::Bytes => {
588            out.push_str(&format!("{ind}if not _result:\n"));
589            out.push_str(&format!("{ind}    return b\"\"\n"));
590            out.push_str(&format!("{ind}return bytes(_result[:_out_len.value])\n"));
591        }
592        TypeRef::Struct(name) => {
593            out.push_str(&format!("{ind}if _result is None:\n"));
594            out.push_str(&format!(
595                "{ind}    raise WeaveffiError(-1, \"null pointer\")\n"
596            ));
597            out.push_str(&format!("{ind}return {name}(_result)\n"));
598        }
599        TypeRef::Enum(name) => {
600            out.push_str(&format!("{ind}return {name}(_result)\n"));
601        }
602        TypeRef::Optional(inner) => render_optional_return(out, inner, ind),
603        TypeRef::List(inner) => render_list_return(out, inner, ind),
604        TypeRef::Map(k, v) => render_map_return(out, k, v, ind),
605    }
606}
607
608fn render_optional_return(out: &mut String, inner: &TypeRef, ind: &str) {
609    match inner {
610        TypeRef::StringUtf8 => {
611            out.push_str(&format!("{ind}return _bytes_to_string(_result)\n"));
612        }
613        TypeRef::Bytes => {
614            out.push_str(&format!("{ind}if not _result:\n"));
615            out.push_str(&format!("{ind}    return None\n"));
616            out.push_str(&format!("{ind}return bytes(_result[:_out_len.value])\n"));
617        }
618        TypeRef::Struct(name) => {
619            out.push_str(&format!("{ind}if _result is None:\n"));
620            out.push_str(&format!("{ind}    return None\n"));
621            out.push_str(&format!("{ind}return {name}(_result)\n"));
622        }
623        TypeRef::Enum(name) => {
624            out.push_str(&format!("{ind}if not _result:\n"));
625            out.push_str(&format!("{ind}    return None\n"));
626            out.push_str(&format!("{ind}return {name}(_result[0])\n"));
627        }
628        TypeRef::Bool => {
629            out.push_str(&format!("{ind}if not _result:\n"));
630            out.push_str(&format!("{ind}    return None\n"));
631            out.push_str(&format!("{ind}return bool(_result[0])\n"));
632        }
633        _ if !is_c_pointer_type(inner) => {
634            out.push_str(&format!("{ind}if not _result:\n"));
635            out.push_str(&format!("{ind}    return None\n"));
636            out.push_str(&format!("{ind}return _result[0]\n"));
637        }
638        _ => {
639            out.push_str(&format!("{ind}return _result\n"));
640        }
641    }
642}
643
644fn render_list_return(out: &mut String, inner: &TypeRef, ind: &str) {
645    out.push_str(&format!("{ind}if not _result:\n"));
646    out.push_str(&format!("{ind}    return []\n"));
647    let elem = py_read_element("_result[_i]", inner);
648    out.push_str(&format!(
649        "{ind}return [{elem} for _i in range(_out_len.value)]\n"
650    ));
651}
652
653fn render_map_return(out: &mut String, k: &TypeRef, v: &TypeRef, ind: &str) {
654    out.push_str(&format!("{ind}if not _out_keys or not _out_values:\n"));
655    out.push_str(&format!("{ind}    return {{}}\n"));
656    let key_read = py_read_element("_out_keys[_i]", k);
657    let val_read = py_read_element("_out_values[_i]", v);
658    out.push_str(&format!(
659        "{ind}return {{{key_read}: {val_read} for _i in range(_out_len.value)}}\n"
660    ));
661}
662
663// ── Packaging ──
664
665fn render_pyproject_toml(package_name: &str) -> String {
666    format!(
667        r#"[build-system]
668requires = ["setuptools>=61.0"]
669build-backend = "setuptools.build_meta"
670
671[project]
672name = "{package_name}"
673version = "0.1.0"
674description = "Python bindings for WeaveFFI (auto-generated)"
675requires-python = ">=3.8"
676
677[tool.setuptools]
678packages = ["{package_name}"]
679"#,
680    )
681}
682
683fn render_setup_py(package_name: &str) -> String {
684    format!(
685        r#"from setuptools import setup
686
687setup(
688    name="{package_name}",
689    version="0.1.0",
690    packages=["{package_name}"],
691)
692"#,
693    )
694}
695
696fn render_readme() -> &'static str {
697    r#"# WeaveFFI Python Bindings
698
699Auto-generated Python bindings using ctypes.
700
701## Prerequisites
702
703- Python >= 3.8
704- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path.
705
706## Install
707
708```bash
709pip install .
710```
711
712## Development install
713
714```bash
715pip install -e .
716```
717
718## Usage
719
720```python
721from weaveffi import *
722```
723"#
724}
725
726// ── Type stub (.pyi) rendering ──
727
728fn render_pyi_module(api: &Api) -> String {
729    let mut out =
730        String::from("from enum import IntEnum\nfrom typing import Dict, List, Optional\n");
731    for m in &api.modules {
732        for e in &m.enums {
733            render_pyi_enum(&mut out, e);
734        }
735        for s in &m.structs {
736            render_pyi_struct(&mut out, s);
737        }
738        for f in &m.functions {
739            render_pyi_function(&mut out, f);
740        }
741    }
742    out
743}
744
745fn render_pyi_enum(out: &mut String, e: &EnumDef) {
746    out.push_str(&format!("\nclass {}(IntEnum):\n", e.name));
747    for v in &e.variants {
748        out.push_str(&format!("    {}: int\n", v.name));
749    }
750}
751
752fn render_pyi_struct(out: &mut String, s: &StructDef) {
753    out.push_str(&format!("\nclass {}:\n", s.name));
754    for field in &s.fields {
755        let py_ty = py_type_hint(&field.ty);
756        out.push_str(&format!(
757            "    @property\n    def {}(self) -> {}: ...\n",
758            field.name, py_ty
759        ));
760    }
761}
762
763fn render_pyi_function(out: &mut String, f: &Function) {
764    let params: Vec<String> = f
765        .params
766        .iter()
767        .map(|p| format!("{}: {}", p.name, py_type_hint(&p.ty)))
768        .collect();
769    let ret = f
770        .returns
771        .as_ref()
772        .map(py_type_hint)
773        .unwrap_or_else(|| "None".into());
774    out.push_str(&format!(
775        "\ndef {}({}) -> {}: ...\n",
776        f.name,
777        params.join(", "),
778        ret
779    ));
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use camino::Utf8Path;
786    use weaveffi_ir::ir::{
787        Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef,
788    };
789
790    fn make_api(modules: Vec<Module>) -> Api {
791        Api {
792            version: "0.1.0".into(),
793            modules,
794        }
795    }
796
797    fn simple_module(functions: Vec<Function>) -> Module {
798        Module {
799            name: "math".into(),
800            functions,
801            structs: vec![],
802            enums: vec![],
803            errors: None,
804        }
805    }
806
807    #[test]
808    fn generator_name_is_python() {
809        assert_eq!(PythonGenerator.name(), "python");
810    }
811
812    #[test]
813    fn generate_creates_output_files() {
814        let api = make_api(vec![simple_module(vec![Function {
815            name: "add".into(),
816            params: vec![
817                Param {
818                    name: "a".into(),
819                    ty: TypeRef::I32,
820                },
821                Param {
822                    name: "b".into(),
823                    ty: TypeRef::I32,
824                },
825            ],
826            returns: Some(TypeRef::I32),
827            doc: None,
828            r#async: false,
829        }])]);
830
831        let tmp = std::env::temp_dir().join("weaveffi_test_python_gen_output");
832        let _ = std::fs::remove_dir_all(&tmp);
833        std::fs::create_dir_all(&tmp).unwrap();
834        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
835
836        PythonGenerator.generate(&api, out_dir).unwrap();
837
838        let init = std::fs::read_to_string(tmp.join("python/weaveffi/__init__.py")).unwrap();
839        assert!(init.contains("from .weaveffi import *"));
840
841        let weaveffi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
842        assert!(weaveffi.contains("WeaveFFI"));
843        assert!(weaveffi.contains("def add("));
844
845        let _ = std::fs::remove_dir_all(&tmp);
846    }
847
848    #[test]
849    fn output_files_lists_all() {
850        let api = make_api(vec![]);
851        let out = Utf8Path::new("/tmp/out");
852        let files = PythonGenerator.output_files(&api, out);
853        assert_eq!(
854            files,
855            vec![
856                "/tmp/out/python/weaveffi/__init__.py",
857                "/tmp/out/python/weaveffi/weaveffi.py",
858                "/tmp/out/python/weaveffi/weaveffi.pyi",
859                "/tmp/out/python/pyproject.toml",
860                "/tmp/out/python/setup.py",
861                "/tmp/out/python/README.md",
862            ]
863        );
864    }
865
866    #[test]
867    fn preamble_has_load_library() {
868        let api = make_api(vec![]);
869        let py = render_python_module(&api);
870        assert!(py.contains("def _load_library()"), "missing _load_library");
871        assert!(
872            py.contains("libweaveffi.dylib"),
873            "missing macOS library name"
874        );
875        assert!(py.contains("libweaveffi.so"), "missing Linux library name");
876        assert!(py.contains("weaveffi.dll"), "missing Windows library name");
877        assert!(py.contains("ctypes.CDLL(name)"), "missing CDLL call");
878    }
879
880    #[test]
881    fn preamble_has_error_handling() {
882        let api = make_api(vec![]);
883        let py = render_python_module(&api);
884        assert!(
885            py.contains("class WeaveffiError(Exception):"),
886            "missing error class"
887        );
888        assert!(
889            py.contains("class _WeaveffiErrorStruct(ctypes.Structure):"),
890            "missing error struct"
891        );
892        assert!(py.contains("def _check_error("), "missing _check_error");
893        assert!(
894            py.contains("weaveffi_error_clear"),
895            "missing error_clear setup"
896        );
897    }
898
899    #[test]
900    fn simple_i32_function() {
901        let api = make_api(vec![simple_module(vec![Function {
902            name: "add".into(),
903            params: vec![
904                Param {
905                    name: "a".into(),
906                    ty: TypeRef::I32,
907                },
908                Param {
909                    name: "b".into(),
910                    ty: TypeRef::I32,
911                },
912            ],
913            returns: Some(TypeRef::I32),
914            doc: None,
915            r#async: false,
916        }])]);
917
918        let py = render_python_module(&api);
919        assert!(
920            py.contains("def add(a: int, b: int) -> int:"),
921            "missing function signature: {py}"
922        );
923        assert!(
924            py.contains("_lib.weaveffi_math_add"),
925            "missing C symbol: {py}"
926        );
927        assert!(
928            py.contains("ctypes.c_int32, ctypes.c_int32"),
929            "missing argtypes: {py}"
930        );
931        assert!(
932            py.contains("_fn.restype = ctypes.c_int32"),
933            "missing restype: {py}"
934        );
935        assert!(
936            py.contains("_check_error(_err)"),
937            "missing error check: {py}"
938        );
939        assert!(py.contains("return _result"), "missing return: {py}");
940    }
941
942    #[test]
943    fn string_function_encode_decode() {
944        let api = make_api(vec![Module {
945            name: "text".into(),
946            functions: vec![Function {
947                name: "echo".into(),
948                params: vec![Param {
949                    name: "msg".into(),
950                    ty: TypeRef::StringUtf8,
951                }],
952                returns: Some(TypeRef::StringUtf8),
953                doc: None,
954                r#async: false,
955            }],
956            structs: vec![],
957            enums: vec![],
958            errors: None,
959        }]);
960
961        let py = render_python_module(&api);
962        assert!(
963            py.contains("def echo(msg: str) -> str:"),
964            "missing signature: {py}"
965        );
966        assert!(py.contains("ctypes.c_char_p"), "missing c_char_p: {py}");
967        assert!(
968            py.contains("_string_to_bytes(msg)"),
969            "missing _string_to_bytes call: {py}"
970        );
971        assert!(
972            py.contains("_bytes_to_string(_result)"),
973            "missing _bytes_to_string call: {py}"
974        );
975    }
976
977    #[test]
978    fn void_function() {
979        let api = make_api(vec![simple_module(vec![Function {
980            name: "reset".into(),
981            params: vec![],
982            returns: None,
983            doc: None,
984            r#async: false,
985        }])]);
986
987        let py = render_python_module(&api);
988        assert!(
989            py.contains("def reset() -> None:"),
990            "missing void signature: {py}"
991        );
992        assert!(
993            py.contains("_fn.restype = None"),
994            "missing None restype: {py}"
995        );
996        assert!(
997            !py.contains("_result ="),
998            "void function should not assign _result: {py}"
999        );
1000    }
1001
1002    #[test]
1003    fn enum_intenum_class() {
1004        let api = make_api(vec![Module {
1005            name: "paint".into(),
1006            functions: vec![],
1007            structs: vec![],
1008            enums: vec![EnumDef {
1009                name: "Color".into(),
1010                doc: Some("Primary colors".into()),
1011                variants: vec![
1012                    EnumVariant {
1013                        name: "Red".into(),
1014                        value: 0,
1015                        doc: None,
1016                    },
1017                    EnumVariant {
1018                        name: "Green".into(),
1019                        value: 1,
1020                        doc: None,
1021                    },
1022                    EnumVariant {
1023                        name: "Blue".into(),
1024                        value: 2,
1025                        doc: None,
1026                    },
1027                ],
1028            }],
1029            errors: None,
1030        }]);
1031
1032        let py = render_python_module(&api);
1033        assert!(
1034            py.contains("class Color(IntEnum):"),
1035            "missing IntEnum class: {py}"
1036        );
1037        assert!(
1038            py.contains("\"\"\"Primary colors\"\"\""),
1039            "missing doc: {py}"
1040        );
1041        assert!(py.contains("Red = 0"), "missing Red: {py}");
1042        assert!(py.contains("Green = 1"), "missing Green: {py}");
1043        assert!(py.contains("Blue = 2"), "missing Blue: {py}");
1044    }
1045
1046    #[test]
1047    fn enum_param_and_return() {
1048        let api = make_api(vec![Module {
1049            name: "paint".into(),
1050            functions: vec![Function {
1051                name: "mix".into(),
1052                params: vec![Param {
1053                    name: "a".into(),
1054                    ty: TypeRef::Enum("Color".into()),
1055                }],
1056                returns: Some(TypeRef::Enum("Color".into())),
1057                doc: None,
1058                r#async: false,
1059            }],
1060            structs: vec![],
1061            enums: vec![],
1062            errors: None,
1063        }]);
1064
1065        let py = render_python_module(&api);
1066        assert!(py.contains("a: \"Color\""), "missing enum param hint: {py}");
1067        assert!(
1068            py.contains("-> \"Color\":"),
1069            "missing enum return hint: {py}"
1070        );
1071        assert!(py.contains("a.value"), "missing .value conversion: {py}");
1072        assert!(
1073            py.contains("return Color(_result)"),
1074            "missing enum return wrap: {py}"
1075        );
1076    }
1077
1078    #[test]
1079    fn struct_class_with_getters() {
1080        let api = make_api(vec![Module {
1081            name: "contacts".into(),
1082            functions: vec![],
1083            structs: vec![StructDef {
1084                name: "Contact".into(),
1085                doc: None,
1086                fields: vec![
1087                    StructField {
1088                        name: "name".into(),
1089                        ty: TypeRef::StringUtf8,
1090                        doc: None,
1091                    },
1092                    StructField {
1093                        name: "age".into(),
1094                        ty: TypeRef::I32,
1095                        doc: None,
1096                    },
1097                ],
1098            }],
1099            enums: vec![],
1100            errors: None,
1101        }]);
1102
1103        let py = render_python_module(&api);
1104        assert!(py.contains("class Contact:"), "missing class: {py}");
1105        assert!(
1106            py.contains("def __init__(self, _ptr: int)"),
1107            "missing __init__: {py}"
1108        );
1109        assert!(
1110            py.contains("self._ptr = _ptr"),
1111            "missing _ptr assignment: {py}"
1112        );
1113        assert!(py.contains("def __del__(self)"), "missing __del__: {py}");
1114        assert!(
1115            py.contains("weaveffi_contacts_Contact_destroy"),
1116            "missing destroy call: {py}"
1117        );
1118        assert!(
1119            py.contains("def name(self) -> str:"),
1120            "missing name getter: {py}"
1121        );
1122        assert!(
1123            py.contains("weaveffi_contacts_Contact_get_name"),
1124            "missing name getter C call: {py}"
1125        );
1126        assert!(
1127            py.contains("_bytes_to_string(_result)"),
1128            "missing _bytes_to_string in getter: {py}"
1129        );
1130        assert!(
1131            py.contains("def age(self) -> int:"),
1132            "missing age getter: {py}"
1133        );
1134        assert!(
1135            py.contains("weaveffi_contacts_Contact_get_age"),
1136            "missing age getter C call: {py}"
1137        );
1138    }
1139
1140    #[test]
1141    fn struct_return() {
1142        let api = make_api(vec![Module {
1143            name: "contacts".into(),
1144            functions: vec![Function {
1145                name: "get_contact".into(),
1146                params: vec![Param {
1147                    name: "id".into(),
1148                    ty: TypeRef::Handle,
1149                }],
1150                returns: Some(TypeRef::Struct("Contact".into())),
1151                doc: None,
1152                r#async: false,
1153            }],
1154            structs: vec![],
1155            enums: vec![],
1156            errors: None,
1157        }]);
1158
1159        let py = render_python_module(&api);
1160        assert!(
1161            py.contains("-> \"Contact\":"),
1162            "missing struct return hint: {py}"
1163        );
1164        assert!(
1165            py.contains("ctypes.c_void_p"),
1166            "missing void_p for struct: {py}"
1167        );
1168        assert!(
1169            py.contains("return Contact(_result)"),
1170            "missing struct wrapping: {py}"
1171        );
1172    }
1173
1174    #[test]
1175    fn bool_uses_c_int32() {
1176        let api = make_api(vec![simple_module(vec![Function {
1177            name: "is_valid".into(),
1178            params: vec![Param {
1179                name: "flag".into(),
1180                ty: TypeRef::Bool,
1181            }],
1182            returns: Some(TypeRef::Bool),
1183            doc: None,
1184            r#async: false,
1185        }])]);
1186
1187        let py = render_python_module(&api);
1188        assert!(py.contains("flag: bool"), "missing bool param: {py}");
1189        assert!(py.contains("-> bool:"), "missing bool return: {py}");
1190        assert!(
1191            py.contains("ctypes.c_int32"),
1192            "missing c_int32 for Bool: {py}"
1193        );
1194        assert!(
1195            py.contains("1 if flag else 0"),
1196            "missing bool-to-int conversion: {py}"
1197        );
1198        assert!(
1199            py.contains("return bool(_result)"),
1200            "missing int-to-bool conversion: {py}"
1201        );
1202    }
1203
1204    #[test]
1205    fn handle_uses_c_uint64() {
1206        let api = make_api(vec![simple_module(vec![Function {
1207            name: "create".into(),
1208            params: vec![],
1209            returns: Some(TypeRef::Handle),
1210            doc: None,
1211            r#async: false,
1212        }])]);
1213
1214        let py = render_python_module(&api);
1215        assert!(
1216            py.contains("ctypes.c_uint64"),
1217            "missing c_uint64 for Handle: {py}"
1218        );
1219    }
1220
1221    #[test]
1222    fn bytes_param_and_return() {
1223        let api = make_api(vec![Module {
1224            name: "store".into(),
1225            functions: vec![Function {
1226                name: "process".into(),
1227                params: vec![Param {
1228                    name: "data".into(),
1229                    ty: TypeRef::Bytes,
1230                }],
1231                returns: Some(TypeRef::Bytes),
1232                doc: None,
1233                r#async: false,
1234            }],
1235            structs: vec![],
1236            enums: vec![],
1237            errors: None,
1238        }]);
1239
1240        let py = render_python_module(&api);
1241        assert!(py.contains("data: bytes"), "missing bytes param: {py}");
1242        assert!(py.contains("-> bytes:"), "missing bytes return: {py}");
1243        assert!(
1244            py.contains("ctypes.POINTER(ctypes.c_uint8)"),
1245            "missing uint8 pointer: {py}"
1246        );
1247        assert!(py.contains("ctypes.c_size_t"), "missing size_t: {py}");
1248        assert!(py.contains("_out_len"), "missing out_len: {py}");
1249    }
1250
1251    #[test]
1252    fn optional_value_param_and_return() {
1253        let api = make_api(vec![Module {
1254            name: "store".into(),
1255            functions: vec![Function {
1256                name: "find".into(),
1257                params: vec![Param {
1258                    name: "id".into(),
1259                    ty: TypeRef::Optional(Box::new(TypeRef::I32)),
1260                }],
1261                returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))),
1262                doc: None,
1263                r#async: false,
1264            }],
1265            structs: vec![],
1266            enums: vec![],
1267            errors: None,
1268        }]);
1269
1270        let py = render_python_module(&api);
1271        assert!(
1272            py.contains("id: Optional[int]"),
1273            "missing optional param: {py}"
1274        );
1275        assert!(
1276            py.contains("-> Optional[int]:"),
1277            "missing optional return: {py}"
1278        );
1279        assert!(
1280            py.contains("ctypes.POINTER(ctypes.c_int32)"),
1281            "missing POINTER for optional: {py}"
1282        );
1283        assert!(
1284            py.contains("ctypes.byref(ctypes.c_int32(id)) if id is not None else None"),
1285            "missing optional param conversion: {py}"
1286        );
1287        assert!(py.contains("return None"), "missing None return path: {py}");
1288        assert!(
1289            py.contains("return _result[0]"),
1290            "missing pointer deref: {py}"
1291        );
1292    }
1293
1294    #[test]
1295    fn optional_string_return() {
1296        let api = make_api(vec![Module {
1297            name: "store".into(),
1298            functions: vec![Function {
1299                name: "get_name".into(),
1300                params: vec![],
1301                returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))),
1302                doc: None,
1303                r#async: false,
1304            }],
1305            structs: vec![],
1306            enums: vec![],
1307            errors: None,
1308        }]);
1309
1310        let py = render_python_module(&api);
1311        assert!(
1312            py.contains("-> Optional[str]:"),
1313            "missing optional str return: {py}"
1314        );
1315        assert!(
1316            py.contains("return _bytes_to_string(_result)"),
1317            "missing _bytes_to_string for optional string: {py}"
1318        );
1319    }
1320
1321    #[test]
1322    fn list_param_and_return() {
1323        let api = make_api(vec![Module {
1324            name: "batch".into(),
1325            functions: vec![
1326                Function {
1327                    name: "process".into(),
1328                    params: vec![Param {
1329                        name: "ids".into(),
1330                        ty: TypeRef::List(Box::new(TypeRef::I32)),
1331                    }],
1332                    returns: None,
1333                    doc: None,
1334                    r#async: false,
1335                },
1336                Function {
1337                    name: "get_ids".into(),
1338                    params: vec![],
1339                    returns: Some(TypeRef::List(Box::new(TypeRef::I32))),
1340                    doc: None,
1341                    r#async: false,
1342                },
1343            ],
1344            structs: vec![],
1345            enums: vec![],
1346            errors: None,
1347        }]);
1348
1349        let py = render_python_module(&api);
1350        assert!(py.contains("ids: List[int]"), "missing list param: {py}");
1351        assert!(py.contains("-> List[int]:"), "missing list return: {py}");
1352        assert!(
1353            py.contains("ctypes.c_int32 * len(ids)"),
1354            "missing ctypes array creation: {py}"
1355        );
1356        assert!(
1357            py.contains("_out_len"),
1358            "missing out_len for list return: {py}"
1359        );
1360        assert!(
1361            py.contains("for _i in range(_out_len.value)"),
1362            "missing list iteration: {py}"
1363        );
1364    }
1365
1366    #[test]
1367    fn map_param_and_return() {
1368        let api = make_api(vec![Module {
1369            name: "store".into(),
1370            functions: vec![
1371                Function {
1372                    name: "update".into(),
1373                    params: vec![Param {
1374                        name: "scores".into(),
1375                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
1376                    }],
1377                    returns: None,
1378                    doc: None,
1379                    r#async: false,
1380                },
1381                Function {
1382                    name: "get_scores".into(),
1383                    params: vec![],
1384                    returns: Some(TypeRef::Map(
1385                        Box::new(TypeRef::StringUtf8),
1386                        Box::new(TypeRef::I32),
1387                    )),
1388                    doc: None,
1389                    r#async: false,
1390                },
1391            ],
1392            structs: vec![],
1393            enums: vec![],
1394            errors: None,
1395        }]);
1396
1397        let py = render_python_module(&api);
1398        assert!(
1399            py.contains("scores: Dict[str, int]"),
1400            "missing map param: {py}"
1401        );
1402        assert!(
1403            py.contains("-> Dict[str, int]:"),
1404            "missing map return: {py}"
1405        );
1406        assert!(
1407            py.contains("list(scores.keys())"),
1408            "missing keys extraction: {py}"
1409        );
1410        assert!(py.contains("_out_keys"), "missing out_keys: {py}");
1411        assert!(py.contains("_out_values"), "missing out_values: {py}");
1412    }
1413
1414    #[test]
1415    fn struct_optional_string_getter() {
1416        let api = make_api(vec![Module {
1417            name: "contacts".into(),
1418            functions: vec![],
1419            structs: vec![StructDef {
1420                name: "Contact".into(),
1421                doc: None,
1422                fields: vec![StructField {
1423                    name: "email".into(),
1424                    ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1425                    doc: None,
1426                }],
1427            }],
1428            enums: vec![],
1429            errors: None,
1430        }]);
1431
1432        let py = render_python_module(&api);
1433        assert!(
1434            py.contains("def email(self) -> Optional[str]:"),
1435            "missing optional getter: {py}"
1436        );
1437        assert!(
1438            py.contains("_bytes_to_string(_result)"),
1439            "missing _bytes_to_string in optional getter: {py}"
1440        );
1441    }
1442
1443    #[test]
1444    fn struct_enum_field_getter() {
1445        let api = make_api(vec![Module {
1446            name: "contacts".into(),
1447            functions: vec![],
1448            structs: vec![StructDef {
1449                name: "Contact".into(),
1450                doc: None,
1451                fields: vec![StructField {
1452                    name: "role".into(),
1453                    ty: TypeRef::Enum("Role".into()),
1454                    doc: None,
1455                }],
1456            }],
1457            enums: vec![],
1458            errors: None,
1459        }]);
1460
1461        let py = render_python_module(&api);
1462        assert!(
1463            py.contains("def role(self) -> \"Role\":"),
1464            "missing enum getter: {py}"
1465        );
1466        assert!(
1467            py.contains("return Role(_result)"),
1468            "missing enum wrapping in getter: {py}"
1469        );
1470    }
1471
1472    #[test]
1473    fn comprehensive_contacts_api() {
1474        let api = make_api(vec![Module {
1475            name: "contacts".into(),
1476            enums: vec![EnumDef {
1477                name: "ContactType".into(),
1478                doc: None,
1479                variants: vec![
1480                    EnumVariant {
1481                        name: "Personal".into(),
1482                        value: 0,
1483                        doc: None,
1484                    },
1485                    EnumVariant {
1486                        name: "Work".into(),
1487                        value: 1,
1488                        doc: None,
1489                    },
1490                ],
1491            }],
1492            structs: vec![StructDef {
1493                name: "Contact".into(),
1494                doc: None,
1495                fields: vec![
1496                    StructField {
1497                        name: "id".into(),
1498                        ty: TypeRef::I64,
1499                        doc: None,
1500                    },
1501                    StructField {
1502                        name: "first_name".into(),
1503                        ty: TypeRef::StringUtf8,
1504                        doc: None,
1505                    },
1506                    StructField {
1507                        name: "email".into(),
1508                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1509                        doc: None,
1510                    },
1511                    StructField {
1512                        name: "contact_type".into(),
1513                        ty: TypeRef::Enum("ContactType".into()),
1514                        doc: None,
1515                    },
1516                ],
1517            }],
1518            functions: vec![
1519                Function {
1520                    name: "create_contact".into(),
1521                    params: vec![
1522                        Param {
1523                            name: "first_name".into(),
1524                            ty: TypeRef::StringUtf8,
1525                        },
1526                        Param {
1527                            name: "email".into(),
1528                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1529                        },
1530                        Param {
1531                            name: "contact_type".into(),
1532                            ty: TypeRef::Enum("ContactType".into()),
1533                        },
1534                    ],
1535                    returns: Some(TypeRef::Handle),
1536                    doc: None,
1537                    r#async: false,
1538                },
1539                Function {
1540                    name: "get_contact".into(),
1541                    params: vec![Param {
1542                        name: "id".into(),
1543                        ty: TypeRef::Handle,
1544                    }],
1545                    returns: Some(TypeRef::Struct("Contact".into())),
1546                    doc: None,
1547                    r#async: false,
1548                },
1549                Function {
1550                    name: "list_contacts".into(),
1551                    params: vec![],
1552                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1553                    doc: None,
1554                    r#async: false,
1555                },
1556                Function {
1557                    name: "count_contacts".into(),
1558                    params: vec![],
1559                    returns: Some(TypeRef::I32),
1560                    doc: None,
1561                    r#async: false,
1562                },
1563            ],
1564            errors: None,
1565        }]);
1566
1567        let tmp = std::env::temp_dir().join("weaveffi_test_python_gen_contacts");
1568        let _ = std::fs::remove_dir_all(&tmp);
1569        std::fs::create_dir_all(&tmp).unwrap();
1570        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
1571
1572        PythonGenerator.generate(&api, out_dir).unwrap();
1573
1574        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
1575
1576        assert!(py.contains("class ContactType(IntEnum):"));
1577        assert!(py.contains("Personal = 0"));
1578        assert!(py.contains("Work = 1"));
1579
1580        assert!(py.contains("class Contact:"));
1581        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
1582        assert!(py.contains("def id(self) -> int:"));
1583        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
1584        assert!(py.contains("def first_name(self) -> str:"));
1585        assert!(py.contains("def email(self) -> Optional[str]:"));
1586        assert!(py.contains("def contact_type(self) -> \"ContactType\":"));
1587
1588        assert!(py.contains("def create_contact("));
1589        assert!(py.contains("weaveffi_contacts_create_contact"));
1590        assert!(py.contains("def get_contact(id: int) -> \"Contact\":"));
1591        assert!(py.contains("def list_contacts() -> List[\"Contact\"]:"));
1592        assert!(py.contains("def count_contacts() -> int:"));
1593
1594        let _ = std::fs::remove_dir_all(&tmp);
1595    }
1596
1597    #[test]
1598    fn type_hint_mapping() {
1599        assert_eq!(py_type_hint(&TypeRef::I32), "int");
1600        assert_eq!(py_type_hint(&TypeRef::U32), "int");
1601        assert_eq!(py_type_hint(&TypeRef::I64), "int");
1602        assert_eq!(py_type_hint(&TypeRef::F64), "float");
1603        assert_eq!(py_type_hint(&TypeRef::Bool), "bool");
1604        assert_eq!(py_type_hint(&TypeRef::StringUtf8), "str");
1605        assert_eq!(py_type_hint(&TypeRef::Bytes), "bytes");
1606        assert_eq!(py_type_hint(&TypeRef::Handle), "int");
1607        assert_eq!(py_type_hint(&TypeRef::Struct("Foo".into())), "\"Foo\"");
1608        assert_eq!(py_type_hint(&TypeRef::Enum("Bar".into())), "\"Bar\"");
1609        assert_eq!(
1610            py_type_hint(&TypeRef::Optional(Box::new(TypeRef::I32))),
1611            "Optional[int]"
1612        );
1613        assert_eq!(
1614            py_type_hint(&TypeRef::List(Box::new(TypeRef::I32))),
1615            "List[int]"
1616        );
1617        assert_eq!(
1618            py_type_hint(&TypeRef::Map(
1619                Box::new(TypeRef::StringUtf8),
1620                Box::new(TypeRef::I32)
1621            )),
1622            "Dict[str, int]"
1623        );
1624    }
1625
1626    #[test]
1627    fn ctypes_scalar_mapping() {
1628        assert_eq!(py_ctypes_scalar(&TypeRef::I32), "ctypes.c_int32");
1629        assert_eq!(py_ctypes_scalar(&TypeRef::U32), "ctypes.c_uint32");
1630        assert_eq!(py_ctypes_scalar(&TypeRef::I64), "ctypes.c_int64");
1631        assert_eq!(py_ctypes_scalar(&TypeRef::F64), "ctypes.c_double");
1632        assert_eq!(py_ctypes_scalar(&TypeRef::Bool), "ctypes.c_int32");
1633        assert_eq!(py_ctypes_scalar(&TypeRef::StringUtf8), "ctypes.c_char_p");
1634        assert_eq!(py_ctypes_scalar(&TypeRef::Handle), "ctypes.c_uint64");
1635        assert_eq!(py_ctypes_scalar(&TypeRef::Bytes), "ctypes.c_uint8");
1636        assert_eq!(
1637            py_ctypes_scalar(&TypeRef::Struct("X".into())),
1638            "ctypes.c_void_p"
1639        );
1640        assert_eq!(
1641            py_ctypes_scalar(&TypeRef::Enum("X".into())),
1642            "ctypes.c_int32"
1643        );
1644    }
1645
1646    #[test]
1647    fn list_struct_return() {
1648        let api = make_api(vec![Module {
1649            name: "store".into(),
1650            functions: vec![Function {
1651                name: "list_items".into(),
1652                params: vec![],
1653                returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Item".into())))),
1654                doc: None,
1655                r#async: false,
1656            }],
1657            structs: vec![],
1658            enums: vec![],
1659            errors: None,
1660        }]);
1661
1662        let py = render_python_module(&api);
1663        assert!(
1664            py.contains("-> List[\"Item\"]:"),
1665            "missing list struct return: {py}"
1666        );
1667        assert!(
1668            py.contains("Item(_result[_i])"),
1669            "missing struct wrapping in list: {py}"
1670        );
1671    }
1672
1673    #[test]
1674    fn struct_bytes_field_getter() {
1675        let api = make_api(vec![Module {
1676            name: "storage".into(),
1677            functions: vec![],
1678            structs: vec![StructDef {
1679                name: "Blob".into(),
1680                doc: None,
1681                fields: vec![StructField {
1682                    name: "data".into(),
1683                    ty: TypeRef::Bytes,
1684                    doc: None,
1685                }],
1686            }],
1687            enums: vec![],
1688            errors: None,
1689        }]);
1690
1691        let py = render_python_module(&api);
1692        assert!(
1693            py.contains("def data(self) -> bytes:"),
1694            "missing bytes getter: {py}"
1695        );
1696        assert!(
1697            py.contains("_out_len = ctypes.c_size_t(0)"),
1698            "missing out_len in bytes getter: {py}"
1699        );
1700        assert!(
1701            py.contains("_result[:_out_len.value]"),
1702            "missing bytes slice: {py}"
1703        );
1704    }
1705
1706    #[test]
1707    fn python_generates_type_stubs() {
1708        let api = make_api(vec![Module {
1709            name: "contacts".into(),
1710            enums: vec![EnumDef {
1711                name: "ContactType".into(),
1712                doc: None,
1713                variants: vec![
1714                    EnumVariant {
1715                        name: "Personal".into(),
1716                        value: 0,
1717                        doc: None,
1718                    },
1719                    EnumVariant {
1720                        name: "Work".into(),
1721                        value: 1,
1722                        doc: None,
1723                    },
1724                ],
1725            }],
1726            structs: vec![StructDef {
1727                name: "Contact".into(),
1728                doc: None,
1729                fields: vec![
1730                    StructField {
1731                        name: "id".into(),
1732                        ty: TypeRef::I64,
1733                        doc: None,
1734                    },
1735                    StructField {
1736                        name: "name".into(),
1737                        ty: TypeRef::StringUtf8,
1738                        doc: None,
1739                    },
1740                    StructField {
1741                        name: "email".into(),
1742                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1743                        doc: None,
1744                    },
1745                    StructField {
1746                        name: "tags".into(),
1747                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
1748                        doc: None,
1749                    },
1750                    StructField {
1751                        name: "metadata".into(),
1752                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
1753                        doc: None,
1754                    },
1755                ],
1756            }],
1757            functions: vec![
1758                Function {
1759                    name: "create_contact".into(),
1760                    params: vec![
1761                        Param {
1762                            name: "name".into(),
1763                            ty: TypeRef::StringUtf8,
1764                        },
1765                        Param {
1766                            name: "email".into(),
1767                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1768                        },
1769                    ],
1770                    returns: Some(TypeRef::Handle),
1771                    doc: None,
1772                    r#async: false,
1773                },
1774                Function {
1775                    name: "get_contact".into(),
1776                    params: vec![Param {
1777                        name: "id".into(),
1778                        ty: TypeRef::Handle,
1779                    }],
1780                    returns: Some(TypeRef::Struct("Contact".into())),
1781                    doc: None,
1782                    r#async: false,
1783                },
1784                Function {
1785                    name: "delete_contact".into(),
1786                    params: vec![Param {
1787                        name: "id".into(),
1788                        ty: TypeRef::Handle,
1789                    }],
1790                    returns: None,
1791                    doc: None,
1792                    r#async: false,
1793                },
1794            ],
1795            errors: None,
1796        }]);
1797
1798        let tmp = std::env::temp_dir().join("weaveffi_test_python_pyi");
1799        let _ = std::fs::remove_dir_all(&tmp);
1800        std::fs::create_dir_all(&tmp).unwrap();
1801        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
1802
1803        PythonGenerator.generate(&api, out_dir).unwrap();
1804
1805        let pyi_path = tmp.join("python/weaveffi/weaveffi.pyi");
1806        assert!(pyi_path.exists(), ".pyi file must exist");
1807
1808        let pyi = std::fs::read_to_string(&pyi_path).unwrap();
1809
1810        assert!(
1811            pyi.contains("from enum import IntEnum"),
1812            "missing IntEnum import"
1813        );
1814        assert!(
1815            pyi.contains("from typing import Dict, List, Optional"),
1816            "missing typing imports"
1817        );
1818
1819        assert!(
1820            pyi.contains("class ContactType(IntEnum):"),
1821            "missing enum stub"
1822        );
1823        assert!(
1824            pyi.contains("    Personal: int"),
1825            "missing enum variant Personal"
1826        );
1827        assert!(pyi.contains("    Work: int"), "missing enum variant Work");
1828
1829        assert!(pyi.contains("class Contact:"), "missing struct stub");
1830        assert!(
1831            pyi.contains("    def id(self) -> int: ..."),
1832            "missing id property: {pyi}"
1833        );
1834        assert!(
1835            pyi.contains("    def name(self) -> str: ..."),
1836            "missing name property: {pyi}"
1837        );
1838        assert!(
1839            pyi.contains("    def email(self) -> Optional[str]: ..."),
1840            "missing email property: {pyi}"
1841        );
1842        assert!(
1843            pyi.contains("    def tags(self) -> List[str]: ..."),
1844            "missing tags property: {pyi}"
1845        );
1846        assert!(
1847            pyi.contains("    def metadata(self) -> Dict[str, int]: ..."),
1848            "missing metadata property: {pyi}"
1849        );
1850
1851        assert!(
1852            pyi.contains("def create_contact(name: str, email: Optional[str]) -> int: ..."),
1853            "missing create_contact stub: {pyi}"
1854        );
1855        assert!(
1856            pyi.contains("def get_contact(id: int) -> \"Contact\": ..."),
1857            "missing get_contact stub: {pyi}"
1858        );
1859        assert!(
1860            pyi.contains("def delete_contact(id: int) -> None: ..."),
1861            "missing delete_contact stub: {pyi}"
1862        );
1863
1864        let _ = std::fs::remove_dir_all(&tmp);
1865    }
1866
1867    #[test]
1868    fn generate_python_basic() {
1869        let api = make_api(vec![simple_module(vec![Function {
1870            name: "add".into(),
1871            params: vec![
1872                Param {
1873                    name: "a".into(),
1874                    ty: TypeRef::I32,
1875                },
1876                Param {
1877                    name: "b".into(),
1878                    ty: TypeRef::I32,
1879                },
1880            ],
1881            returns: Some(TypeRef::I32),
1882            doc: None,
1883            r#async: false,
1884        }])]);
1885
1886        let tmp = std::env::temp_dir().join("weaveffi_test_py_basic");
1887        let _ = std::fs::remove_dir_all(&tmp);
1888        std::fs::create_dir_all(&tmp).unwrap();
1889        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
1890
1891        PythonGenerator.generate(&api, out_dir).unwrap();
1892
1893        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
1894
1895        assert!(py.contains("def add(a: int, b: int) -> int:"));
1896        assert!(py.contains("_fn = _lib.weaveffi_math_add"));
1897        assert!(py.contains("ctypes.c_int32, ctypes.c_int32"));
1898        assert!(py.contains("_fn.restype = ctypes.c_int32"));
1899        assert!(py.contains("_err = _WeaveffiErrorStruct()"));
1900        assert!(py.contains("_check_error(_err)"));
1901        assert!(py.contains("return _result"));
1902
1903        assert!(py.contains("import ctypes"));
1904        assert!(py.contains("from enum import IntEnum"));
1905        assert!(py.contains("from typing import Dict, List, Optional"));
1906        assert!(py.contains("class WeaveffiError(Exception):"));
1907        assert!(py.contains("def _load_library()"));
1908        assert!(py.contains("_lib = _load_library()"));
1909
1910        let _ = std::fs::remove_dir_all(&tmp);
1911    }
1912
1913    #[test]
1914    fn generate_python_with_structs() {
1915        let api = make_api(vec![Module {
1916            name: "contacts".into(),
1917            functions: vec![],
1918            structs: vec![StructDef {
1919                name: "Contact".into(),
1920                doc: Some("A contact record".into()),
1921                fields: vec![
1922                    StructField {
1923                        name: "id".into(),
1924                        ty: TypeRef::I64,
1925                        doc: None,
1926                    },
1927                    StructField {
1928                        name: "first_name".into(),
1929                        ty: TypeRef::StringUtf8,
1930                        doc: None,
1931                    },
1932                    StructField {
1933                        name: "last_name".into(),
1934                        ty: TypeRef::StringUtf8,
1935                        doc: None,
1936                    },
1937                    StructField {
1938                        name: "email".into(),
1939                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1940                        doc: None,
1941                    },
1942                ],
1943            }],
1944            enums: vec![],
1945            errors: None,
1946        }]);
1947
1948        let py = render_python_module(&api);
1949
1950        assert!(py.contains("class Contact:"), "missing class decl");
1951        assert!(
1952            py.contains("\"\"\"A contact record\"\"\""),
1953            "missing doc: {py}"
1954        );
1955        assert!(py.contains("def __init__(self, _ptr: int) -> None:"));
1956        assert!(py.contains("self._ptr = _ptr"));
1957        assert!(py.contains("def __del__(self) -> None:"));
1958        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
1959
1960        assert!(py.contains("@property\n    def id(self) -> int:"));
1961        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
1962        assert!(py.contains("_fn.restype = ctypes.c_int64"));
1963
1964        assert!(py.contains("@property\n    def first_name(self) -> str:"));
1965        assert!(py.contains("weaveffi_contacts_Contact_get_first_name"));
1966
1967        assert!(py.contains("@property\n    def last_name(self) -> str:"));
1968        assert!(py.contains("weaveffi_contacts_Contact_get_last_name"));
1969
1970        assert!(py.contains("@property\n    def email(self) -> Optional[str]:"));
1971        assert!(py.contains("weaveffi_contacts_Contact_get_email"));
1972    }
1973
1974    #[test]
1975    fn generate_python_with_enums() {
1976        let api = make_api(vec![Module {
1977            name: "contacts".into(),
1978            functions: vec![Function {
1979                name: "get_type".into(),
1980                params: vec![Param {
1981                    name: "ct".into(),
1982                    ty: TypeRef::Enum("ContactType".into()),
1983                }],
1984                returns: Some(TypeRef::Enum("ContactType".into())),
1985                doc: None,
1986                r#async: false,
1987            }],
1988            structs: vec![],
1989            enums: vec![EnumDef {
1990                name: "ContactType".into(),
1991                doc: Some("Type of contact".into()),
1992                variants: vec![
1993                    EnumVariant {
1994                        name: "Personal".into(),
1995                        value: 0,
1996                        doc: None,
1997                    },
1998                    EnumVariant {
1999                        name: "Work".into(),
2000                        value: 1,
2001                        doc: None,
2002                    },
2003                    EnumVariant {
2004                        name: "Other".into(),
2005                        value: 2,
2006                        doc: None,
2007                    },
2008                ],
2009            }],
2010            errors: None,
2011        }]);
2012
2013        let py = render_python_module(&api);
2014
2015        assert!(py.contains("class ContactType(IntEnum):"));
2016        assert!(py.contains("\"\"\"Type of contact\"\"\""));
2017        assert!(py.contains("Personal = 0"));
2018        assert!(py.contains("Work = 1"));
2019        assert!(py.contains("Other = 2"));
2020
2021        assert!(
2022            py.contains("ct: \"ContactType\""),
2023            "missing enum param hint"
2024        );
2025        assert!(
2026            py.contains("-> \"ContactType\":"),
2027            "missing enum return hint"
2028        );
2029        assert!(py.contains("ct.value"), "missing .value for enum param");
2030        assert!(
2031            py.contains("return ContactType(_result)"),
2032            "missing enum return wrap"
2033        );
2034        assert!(py.contains("ctypes.c_int32"), "enum should use c_int32 ABI");
2035    }
2036
2037    #[test]
2038    fn generate_python_with_optionals() {
2039        let api = make_api(vec![Module {
2040            name: "store".into(),
2041            functions: vec![
2042                Function {
2043                    name: "find_int".into(),
2044                    params: vec![Param {
2045                        name: "key".into(),
2046                        ty: TypeRef::Optional(Box::new(TypeRef::I32)),
2047                    }],
2048                    returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))),
2049                    doc: None,
2050                    r#async: false,
2051                },
2052                Function {
2053                    name: "find_name".into(),
2054                    params: vec![Param {
2055                        name: "prefix".into(),
2056                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2057                    }],
2058                    returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))),
2059                    doc: None,
2060                    r#async: false,
2061                },
2062                Function {
2063                    name: "find_contact".into(),
2064                    params: vec![],
2065                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
2066                        "Contact".into(),
2067                    )))),
2068                    doc: None,
2069                    r#async: false,
2070                },
2071                Function {
2072                    name: "find_flag".into(),
2073                    params: vec![],
2074                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Bool))),
2075                    doc: None,
2076                    r#async: false,
2077                },
2078            ],
2079            structs: vec![],
2080            enums: vec![],
2081            errors: None,
2082        }]);
2083
2084        let py = render_python_module(&api);
2085
2086        assert!(
2087            py.contains("key: Optional[int]"),
2088            "missing Optional[int] param"
2089        );
2090        assert!(
2091            py.contains("-> Optional[int]:"),
2092            "missing Optional[int] return"
2093        );
2094        assert!(
2095            py.contains("ctypes.byref(ctypes.c_int32(key)) if key is not None else None"),
2096            "missing optional i32 conversion"
2097        );
2098        assert!(
2099            py.contains("ctypes.POINTER(ctypes.c_int32)"),
2100            "missing POINTER for optional i32"
2101        );
2102
2103        assert!(
2104            py.contains("prefix: Optional[str]"),
2105            "missing Optional[str] param"
2106        );
2107        assert!(
2108            py.contains("-> Optional[str]:"),
2109            "missing Optional[str] return"
2110        );
2111        assert!(
2112            py.contains("_string_to_bytes(prefix)"),
2113            "missing optional _string_to_bytes"
2114        );
2115
2116        assert!(
2117            py.contains("-> Optional[\"Contact\"]:"),
2118            "missing Optional struct return"
2119        );
2120        assert!(
2121            py.contains("if _result is None:\n        return None\n    return Contact(_result)"),
2122            "missing optional struct None check"
2123        );
2124
2125        assert!(
2126            py.contains("-> Optional[bool]:"),
2127            "missing Optional[bool] return"
2128        );
2129        assert!(
2130            py.contains("return bool(_result[0])"),
2131            "missing optional bool deref"
2132        );
2133    }
2134
2135    #[test]
2136    fn generate_python_with_lists() {
2137        let api = make_api(vec![Module {
2138            name: "batch".into(),
2139            functions: vec![
2140                Function {
2141                    name: "process_ids".into(),
2142                    params: vec![Param {
2143                        name: "ids".into(),
2144                        ty: TypeRef::List(Box::new(TypeRef::I32)),
2145                    }],
2146                    returns: None,
2147                    doc: None,
2148                    r#async: false,
2149                },
2150                Function {
2151                    name: "get_names".into(),
2152                    params: vec![],
2153                    returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
2154                    doc: None,
2155                    r#async: false,
2156                },
2157                Function {
2158                    name: "get_items".into(),
2159                    params: vec![],
2160                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Item".into())))),
2161                    doc: None,
2162                    r#async: false,
2163                },
2164            ],
2165            structs: vec![],
2166            enums: vec![],
2167            errors: None,
2168        }]);
2169
2170        let py = render_python_module(&api);
2171
2172        assert!(py.contains("ids: List[int]"), "missing List[int] param");
2173        assert!(
2174            py.contains("(ctypes.c_int32 * len(ids))(*ids)"),
2175            "missing list-to-array conversion"
2176        );
2177        assert!(
2178            py.contains("ctypes.POINTER(ctypes.c_int32)"),
2179            "missing POINTER for list param"
2180        );
2181        assert!(py.contains("ctypes.c_size_t"), "missing size_t for length");
2182
2183        assert!(
2184            py.contains("-> List[str]:"),
2185            "missing List[str] return: {py}"
2186        );
2187        assert!(
2188            py.contains("_bytes_to_string(_result[_i]) for _i in range(_out_len.value)"),
2189            "missing string list _bytes_to_string: {py}"
2190        );
2191
2192        assert!(
2193            py.contains("-> List[\"Item\"]:"),
2194            "missing List struct return"
2195        );
2196        assert!(
2197            py.contains("Item(_result[_i]) for _i in range(_out_len.value)"),
2198            "missing struct wrapping in list"
2199        );
2200    }
2201
2202    #[test]
2203    fn generate_python_with_maps() {
2204        let api = make_api(vec![Module {
2205            name: "config".into(),
2206            functions: vec![
2207                Function {
2208                    name: "set_config".into(),
2209                    params: vec![Param {
2210                        name: "settings".into(),
2211                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
2212                    }],
2213                    returns: None,
2214                    doc: None,
2215                    r#async: false,
2216                },
2217                Function {
2218                    name: "get_config".into(),
2219                    params: vec![],
2220                    returns: Some(TypeRef::Map(
2221                        Box::new(TypeRef::StringUtf8),
2222                        Box::new(TypeRef::I32),
2223                    )),
2224                    doc: None,
2225                    r#async: false,
2226                },
2227            ],
2228            structs: vec![],
2229            enums: vec![],
2230            errors: None,
2231        }]);
2232
2233        let py = render_python_module(&api);
2234
2235        assert!(
2236            py.contains("settings: Dict[str, int]"),
2237            "missing Dict param hint"
2238        );
2239        assert!(
2240            py.contains("list(settings.keys())"),
2241            "missing keys extraction"
2242        );
2243        assert!(
2244            py.contains("_settings_vals = [settings[_k] for _k in _settings_keys]"),
2245            "missing values extraction"
2246        );
2247        assert!(
2248            py.contains("ctypes.c_char_p * len(_settings_keys)"),
2249            "missing key array creation"
2250        );
2251        assert!(
2252            py.contains("ctypes.c_int32 * len(_settings_vals)"),
2253            "missing value array creation"
2254        );
2255
2256        assert!(
2257            py.contains("-> Dict[str, int]:"),
2258            "missing Dict return hint"
2259        );
2260        assert!(
2261            py.contains("_out_keys = ctypes.POINTER(ctypes.c_char_p)()"),
2262            "missing out_keys init"
2263        );
2264        assert!(
2265            py.contains("_out_values = ctypes.POINTER(ctypes.c_int32)()"),
2266            "missing out_values init"
2267        );
2268        assert!(
2269            py.contains("_out_len = ctypes.c_size_t(0)"),
2270            "missing out_len init"
2271        );
2272        assert!(
2273            py.contains("if not _out_keys or not _out_values:"),
2274            "missing empty map check"
2275        );
2276        assert!(
2277            py.contains("_bytes_to_string(_out_keys[_i]): _out_values[_i]"),
2278            "missing map comprehension"
2279        );
2280    }
2281
2282    #[test]
2283    fn generate_python_pyi_types() {
2284        let api = make_api(vec![Module {
2285            name: "contacts".into(),
2286            enums: vec![EnumDef {
2287                name: "ContactType".into(),
2288                doc: None,
2289                variants: vec![
2290                    EnumVariant {
2291                        name: "Personal".into(),
2292                        value: 0,
2293                        doc: None,
2294                    },
2295                    EnumVariant {
2296                        name: "Work".into(),
2297                        value: 1,
2298                        doc: None,
2299                    },
2300                    EnumVariant {
2301                        name: "Other".into(),
2302                        value: 2,
2303                        doc: None,
2304                    },
2305                ],
2306            }],
2307            structs: vec![StructDef {
2308                name: "Contact".into(),
2309                doc: None,
2310                fields: vec![
2311                    StructField {
2312                        name: "id".into(),
2313                        ty: TypeRef::I64,
2314                        doc: None,
2315                    },
2316                    StructField {
2317                        name: "first_name".into(),
2318                        ty: TypeRef::StringUtf8,
2319                        doc: None,
2320                    },
2321                    StructField {
2322                        name: "email".into(),
2323                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2324                        doc: None,
2325                    },
2326                    StructField {
2327                        name: "tags".into(),
2328                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
2329                        doc: None,
2330                    },
2331                    StructField {
2332                        name: "scores".into(),
2333                        ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
2334                        doc: None,
2335                    },
2336                ],
2337            }],
2338            functions: vec![
2339                Function {
2340                    name: "create_contact".into(),
2341                    params: vec![
2342                        Param {
2343                            name: "name".into(),
2344                            ty: TypeRef::StringUtf8,
2345                        },
2346                        Param {
2347                            name: "email".into(),
2348                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2349                        },
2350                    ],
2351                    returns: Some(TypeRef::Handle),
2352                    doc: None,
2353                    r#async: false,
2354                },
2355                Function {
2356                    name: "get_contact".into(),
2357                    params: vec![Param {
2358                        name: "id".into(),
2359                        ty: TypeRef::Handle,
2360                    }],
2361                    returns: Some(TypeRef::Struct("Contact".into())),
2362                    doc: None,
2363                    r#async: false,
2364                },
2365                Function {
2366                    name: "list_contacts".into(),
2367                    params: vec![],
2368                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
2369                    doc: None,
2370                    r#async: false,
2371                },
2372                Function {
2373                    name: "delete_contact".into(),
2374                    params: vec![Param {
2375                        name: "id".into(),
2376                        ty: TypeRef::Handle,
2377                    }],
2378                    returns: None,
2379                    doc: None,
2380                    r#async: false,
2381                },
2382            ],
2383            errors: None,
2384        }]);
2385
2386        let pyi = render_pyi_module(&api);
2387
2388        assert!(pyi.contains("from enum import IntEnum"));
2389        assert!(pyi.contains("from typing import Dict, List, Optional"));
2390
2391        assert!(pyi.contains("class ContactType(IntEnum):"));
2392        assert!(pyi.contains("    Personal: int"));
2393        assert!(pyi.contains("    Work: int"));
2394        assert!(pyi.contains("    Other: int"));
2395
2396        assert!(pyi.contains("class Contact:"));
2397        assert!(pyi.contains("    def id(self) -> int: ..."));
2398        assert!(pyi.contains("    def first_name(self) -> str: ..."));
2399        assert!(pyi.contains("    def email(self) -> Optional[str]: ..."));
2400        assert!(pyi.contains("    def tags(self) -> List[str]: ..."));
2401        assert!(pyi.contains("    def scores(self) -> Dict[str, int]: ..."));
2402
2403        assert!(pyi.contains("def create_contact(name: str, email: Optional[str]) -> int: ..."));
2404        assert!(pyi.contains("def get_contact(id: int) -> \"Contact\": ..."));
2405        assert!(pyi.contains("def list_contacts() -> List[\"Contact\"]: ..."));
2406        assert!(pyi.contains("def delete_contact(id: int) -> None: ..."));
2407    }
2408
2409    #[test]
2410    fn generate_python_full_contacts() {
2411        let api = make_api(vec![Module {
2412            name: "contacts".into(),
2413            enums: vec![EnumDef {
2414                name: "ContactType".into(),
2415                doc: None,
2416                variants: vec![
2417                    EnumVariant {
2418                        name: "Personal".into(),
2419                        value: 0,
2420                        doc: None,
2421                    },
2422                    EnumVariant {
2423                        name: "Work".into(),
2424                        value: 1,
2425                        doc: None,
2426                    },
2427                    EnumVariant {
2428                        name: "Other".into(),
2429                        value: 2,
2430                        doc: None,
2431                    },
2432                ],
2433            }],
2434            structs: vec![StructDef {
2435                name: "Contact".into(),
2436                doc: None,
2437                fields: vec![
2438                    StructField {
2439                        name: "id".into(),
2440                        ty: TypeRef::I64,
2441                        doc: None,
2442                    },
2443                    StructField {
2444                        name: "first_name".into(),
2445                        ty: TypeRef::StringUtf8,
2446                        doc: None,
2447                    },
2448                    StructField {
2449                        name: "last_name".into(),
2450                        ty: TypeRef::StringUtf8,
2451                        doc: None,
2452                    },
2453                    StructField {
2454                        name: "email".into(),
2455                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2456                        doc: None,
2457                    },
2458                    StructField {
2459                        name: "contact_type".into(),
2460                        ty: TypeRef::Enum("ContactType".into()),
2461                        doc: None,
2462                    },
2463                ],
2464            }],
2465            functions: vec![
2466                Function {
2467                    name: "create_contact".into(),
2468                    params: vec![
2469                        Param {
2470                            name: "first_name".into(),
2471                            ty: TypeRef::StringUtf8,
2472                        },
2473                        Param {
2474                            name: "last_name".into(),
2475                            ty: TypeRef::StringUtf8,
2476                        },
2477                        Param {
2478                            name: "email".into(),
2479                            ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2480                        },
2481                        Param {
2482                            name: "contact_type".into(),
2483                            ty: TypeRef::Enum("ContactType".into()),
2484                        },
2485                    ],
2486                    returns: Some(TypeRef::Handle),
2487                    doc: None,
2488                    r#async: false,
2489                },
2490                Function {
2491                    name: "get_contact".into(),
2492                    params: vec![Param {
2493                        name: "id".into(),
2494                        ty: TypeRef::Handle,
2495                    }],
2496                    returns: Some(TypeRef::Struct("Contact".into())),
2497                    doc: None,
2498                    r#async: false,
2499                },
2500                Function {
2501                    name: "list_contacts".into(),
2502                    params: vec![],
2503                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
2504                    doc: None,
2505                    r#async: false,
2506                },
2507                Function {
2508                    name: "delete_contact".into(),
2509                    params: vec![Param {
2510                        name: "id".into(),
2511                        ty: TypeRef::Handle,
2512                    }],
2513                    returns: Some(TypeRef::Bool),
2514                    doc: None,
2515                    r#async: false,
2516                },
2517                Function {
2518                    name: "count_contacts".into(),
2519                    params: vec![],
2520                    returns: Some(TypeRef::I32),
2521                    doc: None,
2522                    r#async: false,
2523                },
2524            ],
2525            errors: None,
2526        }]);
2527
2528        let tmp = std::env::temp_dir().join("weaveffi_test_py_full_contacts");
2529        let _ = std::fs::remove_dir_all(&tmp);
2530        std::fs::create_dir_all(&tmp).unwrap();
2531        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2532
2533        PythonGenerator.generate(&api, out_dir).unwrap();
2534
2535        let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap();
2536        let pyi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.pyi")).unwrap();
2537
2538        assert!(py.contains("class ContactType(IntEnum):"));
2539        assert!(py.contains("Personal = 0"));
2540        assert!(py.contains("Work = 1"));
2541        assert!(py.contains("Other = 2"));
2542
2543        assert!(py.contains("class Contact:"));
2544        assert!(py.contains("weaveffi_contacts_Contact_destroy"));
2545        assert!(py.contains("@property\n    def id(self) -> int:"));
2546        assert!(py.contains("weaveffi_contacts_Contact_get_id"));
2547        assert!(py.contains("@property\n    def first_name(self) -> str:"));
2548        assert!(py.contains("weaveffi_contacts_Contact_get_first_name"));
2549        assert!(py.contains("@property\n    def last_name(self) -> str:"));
2550        assert!(py.contains("weaveffi_contacts_Contact_get_last_name"));
2551        assert!(py.contains("@property\n    def email(self) -> Optional[str]:"));
2552        assert!(py.contains("weaveffi_contacts_Contact_get_email"));
2553        assert!(py.contains("@property\n    def contact_type(self) -> \"ContactType\":"));
2554        assert!(py.contains("weaveffi_contacts_Contact_get_contact_type"));
2555        assert!(py.contains("return ContactType(_result)"));
2556
2557        assert!(py.contains("def create_contact("));
2558        assert!(py.contains("first_name: str"));
2559        assert!(py.contains("last_name: str"));
2560        assert!(py.contains("email: Optional[str]"));
2561        assert!(py.contains("contact_type: \"ContactType\""));
2562        assert!(py.contains("-> int:"));
2563        assert!(py.contains("weaveffi_contacts_create_contact"));
2564        assert!(py.contains("_string_to_bytes(first_name)"));
2565        assert!(py.contains("contact_type.value"));
2566
2567        assert!(py.contains("def get_contact(id: int) -> \"Contact\":"));
2568        assert!(py.contains("weaveffi_contacts_get_contact"));
2569        assert!(py.contains("return Contact(_result)"));
2570
2571        assert!(py.contains("def list_contacts() -> List[\"Contact\"]:"));
2572        assert!(py.contains("weaveffi_contacts_list_contacts"));
2573        assert!(py.contains("Contact(_result[_i]) for _i in range(_out_len.value)"));
2574
2575        assert!(py.contains("def delete_contact(id: int) -> bool:"));
2576        assert!(py.contains("weaveffi_contacts_delete_contact"));
2577        assert!(py.contains("return bool(_result)"));
2578
2579        assert!(py.contains("def count_contacts() -> int:"));
2580        assert!(py.contains("weaveffi_contacts_count_contacts"));
2581
2582        assert!(pyi.contains("class ContactType(IntEnum):"));
2583        assert!(pyi.contains("    Personal: int"));
2584        assert!(pyi.contains("    Work: int"));
2585        assert!(pyi.contains("    Other: int"));
2586        assert!(pyi.contains("class Contact:"));
2587        assert!(pyi.contains("def create_contact("));
2588        assert!(pyi.contains("def get_contact(id: int) -> \"Contact\": ..."));
2589        assert!(pyi.contains("def list_contacts() -> List[\"Contact\"]: ..."));
2590        assert!(pyi.contains("def delete_contact(id: int) -> bool: ..."));
2591        assert!(pyi.contains("def count_contacts() -> int: ..."));
2592
2593        let _ = std::fs::remove_dir_all(&tmp);
2594    }
2595
2596    #[test]
2597    fn python_generates_packaging() {
2598        let api = make_api(vec![simple_module(vec![Function {
2599            name: "add".into(),
2600            params: vec![
2601                Param {
2602                    name: "a".into(),
2603                    ty: TypeRef::I32,
2604                },
2605                Param {
2606                    name: "b".into(),
2607                    ty: TypeRef::I32,
2608                },
2609            ],
2610            returns: Some(TypeRef::I32),
2611            doc: None,
2612            r#async: false,
2613        }])]);
2614
2615        let tmp = std::env::temp_dir().join("weaveffi_test_python_packaging");
2616        let _ = std::fs::remove_dir_all(&tmp);
2617        std::fs::create_dir_all(&tmp).unwrap();
2618        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2619
2620        PythonGenerator.generate(&api, out_dir).unwrap();
2621
2622        let pyproject = std::fs::read_to_string(tmp.join("python/pyproject.toml")).unwrap();
2623        assert!(
2624            pyproject.contains("[build-system]"),
2625            "missing build-system: {pyproject}"
2626        );
2627        assert!(
2628            pyproject.contains("setuptools"),
2629            "missing setuptools: {pyproject}"
2630        );
2631        assert!(
2632            pyproject.contains("[project]"),
2633            "missing project section: {pyproject}"
2634        );
2635        assert!(
2636            pyproject.contains("name = \"weaveffi\""),
2637            "missing project name: {pyproject}"
2638        );
2639        assert!(
2640            pyproject.contains("version = \"0.1.0\""),
2641            "missing version: {pyproject}"
2642        );
2643        assert!(
2644            pyproject.contains("[tool.setuptools]"),
2645            "missing tool.setuptools: {pyproject}"
2646        );
2647        assert!(
2648            pyproject.contains("packages = [\"weaveffi\"]"),
2649            "missing packages list: {pyproject}"
2650        );
2651
2652        let setup = std::fs::read_to_string(tmp.join("python/setup.py")).unwrap();
2653        assert!(
2654            setup.contains("from setuptools import setup"),
2655            "missing setuptools import: {setup}"
2656        );
2657        assert!(
2658            setup.contains("name=\"weaveffi\""),
2659            "missing package name: {setup}"
2660        );
2661
2662        let readme = std::fs::read_to_string(tmp.join("python/README.md")).unwrap();
2663        assert!(
2664            readme.contains("pip install"),
2665            "missing install instructions: {readme}"
2666        );
2667
2668        let _ = std::fs::remove_dir_all(&tmp);
2669    }
2670
2671    #[test]
2672    fn python_has_memory_helpers() {
2673        let api = make_api(vec![]);
2674        let py = render_python_module(&api);
2675        assert!(
2676            py.contains("import contextlib"),
2677            "missing contextlib import"
2678        );
2679        assert!(
2680            py.contains("class _PointerGuard(contextlib.AbstractContextManager):"),
2681            "missing _PointerGuard class"
2682        );
2683        assert!(
2684            py.contains("def __exit__(self, *exc)"),
2685            "missing _PointerGuard.__exit__"
2686        );
2687        assert!(
2688            py.contains("def _string_to_bytes("),
2689            "missing _string_to_bytes helper"
2690        );
2691        assert!(
2692            py.contains("def _bytes_to_string("),
2693            "missing _bytes_to_string helper"
2694        );
2695    }
2696}