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