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