-
Notifications
You must be signed in to change notification settings - Fork 788
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
FromPyObject derivation for structs and enums #1065
Conversation
Thank you, but... Some complaints about the
So, how about just making it |
Thanks for the feedback. As mentioned, this is just a rough draft to figure out whether this is the right path or not.
That's a valid point although I'm not sure how to address this or whether this needs to be addressed.
I chose that name since it's the most specific term within Python, also it was the one discussed over at #301. Different naming would be fine, too.
Sure, at this point that's all that the macro is doing. Before proceeding, we should probably discuss whether deriving |
Thanks for this. I think @kngwyu is right that because However, I think that if we proceed to add this Order dependence - I think this is a desirable feature and should be documented. e.g. consider this:
In the above, |
And another question is how should we distinguish this from the enum type introduced in #1045 |
IMO that's a question for documentation and easy enough for us to solve:
|
I've been working a bit on this on and off: Should the Wrt. unit structs / variants I have a check that rules those out since it wouldn't be possible to decide what to extract, or rather, any unit struct can always be extracted. #[derive(Debug, FromPyObject)]
pub struct A<'a>
{
s: String,
t: &'a PyString,
p: &'a PyAny,
}
//generated
impl<'a> ::pyo3::FromPyObject<'a> for A<'a> {
fn extract(ob: &'a ::pyo3::PyAny) -> ::pyo3::PyResult<Self> {
Ok(Self {
s: ::pyo3::FromPyObject::extract(ob)?,
t: ::pyo3::FromPyObject::extract(ob)?,
p: ::pyo3::FromPyObject::extract(ob)?,
})
}
} for structs and for enums: pub enum Union<'a> {
#[err_rename = "str"]
Named {
a: B,
b: String,
c: &'a PyString,
},
Int(&'a PyLong),
#[err_rename = "List[str]"]
StringList(Vec<String>),
}
// generated
impl<'a> ::pyo3::FromPyObject<'a> for Union<'a> {
fn extract(ob: &'a ::pyo3::PyAny) -> ::pyo3::PyResult<Self> {
if let (Ok(a), Ok(b), Ok(c)) = (
::pyo3::FromPyObject::extract(ob),
::pyo3::FromPyObject::extract(ob),
::pyo3::FromPyObject::extract(ob),
) {
return Ok(Union::Named { a, b, c });
};
if let (Ok(_field_0)) = (::pyo3::FromPyObject::extract(ob)) {
return Ok(Union::Int(_field_0));
};
if let (Ok(_field_0)) = (::pyo3::FromPyObject::extract(ob)) {
return Ok(Union::StringList(_field_0));
};
let type_name = ob.get_type().name();
let from = ob
.repr()
.map(|s| {
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["", " (", ")"],
&match (&s.to_string_lossy(), &type_name) {
(arg0, arg1) => [
::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Display::fmt),
::core::fmt::ArgumentV1::new(arg1, ::core::fmt::Display::fmt),
],
},
));
res
})
.unwrap_or_else(|_| type_name.to_string());
let err_msg = {
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["Can\'t convert ", " to "],
&match (&from, &"str, Int, List[str]") {
(arg0, arg1) => [
::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Display::fmt),
::core::fmt::ArgumentV1::new(arg1, ::core::fmt::Display::fmt),
],
},
));
res
};
Err(::pyo3::exceptions::PyTypeError::py_err(err_msg))
}
} |
It seems reasonable to me that the Python "equivalent" of
Can I propose that we instead name this attribute
I'm not sure I agree with this implementation - it looks to me that I think we need to consider what If I were to write it as a Python type annotation, I think the Python "type" that
|
The Python equivalent would actually be #[derive(FromPyObject)]
struct Tuple<'a>((String, String, &'a PyAny));
Sure that was just whatever I came up with at the time.
This is more or less the same as above: If a dict should be extracted, that needs to be guided from the type of the field that it gets extracted to. I'm not sure how this would work otherwise... |
For example I think the implementation for
and for
|
Any news? Can't wait to try out pyo3 with ADTs. Is there anything we can do to help? |
I got caught up in some other stuff, I might find some time later this week / towards the weekend to get back to this. I'm not completely sold on the assumed dict-struct relation in the derivation. IMO, a more intuitive parallel for struct fields would be properties / attributes in Python classes, so what's your opinion on The PyTuple-to-tuple-struct extraction makes sense to me as proposed by @davidhewitt |
This is a fair view and I don't disagree that I think that whatever design we build here should have an equivalent to-python implementation we can also derive in pyo3. (Even if we don't implement the to-python conversion as part of this PR.) I can't think of an obvious class type for the to-python conversion, so I think struct -> dict seems best for that direction. And equivalently this makes me think the from-python conversion should be dict->struct. Another viewpoint I've been thinking on: how to implement a 1. I think that both of these two rules should probably apply to the design here? |
I don't think serde's I wouldn't expect |
True, but I think in practice if data does not roundtrip, it's extremely suprising to unexpecting users. If it's possible for us to make roundtripping work, I think it should. See e.g. RReverser/serde-xml-rs#130
This is true, but I'm not convinced that adding a
I'd be much more happy with this solution. There's still questions of how to construct the A possible interesting case of discussion: I've now started a WIP implementation on such a serde/python implementation. See https://github.com/davidhewitt/pythonize I've not quite settled on the final design, so I'd welcome any feedback on that. I kinda see these |
I tried implementing some of the suggestions but I think there's some issues to smooth out: #[derive(FromPyObject)]
struct Wrapper(String); With such a struct it doesn't seem intuitive to extract the #[derive(FromPyObject)]
#[pyo3(wrapper)]
struct Wrapper(String); The same applies to a wrapper struct with a named field: #[derive(FromPyObject)]
struct Wrapper { wrapped: String } This also extends to enums. Regarding the extraction of named fields, I think it'd be nice to decide this based on a field attribute, i.e.: #[derive(FromPyObject)]
struct WithNamedFields {
#[extract(getattr)]
from_attr: String,
#[extract(get_item)]
from_item: String,
}
// expands to
impl<'a> FromPyObject<'a> for WithNamedFields {
fn extract(obj: &'a PyAny) -> PyResult<Self> {
Ok(WithNamedFields {
from_attr: obj.getattr("from_attr")?.extract()?,
from_item: obj.get_item("from_item")?.extract()?
}
}
} It'd probably be nice to untie the field name from the Rust implementation. Otherwise trying to derive |
Thanks for proceeding forwards with this implementation! I think we can can draw inspiration to make configurable attributes from serde's set of attributes.
Let's make
Serde does this with So perhaps
Agreed, though I think probably should go on the struct rather than the fields - probably you want either all As it's not clear right now whether attributes or items is a better default: how about making it required for one of If you have ideas for better names for this pair of attributes I'm happy for you to use something else. Later we could make it optional when we figure out the better default strategy. |
How about making it only for |
I'm on vacation starting monday, so I should find time to turn this into a proper PR next week. |
I think unfortunately because struct variants and tuple variants encode all the possibilites, solving for enum pretty much requires solving for structs too. If the scope was to reduce on this PR I think we'd have to dial it back pretty much to just the original design (i.e. single-item tuple variants). I'm happy with whatever @sebpuetz is planning to build! 😄 |
I have an ugly WIP solution that handles most of the desired cases for both enums and structs (I haven't handled the wrapper-struct-variant yet). It's pretty hard to abstract over tuple structs, tuple variants, proper structs and struct variants, at least I'm getting lost with all the different I'm pushing the current state to this branch so this doesn't look all that stale. Perhaps you can voice your opinion on some of the decisions, too. #[pyfunction]
pub fn foo(mut inp: A) {
println!("{:?}", inp);
}
#[pyfunction]
pub fn bar(mut inp: Foo) {
println!("{:?}", inp);
}
#[pyfunction]
pub fn baz(mut inp: B) {
println!("{:?}", inp);
}
#[derive(Debug, FromPyObject)]
pub struct A<'a> {
#[extract(getattr)]
s: String,
#[extract(get_item)]
t: &'a PyString,
#[extract(getattr)]
#[name = "foo"]
p: &'a PyAny,
}
#[derive(Debug, FromPyObject)]
#[wrapper]
pub struct B {
test: String
}
#[derive(Debug, FromPyObject)]
pub struct C {
#[extract(getattr)]
#[name = "bla"]
test: String
}
#[derive(Debug, FromPyObject)]
pub enum Foo<'a> {
#[err_rename = "bla"]
FF(&'a PyAny, String),
#[wrapper]
F(&'a PyAny)
} >>>import from_py_object
>>> from_py_object.bar(("test", "test"))
F('test')
>>> from_py_object.bar(("test", "test"))
FF('test', "test")
>>> class Foo:
... def __init__(self):
... self.s = "test"
... self.foo = None
... def __getitem__(self, key):
... return "Foo"
>>> from_py_object.foo(Foo())
A { s: "test", t: 'Foo', p: None }
>>> from_py_object.baz("test")
B { test: "test" } |
Awesome! 👍 I'll try to find time to review whatever state this PR is in at some point in the next few days. Perhaps that can help with some of the refactoring. |
1ba2084
to
b1a6500
Compare
(I just merged #1121.) |
All green, do we want UI tests & docs done within this PR? |
🎉 UI tests - I think it's desirable to get as much test coverage as possible in the original PR, so if you have ideas what you would make for UI tests it would be great to have here. Docs - I'm okay with merging PRs without docs, but especially for a new feature like this nobody will use it until we write good documentation. So if we choose to wait on docs we should open an issue when closing this PR so that we remind ourselves docs are needed. |
I'm working on the UI tests, docs are up next. Should be done in the afternoon! |
Fix some error messages and accidental passes.
If everything turns out green, this should be reviewable! |
ff47b33
to
7d0a58f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thank you for the hard work!
My suggestions are mostly for reducing LOC and not critical. Please address them if you like.
Thanks for the suggestions, I pushed the sugested changes. Hoping for all-green CI! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking absolutely excellent; great tests and docs! I just have a few final suggestions/questions and then let's merge it!
Once again, thanks for the suggestions. CI is slowly turning green, too! edit had to force-push again, the attribute list ended up under the wrong section in the guide... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks absolutely excellent to me. Thanks so much! 🚀
Is there still something left to be addressed or is this good to go? |
I'm happy, just leaving some time for @kngwyu to have a final chance to review and make the merge. |
Thanks, I'm happy to see this became a reality in such a nice way. |
This is a draft addressing a bit of both topics in #301 and #1055.
I have not written proc-macro code before, so this might be quite ugly. This is mostly proof-of-concept, but I think it'd be a great quality-of-life improvement if something like this was available.
This produces the following behaviour:
This implementation covers both the cheap downcast variant and extraction of proper Rust types. Although I'm not sure whether relying on the variant holding a reference is the proper heuristic to apply here.
Todos are marked in the code, a proper implementation should in my opinion: