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