Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions crates/derive-impl/src/from_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,3 +653,73 @@ pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
};
Ok(output)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn option_none_default_keeps_rust_value_and_metadata() {
let types: [Type; 2] = [
parse_quote!(Option<PyObjectRef>),
parse_quote!(Option<PyStrRef>),
];
for ty in types {
let field: Field = parse_quote! {
#[pyarg(any, default = None)]
value: #ty
};
let tokens = generate_field((0, &field)).unwrap().to_string();
assert!(tokens.contains(&quote!(unwrap_or_else(|| None)).to_string()));
assert!(!tokens.contains("vm . ctx . none"));
let attr = ArgAttribute::try_from(&field).unwrap();
let repr = repr_path();
assert_eq!(
signature_default(&field, &attr).unwrap().to_string(),
quote!(Some(#repr::None)).to_string()
);
}
}

#[test]
fn none_default_keeps_wrapped_argument_paths() {
let types: [Type; 3] = [
parse_quote!(Option<PyObjectRef>),
parse_quote!(OptionalArg<PyObjectRef>),
parse_quote!(OptionalOption<PyObjectRef>),
];
for ty in types {
let field: Field = parse_quote! {
#[pyarg(any, default = None)]
value: #ty
};
let tokens = generate_field((0, &field)).unwrap().to_string();
assert!(!tokens.contains("vm . ctx . none"));
assert!(tokens.contains(&quote!(unwrap_or_else(|| None)).to_string()));
}
}

#[test]
fn none_is_not_a_python_object_literal() {
for expr in [parse_quote!(None), parse_quote!((None))] {
assert!(literal_object_expr(&expr).is_none());
}
for expr in [
parse_quote!("text"),
parse_quote!(b"bytes"),
parse_quote!(true),
] {
assert_eq!(
literal_object_expr(&expr).unwrap().to_string(),
quote!(#expr).to_string()
);
}
let field: Field = parse_quote! {
#[pyarg(any, default = "text")]
value: PyStrRef
};
let tokens = generate_field((0, &field)).unwrap().to_string();
assert!(tokens.contains("< PyStrRef as :: rustpython_vm :: convert :: TryFromObject >"));
assert!(!tokens.contains("vm . ctx . none"));
}
}
4 changes: 4 additions & 0 deletions crates/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ use syn::punctuated::Punctuated;
/// `OptionalArg<T>` is converted only when the argument is missing:
/// `<FieldTy as TryFromObject>::try_from_object(vm, ToPyObject::to_pyobject(LIT, vm))?`.
/// A byte-string literal becomes a Python `bytes` object.
/// `default = None` on `Option<T>`, including `Option<PyObjectRef>`, stores
/// Rust `None` and displays `None` in the signature without `py_default`.
/// It does not construct Python's None object for an unwrapped field or
/// mean `OptionalArg::Missing`.
/// - `default = ::NAME`: `NAME` is one identifier. The signature copies that
/// name, and the missing argument stores `Into::into(NAME)` (the leading
/// `::` is not Rust syntax for a local constant). A longer `::` path is a
Expand Down
Loading