-
Notifications
You must be signed in to change notification settings - Fork 760
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
#1064: Comparisons with __eq__ should not raise TypeError #1072
Conversation
I think the implementation would be straightforward as @davidhewitt commented, but please give us any question you have 😺 |
Is it ok to rewrite the commit for the changes proposed? |
Yes, force-pushing to this branch is fine if you want to rewrite it as a single commit! Really up to you how you prefer to make PR :) |
I'm reading the comment very carefully. I want to understand every bit of it. As soon as I have questions you'll know. |
541ad38
to
52210e0
Compare
I've now made the first bit of progress.
The tests are indirect because they don't actually test the pyclass returns NotImplemented. Instead they rely heavily on Python reversing the operations (BTW, I didn't know Python would try first the reversed operator of So, the setup is always the same: a Python implementation of There's a second part of each test when For the time being, I'm focusing on richcmp. The rest of the protocols are laid out in the tests but not being run yet.
The previous tests are simply minded. They only test the cases when the pyclass don't have an implementation of the protocol or when extracting the arguments fails. I'm not really sure if we need to test what happens when the pyclass does provide an implementation. Whether this code returns NotImplemented or fails, is up to the author. Right?
Following the 3rd point in David's comment I came up with commit 52210e0. I had to comment the case test expecting a TypeError when I plan to make richcmp pass all the tests this weekend. Do you any more pointers? |
It wasn't the macro. It was an error in the Python code of the error. Fixed. |
I managed to also change the implementation of The next step would be |
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.
Thanks! If py_binary_self_func
is too complicated to change in it's current form, we can discuss thinking about refactors that would make this easier?
- Further tests?
I think the tests you've written for this should be sufficient, nice work 👍
My first attempt at it was disastrous 🤪. A change in diff --git a/src/class/macros.rs b/src/class/macros.rs
index e3a0a3401..094466a1e 100644
--- a/src/class/macros.rs
+++ b/src/class/macros.rs
@@ -141,7 +141,7 @@ macro_rules! py_binary_self_func {
$crate::callback_body!(py, {
let slf_ = py.from_borrowed_ptr::<$crate::PyCell<T>>(slf);
let arg = py.from_borrowed_ptr::<$crate::PyAny>(arg);
- call_mut!(slf_, $f, arg).convert(py)?;
+ call_proto_mut!(py, slf_, $f, arg).convert(py)?;
ffi::Py_INCREF(slf);
Ok(slf)
})
@@ -385,6 +385,16 @@ macro_rules! _call_impl {
($slf: expr, $fn: ident, $raw_arg: expr $(,$raw_args: expr)* $(; $args: expr)*) => {
_call_impl!($slf, $fn $(,$raw_args)* $(;$args)* ;$raw_arg.extract()?)
};
+ (proto $py:ident, $slf: expr, $fn: ident, $raw_arg: expr $(,$raw_args: expr)* $(; $args: expr)*) => {
+ _call_impl!(
+ $slf, $fn ;
+ (match $raw_arg.extract() {
+ Ok(res) => res,
+ _=> return Ok($py.NotImplemented().convert($py))
+ })
+ $(;$args)*
+ )
+ }
}
/// Call `slf.try_borrow()?.$fn(...)`
@@ -400,3 +410,9 @@ macro_rules! call_mut {
_call_impl!($slf.try_borrow_mut()?, $fn $(,$raw_args)* $(;$args)*)
};
}
+
+macro_rules! call_proto_mut {
+ ($py:ident, $slf: expr, $fn: ident $(,$raw_args: expr)* $(; $args: expr)*) => {
+ _call_impl!(proto $py, $slf.try_borrow_mut()?, $fn $(,$raw_args)* $(;$args)*)
+ };
+}
You can imagine how that went. Pages of errors... |
We don't have to make the situation perfect in a PR. Let's do that in the next one.
So... when do we want not to return |
Ok. Cool. It buys a little bit of time. Nevertheless, let me solve one remaining puzzle in this PR: I think the tests are not catching one error in this PR. I didn't changed |
Definitively. There's something wrong in the tests as they stand now. I added a trivial one focusing on #[test]
fn mistery_power() {
let gil = Python::acquire_gil();
let py = gil.python();
let c2 = PyCell::new(py, RichComparisonToSelf {}).unwrap();
py_run!(
py,
c2,
"\
class Other:
def __rpow__(self, other):
return other
assert c2 ** Other() == c2"
);
} |
Ah! The And I noticed I didn't implemented |
782e452
to
337aba8
Compare
I've just noticed that the guide promised to return NotImplemented for
Hum... was that implemented previously and got lost? |
I'm sorry but please wait for one or two days until I find the time to review the |
Ok. I'm completing the guide, which didn't have a section on PyNumberProtocol. |
I noticed that Should we include such change in this PR? |
I'm a little bit puzzled about how Python tells apart >>> class X:
... def __add__(self, other):
... return self
... def __radd__(self, other):
... return other
...
>>> X() + 1
<__main__.X at 0x7fd308820df0>
>>> 1 + X()
1 🤔 |
I confirmed that >>> class Foo:
... def __ipow__(self, other):
... return NotImplemented
...
>>> class X:
... def __rpow__(self, other):
... return other
...
>>> f = Foo()
>>> f **= X()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-16ca508b06d5> in <module>
----> 1 f **= X()
TypeError: unsupported operand type(s) for ** or pow(): 'Foo' and 'X' |
Our previous test were rather indirect: were relying that Python behaves correctly when we return NotImplemented. Now we only test that calling a pyclass dunder method returns NotImplemented when the argument doesn't match the type signature. This is the expected behavior.
45460fe
to
f830b7a
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.
Many thanks @mvaled, this PR is now looking excellent to me! Is there anything else you're planning to do with it?
I'm a little bit puzzled about how Python tells apart
__add__
and__radd__
, I see that both are assigned to thenb_add
slot. Yet they are both there:
Yeah, at the moment we actually ensure that only one of them is assigned to the slot: (__add__
if possible, else __radd__
):
pyo3/pyo3-derive-backend/src/defs.rs
Lines 769 to 774 in 0c59b05
SlotSetter { | |
proto_names: &["__add__"], | |
set_function: "set_add", | |
skipped_setters: &["set_radd"], | |
}, | |
SlotSetter::new(&["__radd__"], "set_radd"), |
This is actually a bug: #844. There's a little bit of discussion there already. If it interests you, I'd be very happy to mentor you through a solution to that ticket also? (I was planning to look at it myself soon, but so many pieces I'm busy with I'm always happy to share the work!)
I was thinking we should return NotImplemented in reversed operators as well. While studying the code of But, then, I don't see how CPython itself chooses the
I will look at it. Maybe there's already an explanation there. If think I'm up to the challenge I'll let you know. I rather let this PR as it is now and work the |
The won't be used because of PyO3#844.
Great! I'm satisfied with this PR, let's just wait for @kngwyu to confirm.
That's pretty much what I'm trying to get to in #844 - CPython doesn't just set |
Thank you! |
Co-authored-by: Yuji Kanagawa <yuji.kngw.80s.revive@gmail.com>
Thank you for all your guidance and support. I've committed the suggestions and also added a note to #844. I was looking at it to see if I could tackle it, but it would take some time before I could go back to it and study the CPython code (some deadlines here to meet). I'll let you know. |
Thank you for all your work!
Since our proc-macro implementation for |
Ok. I think I'll pass on this, but I'll be looking at your work to learn a bit more. I'm gonna review the list of issues remaining for 0.12 to see if there's anything I could do. |
❤️ as well as |
RichComparisonToSelf expects to compare itself with objects of the same type, but then we shouldn't raise TypeError but return NotImplemented.
Fixes #1064