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