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