pub fn gen_pyo3_error_types(error: &ErrorDef, module_name: &str, seen_exceptions: &mut AHashSet<String>) -> String {
gen_pyo3_error_types_with_parent(error, module_name, seen_exceptions, None)
}
pub fn gen_pyo3_error_types_with_parent(
error: &ErrorDef,
module_name: &str,
seen_exceptions: &mut AHashSet<String>,
parent: Option<&str>,
) -> String {
let mut variant_names = Vec::new();
for variant in &error.variants {
let variant_name = python_exception_name(&variant.name, &error.name);
if seen_exceptions.insert(variant_name.clone()) {
variant_names.push(variant_name);
}
}
let include_base = seen_exceptions.insert(error.name.clone());
crate::codegen::template_env::render(
"error_gen/pyo3_error_types.jinja",
crate::alef_context! {
variant_names => variant_names,
module_name => module_name,
error_name => error.name.as_str(),
include_base => include_base,
base_parent => parent.unwrap_or("pyo3::exceptions::PyException"),
},
)
}
pub fn gen_pyo3_error_converter(error: &ErrorDef, core_import: &str) -> String {
let rust_path = if error.rust_path.is_empty() {
format!("{core_import}::{}", error.name)
} else {
let normalized = error.rust_path.replace('-', "_");
let segments: Vec<&str> = normalized.split("::").collect();
if segments.len() > 2 {
let crate_name = segments[0];
let error_name = segments[segments.len() - 1];
format!("{crate_name}::{error_name}")
} else {
normalized
}
};
let fn_name = format!("{}_to_py_err", to_snake_case(&error.name));
let has_methods = !error.methods.is_empty();
let slots = info_slots(error);
let ctor_args = |code: u32| -> String {
if has_methods {
let extra: String = slots.iter().map(|slot| format!(", {}", slot.tuple_expr)).collect();
format!("(msg, {code}u32{extra})")
} else {
"msg".to_string()
}
};
let mut arms = Vec::with_capacity(error.variants.len());
for variant in &error.variants {
let pattern = error_variant_wildcard_pattern(&rust_path, variant);
let variant_exc_name = python_exception_name(&variant.name, &error.name);
let code = variant
.taxonomy(&error.rust_path)
.map_or(crate::core::ir::ApiSurface::FFI_ERROR_CODE_UNKNOWN, |taxonomy| {
taxonomy.code
});
arms.push(format!(
"{pattern} => {variant_exc_name}::new_err({}),",
ctor_args(code)
));
}
let default_arm = format!(
"_ => {}::new_err({}),",
error.name,
ctor_args(crate::core::ir::ApiSurface::FFI_ERROR_CODE_UNKNOWN)
);
crate::codegen::template_env::render(
"error_gen/pyo3_error_converter.jinja",
crate::alef_context! {
rust_path => rust_path.as_str(),
fn_name => fn_name.as_str(),
arms => arms,
default_arm => default_arm.as_str(),
},
)
}
pub fn gen_pyo3_error_registration(error: &ErrorDef, seen_registrations: &mut AHashSet<String>) -> Vec<String> {
let mut registrations = Vec::with_capacity(error.variants.len() + 1);
for variant in &error.variants {
let variant_exc_name = python_exception_name(&variant.name, &error.name);
if seen_registrations.insert(variant_exc_name.clone()) {
registrations.push(format!(
" m.add(\"{}\", m.py().get_type::<{}>())?;",
variant_exc_name, variant_exc_name
));
}
}
if seen_registrations.insert(error.name.clone()) {
registrations.push(format!(
" m.add(\"{}\", m.py().get_type::<{}>())?;",
error.name, error.name
));
}
registrations
}
pub fn converter_fn_name(error: &ErrorDef) -> String {
format!("{}_to_py_err", to_snake_case(&error.name))
}
struct InfoSlot {
name: String,
field_ty: String,
py_ty: &'static str,
doc: String,
tuple_expr: String,
extract_ty: String,
unwrap: &'static str,
flatten: bool,
}
fn info_slots(error: &ErrorDef) -> Vec<InfoSlot> {
let has = |name: &str| error.methods.iter().any(|m| m.name == name);
let mut slots = Vec::new();
if has("status_code") {
slots.push(InfoSlot {
name: "status_code".to_string(),
field_ty: "u16".to_string(),
py_ty: "int",
doc: "HTTP status code for this error (0 means no associated status).".to_string(),
tuple_expr: "e.status_code()".to_string(),
extract_ty: "u16".to_string(),
unwrap: "unwrap_or(0)",
flatten: false,
});
}
if has("is_transient") {
slots.push(InfoSlot {
name: "is_transient".to_string(),
field_ty: "bool".to_string(),
py_ty: "bool",
doc: "Returns `true` if the error is transient and a retry may succeed.".to_string(),
tuple_expr: "e.is_transient()".to_string(),
extract_ty: "bool".to_string(),
unwrap: "unwrap_or(false)",
flatten: false,
});
}
if has("error_type") {
slots.push(InfoSlot {
name: "error_type".to_string(),
field_ty: "String".to_string(),
py_ty: "str",
doc: "Machine-readable error category string for matching and logging.".to_string(),
tuple_expr: "e.error_type().to_string()".to_string(),
extract_ty: "String".to_string(),
unwrap: "unwrap_or_default()",
flatten: false,
});
}
for (method, optional) in duration_methods(error) {
let ty = millis_type(optional, "u64");
slots.push(InfoSlot {
name: method.name.clone(),
field_ty: ty.clone(),
py_ty: if optional { "int | None" } else { "int" },
doc: format!(
"`{}` in whole milliseconds{}.",
method.name,
if optional { " (`None` when absent)" } else { "" }
),
tuple_expr: millis_expr(&format!("e.{}()", method.name), optional, "u64"),
extract_ty: ty,
unwrap: if optional { "" } else { "unwrap_or(0)" },
flatten: optional,
});
}
slots
}
pub fn pyo3_error_info_field_specs(error: &ErrorDef) -> Vec<(String, String)> {
let mut specs = vec![("code".to_string(), "int".to_string())];
specs.extend(
info_slots(error)
.into_iter()
.map(|slot| (slot.name, slot.py_ty.to_string())),
);
specs
}
pub fn gen_pyo3_error_methods_impl(error: &ErrorDef) -> String {
if error.methods.is_empty() {
return String::new();
}
let struct_name = format!("{}Info", error.name);
let snake_name = to_snake_case(&error.name);
let fn_name = format!("{snake_name}_info");
let slots = info_slots(error);
let mut fields = vec![" pub code: u32,".to_string()];
let mut getters = vec![
concat!(
" /// Stable numeric error code identifying the specific error variant\n",
" /// (see the crate's FFI error taxonomy).\n",
" #[getter]\n",
" fn code(&self) -> u32 {\n",
" self.code\n",
" }",
)
.to_string(),
];
let mut ctor_fields = vec![
" code: args\n\
\x20 .as_ref()\n\
\x20 .and_then(|a| a.get_item(1).ok())\n\
\x20 .and_then(|v| v.extract::<u32>().ok())\n\
\x20 .unwrap_or(0),"
.to_string(),
];
for (position, slot) in slots.iter().enumerate() {
let index = position + 2;
let name = &slot.name;
let ty = &slot.field_ty;
fields.push(format!(" pub {name}: {ty},"));
let body = if slot.field_ty == "String" {
format!("self.{name}.clone()")
} else {
format!("self.{name}")
};
getters.push(format!(
" /// {}\n #[getter]\n fn {name}(&self) -> {ty} {{\n {body}\n }}",
slot.doc
));
let flatten = if slot.flatten { "\n .flatten()" } else { "" };
let unwrap = if slot.unwrap.is_empty() {
String::new()
} else {
format!("\n .{}", slot.unwrap)
};
ctor_fields.push(format!(
" {name}: args\n .as_ref()\n .and_then(|a| a.get_item({index}).ok())\n .and_then(|v| v.extract::<{}>().ok()){flatten}{unwrap},",
slot.extract_ty,
));
}
for method in &error.methods {
let is_slot = matches!(method.name.as_str(), "status_code" | "is_transient" | "error_type")
|| super::duration_shape(&method.return_type).is_some();
if !is_slot {
getters.push(format!(
" // Not emitted: getter for method `{}` on `{struct_name}`",
method.name
));
}
}
let struct_def = format!(
"#[pyclass(name = \"{struct_name}\")]\npub struct {struct_name} {{\n{}\n}}",
fields.join("\n")
);
let impl_block = format!("#[pymethods]\nimpl {struct_name} {{\n{}\n}}", getters.join("\n\n"));
let tuple_shape = slots
.iter()
.fold("message, code".to_string(), |acc, slot| format!("{acc}, {}", slot.name));
let last_index = slots.len() + 1;
let free_fn = format!(
"/// Build a `{struct_name}` from any exception raised by the `{error_name}` hierarchy.\n\
///\n\
/// The converter stores `({tuple_shape})` in\n\
/// the exception args tuple; this function extracts those values at indices 1–{last_index}.\n\
#[pyfunction]\n\
pub fn {fn_name}(err: pyo3::Bound<'_, pyo3::types::PyAny>) -> {struct_name} {{\n\
let args = err.getattr(\"args\").ok();\n\
{struct_name} {{\n\
{ctor}\n\
}}\n\
}}",
error_name = error.name,
ctor = ctor_fields.join("\n"),
);
format!("{struct_def}\n\n{impl_block}\n\n{free_fn}")
}
pub fn pyo3_error_has_methods(error: &ErrorDef) -> bool {
!error.methods.is_empty()
}
pub fn pyo3_error_info_struct_name(error: &ErrorDef) -> String {
format!("{}Info", error.name)
}
pub fn pyo3_error_info_fn_name(error: &ErrorDef) -> String {
format!("{}_info", to_snake_case(&error.name))
}
use crate::core::ir::ErrorDef;
use ahash::AHashSet;
use super::shared::{
duration_methods, error_variant_wildcard_pattern, millis_expr, millis_type, python_exception_name, to_snake_case,
};