Skip to content
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

verify py method args count #2083

Merged
merged 4 commits into from Jan 7, 2022
Merged

verify py method args count #2083

merged 4 commits into from Jan 7, 2022

Conversation

aviramha
Copy link
Member

@aviramha aviramha commented Dec 31, 2021

This fixes #2010. During the change I found out that __ipow__ didn't receive the correct number of arguments.
While fixing it, I encountered a CPython 3.7 bug that passes invalid 3rd parameter in 3.7, so I made the parameters version dependant (3.7>= use 2 params, 3.8>= 3 params)

@aviramha aviramha force-pushed the magic_methods branch 7 times, most recently from 48b3dd0 to 59c3740 Compare December 31, 2021 15:30
@aviramha
Copy link
Member Author

trying to fix #2010
I'm currently stuck having segfault.. help?

@aviramha
Copy link
Member Author

aviramha commented Jan 2, 2022

Ah! Found it https://bugs.python.org/issue36379

@aviramha
Copy link
Member Author

aviramha commented Jan 2, 2022

I'm not sure why the Py_3 cfg doesn't work for the tests?

@aviramha aviramha force-pushed the magic_methods branch 3 times, most recently from 204e6ff to 8f271fd Compare January 2, 2022 18:06
@aviramha aviramha marked this pull request as ready for review January 2, 2022 18:21
@aviramha aviramha changed the title *WIP* verify py method args count verify py method args count Jan 2, 2022
@aviramha aviramha requested a review from mejrs January 2, 2022 20:02
@aviramha
Copy link
Member Author

aviramha commented Jan 2, 2022

Ready for review @davidhewitt

@aviramha aviramha self-assigned this Jan 2, 2022
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.

Wow, thanks for fixing this up, and finally getting to the bottom of the issue with __ipow__ is awesome 👍

As an FYI my laptop keyboard is currently broken so I'm not sure how well I can check in on PRs for the next couple of weeks. Writing longer comments is hard on mobile so I may be a bit slow replying sometimes.

I think the build-dependency on pyo3-build-config for pyo3-macros-backend is unfortunately not correct, however there are alternative ways to resolve it. See other comments.

As a bit of bikeshedding, I also wonder if we should always allow users to write three-argument __ipow__ and then just pass None always on 3.7. If we document this behaviour it seems like the easiest API for users to implement, rather than forcing them to use #[cfg].

use pyo3_build_config::pyo3_build_script_impl::{errors::Result, resolve_interpreter_config};

