Conversation
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, I have a few quick thoughts on this:
-
👍 on adding
add_function, andadd_module, though I would like to also keep support foradd_wrappedfor now, because I don't see a strong reason that users would need to migrate existing code. I'm thinking in 0.12 we can add a doc toadd_wrappedsaying prefer the new methods, and in 0.13 we can mark as deprecated or consider a broader cleanup. (I still would love to have the new module syntax discussed in #694) -
Similarly, I think asking users to migrate to
#[pyfunction(free)]and also changing the arguments towrap_pyfunction!is quite a lot of churn to existing code. I agree you're probably right that most pyfunctions want to be part of modules, but I think we don't necessarily need to force users to change existing code immediately.
I've added an inline comment with a suggestion of how we might be able to achieve a similar result to what you've got, but without forcing ecosystem churn.
pyo3-derive-backend/src/module.rs
Outdated
| } | ||
| Ok(quote! { | ||
| fn #function_wrapper_ident(py: pyo3::Python) -> pyo3::PyObject { | ||
| fn #function_wrapper_ident(py: pyo3::Python #maybe_module_arg) -> pyo3::PyObject { |
There was a problem hiding this comment.
How about here, instead of requiring the module arg optionally depending on the free attribute, we add a little trait to derive_utils, e.g.
trait WrapPyFunctionArguments<'a> {
fn arguments(self) -> (Python<'a>, Option<&'a PyModule>);
}
impl<'a> WrapPyFunctionArguments<'a> for Python<'a> {
fn arguments(self) -> (Python<'a>, Option<&'a PyModule>>) { (self, None) }
}
impl<'a> WrapPyFunctionArguments<'a> for &'a PyModule {
fn arguments(self) -> (Python<'a>, Option<&'a PyModule>>) { (self.py(), Some(self)) }
}Then it should be possible to call either wrap_pyfunction!(foo)(py) or wrap_pyfunction!(foo)(module) if we change this line to:
| fn #function_wrapper_ident(py: pyo3::Python #maybe_module_arg) -> pyo3::PyObject { | |
| fn #function_wrapper_ident(args: impl pyo3::derive_utils::WrapPyFunctionArguments) -> pyo3::PyObject { |
And in this generated wrapper code here we could do different things depending on whether a PyModule is provided.
There was a problem hiding this comment.
EDIT: This idea might not work because wrap_pyfunction! takes a reference to the wrapper, and by adding the impl Trait argument I don't think it can have a reference to it.
If that's the case, I'd still like us to look for a similar trick so that the new functionality comes with new syntax, without changing the existing downstream code.
EDIT 2: Looks like taking reference to functions with impl Trait arguments can work fine: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=bb49984a3d7a9ec9277d7f1cd0b5ee8e
|
Thanks for the comments!
I don't think there are many uses of I'll look into implementing your suggestions tomorrow! |
👍 I guess you're probably right about this! It's sometimes hard to judge how much certain patterns are used downstream. My point still stands that if we can have our cake and eat it (i.e. add new feature without changing any existing code), that'd be the best thing =) |
e296c2b to
e34294a
Compare
davidhewitt
left a comment
There was a problem hiding this comment.
🎉 looks like my suggestion worked!
|
This works pretty well, although figuring out the lifetimes was a bit of work. Nice suggestions! Now we have to figure out a way to signal which |
src/derive_utils.rs
Outdated
|
|
||
| /// Trait to abstract over the arguments of Python function wrappers. | ||
| #[doc(hidden)] | ||
| pub trait WrapPyFunctionArguments<'a, 'b> { |
There was a problem hiding this comment.
Is there a reason to have two lifetime arguments in this trait? If we set them to the same, then Rust can just shorten the longer lifetime to match, which seems good enough to me.
Also, looking again at this trait, I noticed a different pattern to achieve the same thing:
pub enum WrapPyFunctionArguments<'a> {
Python(Python<'a>),
Module(&'a PyModule)
}
And can then just implement Into<WrapPyFunctionArguments> for each of Python and &PyModule, and use impl Into<WrapPyFunctionArguments> in the generated wrapper.
I think that maybe the enum is less confusing to understand in the long run, I'm also happy to leave it as-is (as long as we take the second lifetime argument out). Just an idea in cause you find you prefer one design over the other.
There was a problem hiding this comment.
Is there a reason to have two lifetime arguments in this trait? If we set them to the same, then Rust can just shorten the
longer lifetime to match, which seems good enough to me.
That's weird, I was trying that for almost an hour, now I went back to verify that it in fact doesn't work...and...it compiles!
I'll check how nice the enum variant turns out
e34294a to
4e3a35f
Compare
|
PyPy doesn't seem to have a |
4e3a35f to
ccfb358
Compare
|
@sebpuetz |
|
Yeah, I'm now going through |
ccfb358 to
6e17a05
Compare
kngwyu
left a comment
There was a problem hiding this comment.
Thanks, nicely designed 👍
BTW, how about changing the argument of add_function to wrapper: &impl Fn(&'a Self) -> PyResult<PyObject>?
pyo3-derive-backend/src/module.rs
Outdated
| args: impl Into<pyo3::derive_utils::WrapPyFunctionArguments<'a>> | ||
| ) -> pyo3::PyObject { | ||
| let arg = args.into(); | ||
| let (py, maybe_module) = match arg { |
There was a problem hiding this comment.
How about making this a method of WrapPyFunctionArguments?
PyFunction_New was previously implemented as a Rust function wrapper around a call to the extern C function PyFunction_NewExt with a hard-coded third argument. This commit removes the Rust wrapper and directly exposes the function from the CPython API.
Previously neither the module nor the name of the module of pyfunctions were registered. This commit passes the module and its name when creating a new pyfunction. PyModule::add_function and PyModule::add_module have been added and are set to replace `add_wrapped` in a future release. `add_wrapped` is kept for compatibility reasons during the transition. Depending on whether a `PyModule` or `Python` is the argument for the Python function-wrapper, the module will be registered with the function.
Looking into this now, there are also a few |
|
I don't see how that's possible while not breaking existing code: If If the proc-macro generates Fwiw, I'm fine with making that breaking change and have all So, should we change all The other solution is complicating the codegen and create different wrappers... |
If |
Suggestion by @kngwyu. Additionally replace some `expect` calls with error handling.
|
Well, that doesn't work because the argument to |
Wrapping a function can fail if we can't get the module name. Based on suggestion by @kngwyu
6e17a05 to
3214249
Compare
|
I think I found a solution by making the return value of the |
This commit makes it possible to access the module of a function by passing the `need_module` argument to the pyfn and pyfunction macros.
6f96ae8 to
4aae523
Compare
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, this looks great to me🚀
Great docs and tests much appreciated 😊
Couple of final suggestions from me.
guide/src/function.md
Outdated
| use pyo3::wrap_pyfunction; | ||
| use pyo3::prelude::*; | ||
|
|
||
| #[pyfunction(need_module)] |
There was a problem hiding this comment.
Bikeshedding: I'd like to propose to call the attribute pass_module.
Motivation is that this was already the verb used in #828
Also I've seen this phrasing before e.g. in Python's click library: https://click.palletsprojects.com/en/7.x/api/#click.pass_context
guide/src/function.md
Outdated
|
|
||
| ### Accessing the module of a function | ||
|
|
||
| Functions are usually associated with modules, in the C-API, the self parameter in a function call corresponds |
There was a problem hiding this comment.
I think this first sentence is probably an implementation detail that the user doesn't need to know? Might want to take it out for simplicity.
guide/src/function.md
Outdated
| # fn main() {} | ||
| ``` | ||
|
|
||
| If `need_module` is set, the first argument **must** be the `&PyModule`. It is then possible to interact with |
There was a problem hiding this comment.
Maybe "it is then possible to use the module in the function body"?
guide/src/function.md
Outdated
| # fn main() {} | ||
| ``` | ||
|
|
||
| Within Python, the name of the module that a function belongs to can be accessed through the `__module__` |
There was a problem hiding this comment.
I think this is probably true of all Python functions (although you've just fixed this for pyO3) so not sure this sentence needs to be in this section?
|
Oh also, CHANGELOG entry please! 😄 |
de0d708 to
410dfc0
Compare
410dfc0 to
e65b849
Compare
|
Looks pretty green to me ;) |
src/types/module.rs
Outdated
| /// You can also add a function with a custom name using [add](PyModule::add): | ||
| /// | ||
| /// ```rust,ignore | ||
| /// m.add("also_double", wrap_pyfunction!(double)(py, m)); |
There was a problem hiding this comment.
Does this need to handle the Result from wrap_pyfunction?
There was a problem hiding this comment.
Ah, good spot. This I think is an outdated doc and probably shows why using ignore in docstrings is usually a bad idea! Maybe this should be updated to not use ignore?
There was a problem hiding this comment.
Yeah, nice catch. Looks we need to update this.
There was a problem hiding this comment.
I made all the examples in the docs of fn add_X runnable.
src/types/module.rs
Outdated
| @@ -194,11 +195,50 @@ impl PyModule { | |||
| /// ```rust,ignore | |||
| /// m.add("also_double", wrap_pyfunction!(double)(py)); | |||
There was a problem hiding this comment.
arguments are (py) here, but (py, m) below?
There was a problem hiding this comment.
I missed these because the tests are ignored, I'll go through the guide again to make the add_X calls consistent. The correct way to add a function is to pass the PyModule to the wrapper. Python can be acquired from PyModule.
There was a problem hiding this comment.
(py, m) was an outdated design we removed. ignore docstring example probably was a mistake.
wrap_pyfunction! output can take either py (for backwards compatibility, will probably deprecate eventually) or m (preferred, sets the __module__ correctly).
25b105f to
06cd7c7
Compare
The C-exported wrapper generated through `#[pymodule]` is only required for the top-level module.
|
@davidhewitt I pushed the changes discussed in #1149 as a new commit |
|
Thanks, I'll try to review this later today (I have some related experiments from importing submodules for #759 from a few weeks' ago). |
LGTM, let's merge this after David reviews. |
|
👍 I did some thinking this morning on this and I believe it will cause no issues with what I was trying out; I'll confirm for definite later. |
|
Yep looks great, let's merge this! 🎉 |
|
Thank you for your hard work on this! |
The change to wrap_pyfunction!() PyO3#1143 makes it impossible to implement `context.add_wrapped(wrap_pyfunction!(something))` in `inline-python`. `context` does not carry the GIL lifetime, which causes type deduction trouble now that `wrap_pyfunction` results in a generic function. ``` error[E0308]: mismatched types --> examples/rust-fn.rs:12:4 | 12 | c.add_wrapped(wrap_pyfunction!(rust_print)); | ^^^^^^^^^^^ one type is more general than the other | = note: expected enum `Result<&pyo3::types::PyCFunction, _>` found enum `Result<&pyo3::types::PyCFunction, _>` ``` By re-wrapping the function as a closure, we trigger 'closure signature hinting' when passing `wrap_pyfunction!()` as an argument to a function: Rustc will set the signature of the closure from the function that closure is passed to. This way, the generic arguments can be deduced in more contexts, fixing the problem.
The change to wrap_pyfunction!() PyO3#1143 makes it impossible to implement `context.add_wrapped(wrap_pyfunction!(something))` in `inline-python`. `context` does not carry the GIL lifetime, which causes type deduction trouble now that `wrap_pyfunction` results in a generic function. ``` error[E0308]: mismatched types --> examples/rust-fn.rs:12:4 | 12 | c.add_wrapped(wrap_pyfunction!(rust_print)); | ^^^^^^^^^^^ one type is more general than the other | = note: expected enum `Result<&pyo3::types::PyCFunction, _>` found enum `Result<&pyo3::types::PyCFunction, _>` ``` By re-wrapping the function as a closure, we trigger 'closure signature deduction' when passing `wrap_pyfunction!()` as an argument to a function: Rustc will deduce the signature of the closure from the function that closure is passed to. This way, the generic arguments can be deduced in more contexts, fixing the problem.
The change to wrap_pyfunction!() PyO3#1143 makes it impossible to implement `context.add_wrapped(wrap_pyfunction!(something))` in `inline-python`. `context` does not carry the GIL lifetime, which causes type deduction trouble now that `wrap_pyfunction` results in a generic function. ``` error[E0308]: mismatched types --> examples/rust-fn.rs:12:4 | 12 | c.add_wrapped(wrap_pyfunction!(rust_print)); | ^^^^^^^^^^^ one type is more general than the other | = note: expected enum `Result<&pyo3::types::PyCFunction, _>` found enum `Result<&pyo3::types::PyCFunction, _>` ``` By re-wrapping the function as a closure, we trigger 'closure signature deduction' when passing `wrap_pyfunction!()` as an argument to a function: Rustc will deduce the signature of the closure from the function that closure is passed to. This way, the generic arguments can be deduced in more contexts, fixing the problem.
The change to wrap_pyfunction!() in PyO3#1143 makes it impossible to implement `context.add_wrapped(wrap_pyfunction!(something))` in `inline-python`. `context` does not carry the GIL lifetime, which causes type deduction trouble now that `wrap_pyfunction` results in a generic function. ``` error[E0308]: mismatched types --> examples/rust-fn.rs:12:4 | 12 | c.add_wrapped(wrap_pyfunction!(rust_print)); | ^^^^^^^^^^^ one type is more general than the other | = note: expected enum `Result<&pyo3::types::PyCFunction, _>` found enum `Result<&pyo3::types::PyCFunction, _>` ``` By re-wrapping the function as a closure, we trigger 'closure signature deduction' when passing `wrap_pyfunction!()` as an argument to a function: Rustc will deduce the signature of the closure from the function that closure is passed to. This way, the generic arguments can be deduced in more contexts, fixing the problem.
Closes #828
PyModuleshould be passed