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