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
58fn 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
124fn 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
160fn 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
435fn 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
564fn 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
663fn 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
726fn 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}