fn configure_pyo3() -> Result<()> {
let interpreter_config = resolve_interpreter_config()?;
Copy link
Member

Choose a reason for hiding this comment

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

If I remember correctly, using resolve_interpreter_config from a proc-macro crate won't work correctly when cross compiling, because the environment variables provided by cargo are for the host system, not the cross-compile target.

So I don't think we can add this build script and use #[cfg] in pyo3_build_config. Instead in pyo3_build_config we should use the runtime APIs, like in

fn can_use_fastcall() -> bool {
const PY37: pyo3_build_config::PythonVersion =
pyo3_build_config::PythonVersion { major: 3, minor: 7 };
let config = pyo3_build_config::get();
config.version >= PY37 && !config.abi3
}

Sorry that I did not document this 😭

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought of using it but I was pretty sure I can't do ifs and logic when defining globals? I am still pretty confident?

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, I think you'll need to define both globals and then check which one to use based on the Python version later in the macro code.

E.g. __IPOW__ and __IPOW_PY_3_7__, and then check which one to use at runtime.

This approach might not be so easy for #[pyproto], in which case I'd be fine with leaving #[pyproto] broken. We're deprecating it anyway 😁

Comment on lines 553 to 555
#[cfg(Py_3_8)]
#[pymethods]
impl RichComparisonToSelf {
Copy link
Member

Choose a reason for hiding this comment

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

To avoid passing this #[cfg] pain onto users, do you think we can make it so that users always write three-argument __ipow__, and then on Python 3.7 we just always fill in None as the third argument?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure how to implement this approach and not sure if it's correct. How will the user know he wont ever receive values in Py 3.7 (besides doc?) I think compile error and strict adjustment is better...?
The issue happens where the extract layer of PyO3 tries to convert Option when it's actually an invalid value... The interface would be then OptionInvalid where it will check for python version and do it based on that..?

Copy link
Member

Choose a reason for hiding this comment

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

Agreed that compiler errors are more explicit.

I was thinking that given this is such an edge case anyway, and Python code using =** will always pass None, it's only if users explicitly invoke __ipow__ that there's ever a non-none value. So it doesn't seem so bad if we say "on CPython 3.7, due to an interpreter bug, you will always get None".

(Actually this makes me wonder, does it work with PyPy?)

If you're willing to give me a few days I'll write a PR which attempts to implement this always-pass-none case, to see if we like it?

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay. Would we need the globals in that case though? I think not? so I'll wait with further changes until you do it.
If you want you can give me some pointers on how to get it done. I thought maybe setting the Ty::Object to be Ty::ObjectOrInvalid?

@@ -288,7 +288,7 @@ This trait also has support the augmented arithmetic assignments (`+=`, `-=`,
* `fn __itruediv__(&'p mut self, other: impl FromPyObject) -> PyResult<()>`
* `fn __ifloordiv__(&'p mut self, other: impl FromPyObject) -> PyResult<()>`
* `fn __imod__(&'p mut self, other: impl FromPyObject) -> PyResult<()>`
* `fn __ipow__(&'p mut self, other: impl FromPyObject) -> PyResult<()>`
* `fn __ipow__(&'p mut self, other: impl FromPyObject, _modulo: impl FromPyObject) -> PyResult<()>`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* `fn __ipow__(&'p mut self, other: impl FromPyObject, _modulo: impl FromPyObject) -> PyResult<()>`
* `fn __ipow__(&'p mut self, other: impl FromPyObject, modulo: impl FromPyObject) -> PyResult<()>`

We should also document the special case for 3.7 (whether it's to have two arguments, or always pass None).

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed the doc.. A suggestion for formatting would be welcome (couldn't think of anything pretty)

Comment on lines +1 to +5
error: Expected 1 arguments, got 0
--> tests/ui/invalid_pymethod_proto_args.rs:8:8
|
8 | fn __truediv__(&self) -> PyResult<()> {
| ^^^^^^^^^^^
Copy link
Member

Choose a reason for hiding this comment

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

Do you think we should include the receiver in these counts? E.g. the above becomes Expected 2 arguments, got 1 ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I was thinking about it but it might be a bit of a burden because we can also have "py" argument and take it into account which might be confusing?

Copy link
Member

Choose a reason for hiding this comment

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

Ah I see. Can we add a test with py to confirm the behavior is correct in that situation too?

Other than that, I'm happy for us to leave the error message like this for now. We can always make adjustments later after we get user feedback 👍

`pyo3-macros-backend` is now compiled with PyO3 cfgs to enable different magic method definitions based on version.
Add check for correct number of arguments on magic methods.
@davidhewitt
Copy link
Member

I've pushed a commit which implements the strategy to always pass None on Python 3.7. Let me know what you think of it?

@aviramha
Copy link
Member Author

aviramha commented Jan 5, 2022

The changes look great. I'm still a bit concerned about the user getting confused why __ipow__ doesn't work as expected.
Can we maybe shoot a compiler warning if __ipow__ is implemented and compiled for Py3.7?
Sorry I am being annoyed by this - but as it seems someone was already haunted by this issue (without finding why) and then I had hell finding the root cause so I wish to help others avoid it 😆

@davidhewitt
Copy link
Member

Hmm, I take your point. It's not possible for us to emit a compiler warning (unless on nightly).

Maybe instead of always returning None, we should panic! in IPowModulo::extract on Python 3.7? That way there is a safe crash instead of a segfault, and we can accompany the crash with a clear error message. Anyone implementing __ipow__ with decent testing will then understand they can't use that argument on 3.7.

At least that way users will only need to have the #[cfg(Py_3_8)] or whatever for a few statements inside the function body, rather than on the whole function or #[pymethods] block.

If you agree with that idea, I'll pass this PR back into your hands to finish off (at your leisure) if that's ok? I'm a bit tight on time for the next few days, so I'll end up being a blocker most likely 😬

@aviramha
Copy link
Member Author

aviramha commented Jan 6, 2022

I'm not sure how you mean to achieve that without blocking users the usage of __ipow__ in Py3.7?
I think I'll drop it for now as it might be less cost-efficient to make it perfect.

@aviramha
Copy link
Member Author

aviramha commented Jan 6, 2022

If tests pass now I'm okay with merging as is.

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.

👍 thanks very much again for fixing this!

@davidhewitt davidhewitt merged commit 2cee7fe into PyO3:main Jan 7, 2022
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.

Magic methods in #[pymethods] don't check they have the correct number of arguments
3 participants