Skip to content

derive(FromPyObject): adds default option #4829

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 10, 2025
Merged

Conversation

Tpt
Copy link
Contributor

@Tpt Tpt commented Dec 30, 2024

Takes an optional expression to set a custom value that is not the one from the Default trait

Note that the default value is only used if the key fetching fails (item/attr not present...) but errors are still raised if the value extraction fails

Issue #4643

@Tpt Tpt force-pushed the frompyobject-default branch from a3d6c7d to baf2fce Compare December 30, 2024 14:46
@Tpt Tpt marked this pull request as ready for review December 30, 2024 15:04
@Tpt Tpt requested a review from Icxolu December 30, 2024 15:13
Copy link
Contributor

@Icxolu Icxolu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This looks pretty good already.

Note that the default value is only used if the key fetching fails (item/attr not present...) but errors are still raised if the value extraction fails

I think this makes sense 👍.

Some points that come to my mind:

  • We should document this option in the FromPyObject section of the guide, probably with some example showcasing the error decision from above.
  • It looks like this should work for tuple structs (we should add a test), how about complex enums?
  • How does this interact with from_py_with? I would say only one or the other makes sense, and we should error if both are supplied.

Comment on lines 365 to 367
quote!(Default::default())
};
quote!(if let Ok(value) = #getter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default and Ok are unhygienic, we should add this option to the hygiene tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thank you for spotting!

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This idea makes sense to me, agreed with all the above 👍

@davidhewitt
Copy link
Member

Possible design question: serde uses default = "path" where "path" should be a function. https://serde.rs/field-attrs.html#default--path

Should we be doing similar? I wonder what the advantage of the function is, I guess it's more general? Is it for hygiene reasons? 🤔

@Icxolu
Copy link
Contributor

Icxolu commented Dec 31, 2024

I wonder what the advantage of the function is, I guess it's more general? Is it for hygiene reasons?

I think this is mostly for historical reasons from before Rust allowed expressions (or identifiers) in attributes. Nowadays making use of proper tokens instead of stringly typing it is prefered I would say. IMO it is also way more convenient to just write the expression inline instead of having to write a function for every default.

@davidhewitt
Copy link
Member

True, and if we allow arbitrary expressions we can always allow something like default = f() to call a function 👍

@Tpt
Copy link
Contributor Author

Tpt commented Jan 2, 2025

Thank you for the reviews. I will apply the changes you asked for.

How does this interact with from_py_with? I would say only one or the other makes sense, and we should error if both are supplied.

The default parameter value must return the Rust field type. The current code does:

  • If getitem/getattr returns a value, then call from_py_with with it. If if succeeds, it's the Rust field value, if not raise the error
  • If getitem/getattr fails, call the default expression and set the Rust field value with it.

It seems fairly well defined to me and is consistent with the behavior of #[pyo3(signature)] on functions but I can tweak this behavior if you want.

I am going to add an extra test covering the usage of the two options together

@Icxolu
Copy link
Contributor

Icxolu commented Jan 2, 2025

The default parameter value must return the Rust field type. The current code does:

  • If getitem/getattr returns a value, then call from_py_with with it. If if succeeds, it's the Rust field value, if not raise the error
  • If getitem/getattr fails, call the default expression and set the Rust field value with it.

It seems fairly well defined to me and is consistent with the behavior of #[pyo3(signature)] on functions but I can tweak this behavior if you want.

Ah, having it written out like that makes sense. (In my head it felt more convoluted around error cases 😁) I'm good with keeping it as is then. Maybe this is also a good addition for the guide section.

I am going to add an extra test covering the usage of the two options together

Sounds good, thank you!

Takes an optional expression to set a custom value that is not the one from the Default trait
@Tpt Tpt force-pushed the frompyobject-default branch 2 times, most recently from adb2541 to 9cbf084 Compare January 6, 2025 11:53
@Tpt
Copy link
Contributor Author

Tpt commented Jan 6, 2025

@Icxolu Thank you again for your review! I have improved test coverage and fixed hygiene.

We should document this option in the FromPyObject section of the guide, probably with some example showcasing the error decision from above.

Done!

It looks like this should work for tuple structs (we should add a test), how about complex enums?

Yes, but it would need some extra code. I would prefer to do it in a follow-up MR. On complex enums, I guess it would work too with the surprising behavior that a variant Foo { #[pyo3(default)] value: usize } becomes a catch all just like the Foo {} variant.

@Tpt Tpt force-pushed the frompyobject-default branch 2 times, most recently from 6f5a8de to 15b354b Compare January 6, 2025 12:43
@Tpt Tpt force-pushed the frompyobject-default branch from 15b354b to 1df9a53 Compare January 6, 2025 13:07
Copy link
Contributor

@Icxolu Icxolu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but it would need some extra code. I would prefer to do it in a follow-up MR.

Sure thing. In that case we should probably make sure here, that we properly error out if applied to tuple structs or enums with some ui tests until we add additional support in the future.

Comment on lines 491 to 495
- `pyo3(default)`, `pyo3(default = ...)`
- if the argument is set, uses the given default value.
- in this case, the argument must be a Rust expression returning a value of the desired Rust type.
- if the argument is not set, [`Default::default`](https://doc.rust-lang.org/std/default/trait.Default.html#tymethod.default) is used.
- this attribute is only supported on named struct fields.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should mention here, that the default is used only if the lookup failed (not when the extraction failed).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@Tpt Tpt force-pushed the frompyobject-default branch 3 times, most recently from 9834b90 to b7ef575 Compare January 8, 2025 10:12
@Tpt Tpt force-pushed the frompyobject-default branch from b7ef575 to 4e6f1f7 Compare January 8, 2025 10:18
@Tpt
Copy link
Contributor Author

Tpt commented Jan 8, 2025

Sure thing. In that case we should probably make sure here, that we properly error out if applied to tuple structs or enums with some ui tests until we add additional support in the future.

Thank you! After doing some testing, the current code already works with enum variant named fields so I added support to them. I also included an error when all fields are set as default to be consistent with the current error that prevents deriving FromPyObject on unit structs. I have also

Copy link
Contributor

@Icxolu Icxolu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks you! I think this looks perfect now 🚀

@Icxolu Icxolu added this pull request to the merge queue Jan 9, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 9, 2025
@Icxolu Icxolu added this pull request to the merge queue Jan 10, 2025
Merged via the queue into PyO3:main with commit 21132a8 Jan 10, 2025
46 checks passed
@Tpt Tpt deleted the frompyobject-default branch January 14, 2025 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants