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