Skip to content

Commit fe4a8e5

Browse files
committed
Keep None defaults as Rust Option values
Remove Python None construction for unwrapped fields. Document existing Option defaults and replace the two newly added object-None tests with Option metadata and literal-boundary checks. Assisted-by: OpenAI Codex:model version not exposed
1 parent 7096e70 commit fe4a8e5

2 files changed

Lines changed: 30 additions & 33 deletions

File tree

‎crates/derive-impl/src/from_args.rs‎

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -379,12 +379,9 @@ fn is_wrapped_arg_ty(ty: &Type) -> bool {
379379
)
380380
}
381381

382-
/// Expression passed to `ToPyObject` for a literal Python default.
382+
/// Expression passed to `ToPyObject` for a string, bytes, int, float, or bool literal.
383383
fn literal_object_expr(expr: &Expr) -> Option<TokenStream> {
384384
match expr {
385-
Expr::Path(path) if path.qself.is_none() && path.path.is_ident("None") => {
386-
Some(quote!(vm.ctx.none()))
387-
}
388385
Expr::Lit(syn::ExprLit { lit, .. }) => match lit {
389386
Lit::Str(_) | Lit::ByteStr(_) | Lit::Bool(_) => Some(expr.to_token_stream()),
390387
Lit::Int(_) => Some(quote!((#expr as i128))),
@@ -662,22 +659,26 @@ mod tests {
662659
use super::*;
663660

664661
#[test]
665-
fn none_object_default_is_lazy_and_typed() {
666-
let field: Field = parse_quote! {
667-
#[pyarg(any, default = None)]
668-
value: PyObjectRef
669-
};
670-
let tokens = generate_field((0, &field)).unwrap().to_string();
671-
let fallback = tokens.find("unwrap_or_else").unwrap();
672-
let none = tokens.find("vm . ctx . none ()").unwrap();
673-
assert!(fallback < none);
674-
assert!(tokens.contains("TryFromObject > :: try_from_object"));
675-
let attr = ArgAttribute::try_from(&field).unwrap();
676-
let repr = repr_path();
677-
assert_eq!(
678-
signature_default(&field, &attr).unwrap().to_string(),
679-
quote!(Some(#repr::None)).to_string()
680-
);
662+
fn option_none_default_keeps_rust_value_and_metadata() {
663+
let types: [Type; 2] = [
664+
parse_quote!(Option<PyObjectRef>),
665+
parse_quote!(Option<PyStrRef>),
666+
];
667+
for ty in types {
668+
let field: Field = parse_quote! {
669+
#[pyarg(any, default = None)]
670+
value: #ty
671+
};
672+
let tokens = generate_field((0, &field)).unwrap().to_string();
673+
assert!(tokens.contains(&quote!(unwrap_or_else(|| None)).to_string()));
674+
assert!(!tokens.contains("vm . ctx . none"));
675+
let attr = ArgAttribute::try_from(&field).unwrap();
676+
let repr = repr_path();
677+
assert_eq!(
678+
signature_default(&field, &attr).unwrap().to_string(),
679+
quote!(Some(#repr::None)).to_string()
680+
);
681+
}
681682
}
682683

683684
#[test]
@@ -699,12 +700,9 @@ mod tests {
699700
}
700701

701702
#[test]
702-
fn literal_object_defaults_preserve_conversion() {
703+
fn none_is_not_a_python_object_literal() {
703704
for expr in [parse_quote!(None), parse_quote!((None))] {
704-
assert_eq!(
705-
literal_object_expr(&expr).unwrap().to_string(),
706-
quote!(vm.ctx.none()).to_string()
707-
);
705+
assert!(literal_object_expr(&expr).is_none());
708706
}
709707
for expr in [
710708
parse_quote!("text"),
@@ -717,13 +715,11 @@ mod tests {
717715
);
718716
}
719717
let field: Field = parse_quote! {
720-
#[pyarg(any, default = None)]
718+
#[pyarg(any, default = "text")]
721719
value: PyStrRef
722720
};
723-
// A typed object still validates the default through its converter;
724-
// None must not be transmuted or treated as a missing sentinel.
725721
let tokens = generate_field((0, &field)).unwrap().to_string();
726722
assert!(tokens.contains("< PyStrRef as :: rustpython_vm :: convert :: TryFromObject >"));
727-
assert!(tokens.contains("vm . ctx . none ()"));
723+
assert!(!tokens.contains("vm . ctx . none"));
728724
}
729725
}

‎crates/derive/src/lib.rs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ use syn::punctuated::Punctuated;
2020
/// The signature text is `0`, `False` or `0.0` for a primitive integer, `bool`
2121
/// or float field, and `<unrepresentable>` otherwise, unless `py_default` is set.
2222
/// - `default = <expr>`: missing argument stores that Rust value. Affects parsing.
23-
/// A string, byte-string, integer (optionally negated), float, bool, or `None` literal
23+
/// A string, byte-string, integer (optionally negated), float, or bool literal
2424
/// on any field that is not a Rust primitive (`i8`..`i128`, `u8`..`u128`,
2525
/// `isize`, `usize`, `f32`, `f64`, `bool`), `&'static str`, `Option<T>`, or
2626
/// `OptionalArg<T>` is converted only when the argument is missing:
2727
/// `<FieldTy as TryFromObject>::try_from_object(vm, ToPyObject::to_pyobject(LIT, vm))?`.
2828
/// A byte-string literal becomes a Python `bytes` object.
29-
/// `None` becomes Python's None singleton and is validated by the field's
30-
/// converter only when omitted. Wrapped argument types retain their Rust
31-
/// default expressions; `default = None` does not mean `OptionalArg::Missing`.
29+
/// `default = None` on `Option<T>`, including `Option<PyObjectRef>`, stores
30+
/// Rust `None` and displays `None` in the signature without `py_default`.
31+
/// It does not construct Python's None object for an unwrapped field or
32+
/// mean `OptionalArg::Missing`.
3233
/// - `default = ::NAME`: `NAME` is one identifier. The signature copies that
3334
/// name, and the missing argument stores `Into::into(NAME)` (the leading
3435
/// `::` is not Rust syntax for a local constant). A longer `::` path is a

0 commit comments

Comments
 (0)