use super::{
SHADOWABLE_BUILTIN_TYPES, pyi_docstring, python_safe_name, qualify_parameter_type, qualify_shadowed_builtin_types,
};
use crate::backends::pyo3::type_map::python_type;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
use ahash::AHashSet;
fn factory_param_type(ty: &TypeRef, coercible_dtos: &AHashSet<&str>) -> String {
match ty {
TypeRef::Named(name) if coercible_dtos.contains(name.as_str()) => {
format!("options.{name} | dict[str, Any]")
}
TypeRef::Optional(inner) => format!("{} | None", factory_param_type(inner, coercible_dtos)),
TypeRef::Vec(inner) => format!("list[{}]", factory_param_type(inner, coercible_dtos)),
TypeRef::Map(k, v) => format!(
"dict[{}, {}]",
factory_param_type(k, coercible_dtos),
factory_param_type(v, coercible_dtos)
),
_ => python_type(ty),
}
}
fn to_python_enum_variant(name: &str) -> String {
crate::core::keywords::python_str_enum_ident(&crate::codegen::naming::pascal_to_screaming_snake(name))
}
pub(super) fn gen_enum_stub(
enum_def: &EnumDef,
emit_docstrings: bool,
coercible_dtos: &AHashSet<&str>,
is_host_enum: bool,
api_types: &[TypeDef],
) -> String {
use crate::codegen::generators::enum_has_data_variants;
let mut lines = vec![];
if enum_has_data_variants(enum_def) {
gen_data_enum_typeddicts(&mut lines, enum_def, coercible_dtos, is_host_enum, api_types);
} else {
lines.push(format!("class {}:", enum_def.name));
if emit_docstrings && let Some(docstring) = pyi_docstring(&enum_def.doc, " ") {
lines.push(docstring);
}
for variant in &enum_def.variants {
lines.push(format!(
" {}: {} = ...",
to_python_enum_variant(&variant.name),
enum_def.name
));
if emit_docstrings && let Some(docstring) = pyi_docstring(&variant.doc, " ") {
lines.push(docstring);
}
}
lines.push(" def __init__(self, value: int | str) -> None: ...".to_string());
lines.push(" def __hash__(self) -> int: ...".to_string());
}
lines.join("\n")
}
const DEFAULT_TAG_FIELD: &str = "type";
fn adjacent_payload_type(lines: &mut Vec<String>, enum_def: &EnumDef, variant: &EnumVariant) -> Option<String> {
if variant.fields.is_empty() {
return None;
}
if variant.is_tuple && variant.fields.len() == 1 {
return Some(python_type(&variant.fields[0].ty));
}
let payload_class = format!("{}{}Payload", enum_def.name, variant.name);
lines.push(format!("class {payload_class}(TypedDict):"));
for field in &variant.fields {
lines.push(typed_dict_field_line(field));
}
lines.push(String::new());
Some(payload_class)
}
fn typed_dict_field_line(field: &FieldDef) -> String {
let field_type = python_type(&field.ty);
let field_type = if field.optional && !field_type.contains("| None") {
format!("{field_type} | None")
} else {
field_type
};
format!(" {}: {}", python_safe_name(&field.name), field_type)
}
fn flattened_payload_fields<'a>(variant: &EnumVariant, api_types: &'a [TypeDef]) -> Option<&'a [FieldDef]> {
let TypeRef::Named(payload_type) = &variant.fields.first()?.ty else {
return None;
};
api_types
.iter()
.find(|typ| &typ.name == payload_type)
.map(|typ| typ.fields.as_slice())
}
fn gen_data_enum_typeddicts(
lines: &mut Vec<String>,
enum_def: &EnumDef,
coercible_dtos: &AHashSet<&str>,
is_host_enum: bool,
api_types: &[TypeDef],
) {
let repr = crate::codegen::serde_enum_repr::serde_enum_repr(enum_def);
let tag_field = repr.tag().unwrap_or(DEFAULT_TAG_FIELD);
let rename_all = enum_def.serde_rename_all.as_deref();
for variant in enum_def.variants.iter().filter(|_| repr.tag().is_some()) {
let class_name = format!("{}{}Variant", enum_def.name, variant.name);
let tag_value =
crate::codegen::naming::wire_variant_value(&variant.name, variant.serde_rename.as_deref(), rename_all);
let payload_type_name = repr
.content()
.and_then(|_| adjacent_payload_type(lines, enum_def, variant));
lines.push(format!("class {}(TypedDict):", class_name));
lines.push(format!(" {}: Literal[\"{}\"]", tag_field, tag_value));
let is_internal_newtype_payload =
matches!(repr, crate::codegen::serde_enum_repr::SerdeEnumRepr::Internal { .. })
&& variant.is_tuple
&& variant.fields.len() == 1
&& matches!(variant.fields[0].ty, TypeRef::Named(_));
let flattened_fields =
is_internal_newtype_payload.then(|| flattened_payload_fields(variant, api_types).unwrap_or_default());
match (repr.content(), payload_type_name, flattened_fields) {
(Some(content_field), Some(payload_type), _) => {
lines.push(format!(" {content_field}: {payload_type}"));
}
(Some(_), None, _) => {}
(None, _, Some(payload_fields)) => {
for field in payload_fields {
if python_safe_name(&field.name) == tag_field {
continue;
}
lines.push(typed_dict_field_line(field));
}
}
(None, _, None) => {
let is_single_tuple_field = variant.fields.len() == 1
&& variant.fields[0]
.name
.strip_prefix('_')
.is_some_and(|suffix| suffix.chars().all(|c| c.is_ascii_digit()));
if !is_single_tuple_field {
for field in &variant.fields {
lines.push(typed_dict_field_line(field));
}
}
}
}
lines.push("".to_string());
}
lines.push(format!("class {}:", enum_def.name));
if enum_def.serde_tag.is_some() {
lines.push(format!(" {}: str", tag_field));
}
gen_data_enum_variant_accessor_stubs(lines, enum_def);
gen_data_enum_variant_constructor_stubs(lines, enum_def, coercible_dtos, is_host_enum);
if !crate::codegen::generators::enum_has_sanitized_fields(enum_def) {
lines.push(
" def __init__(self, value: dict[str, Any] | str | None = None, **kwargs: Any) -> None: ...".to_string(),
);
}
lines.push(" def __str__(self) -> str: ...".to_string());
lines.push(" def __repr__(self) -> str: ...".to_string());
}
fn gen_data_enum_variant_accessor_stubs(lines: &mut Vec<String>, enum_def: &EnumDef) {
use crate::codegen::generators::collect_variant_accessors;
for accessor in collect_variant_accessors(enum_def) {
lines.push(format!(
" {}: {} | None",
accessor.py_name,
python_type(&TypeRef::Named(accessor.inner_type_name.to_string()))
));
}
}
fn gen_data_enum_variant_constructor_stubs(
lines: &mut Vec<String>,
enum_def: &EnumDef,
coercible_dtos: &AHashSet<&str>,
is_host_enum: bool,
) {
use crate::codegen::generators::{collect_pyo3_variant_constructors, variant_constructor_is_reachable};
let ctors: Vec<_> = collect_pyo3_variant_constructors(enum_def)
.into_iter()
.filter(|ctor| {
enum_def
.variants
.iter()
.find(|v| v.name == ctor.variant_name)
.is_some_and(|v| variant_constructor_is_reachable(v, is_host_enum))
})
.collect();
let shadowed: Vec<&str> = SHADOWABLE_BUILTIN_TYPES
.iter()
.copied()
.filter(|b| ctors.iter().any(|c| c.snake_name == *b))
.collect();
for ctor in &ctors {
let params: Vec<String> = ctor
.params
.iter()
.enumerate()
.map(|(idx, p)| {
let optional = p.optional || crate::codegen::shared::is_promoted_optional(&ctor.params, idx);
let py_type = factory_param_type(&p.ty, coercible_dtos);
let py_type = qualify_parameter_type(&python_safe_name(&p.name), &py_type);
let py_type = qualify_shadowed_builtin_types(&py_type, &shadowed);
let py_type = if optional && !py_type.contains("| None") {
format!("{py_type} | None")
} else {
py_type
};
crate::backends::pyo3::template_env::render(
"stub_enum_variant_constructor_param.jinja",
minijinja::context! {
name => python_safe_name(&p.name),
py_type => py_type,
optional => optional,
},
)
})
.collect();
lines.push(crate::backends::pyo3::template_env::render(
"stub_enum_variant_constructor.jinja",
minijinja::context! {
method_name => python_safe_name(&ctor.snake_name),
params => params.join(", "),
return_type => &enum_def.name,
},
));
}
}
#[cfg(test)]
mod tests;