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