-
Notifications
You must be signed in to change notification settings - Fork 792
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
Add native Function types. #1163
Conversation
Thanks! As the
|
Add bindings for PyCFunction, PyFunction, PyClassMethod and PyStaticMethod.
Makes sense, I removed the two other method types. I'm wondering if we can find a (nice) way to replace the Without much thought put into this, I'd see 1-2 ways:
|
I think |
I don't see the benefit of providing an API that takes a function to produce a function. I don't think the function-to-produce-another-function is used in any other context than inside Additionally, it's not intuitive at all: Why would E.g.: #[pyfunction]
fn foo() {}
#[pymodule]
fn bar(_py: Python, module: &PyModule) -> PyResult<()> {
let module = PyModule::new("some_module")?;
let fun: &PyCFunction = init_pycfunction!(foo, module)?; // type annotation just for clarity
module.add_function(fun)?;
}
impl PyModule {
fn add_function(&self, fun: &PyCFunction) -> PyResult<()> {
[...]
}
} |
Adding to this: struct Foo(String);
impl Foo {
fn new() {
Foo(String::new)
}
}
struct Bar(Vec<Foo>);
impl Bar {
fn add_foo(&mut self, foo_constructor: impl Fn() -> Foo) {
self.0.push(foo_constructor())
}
} instead of: fn add_foo(&mut self, foo: Foo) {
[...]
} |
Hmm... 🤔 fn #function_wrapper_ident() -> pyo3::class::PyMethodDef {
#wrapper
pyo3::class::PyMethodDef {
ml_name: stringify!(#python_name),
ml_meth: pyo3::class::PyMethodType::PyCFunctionWithKeywords(__wrap),
ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS,
ml_doc: #doc,
};
} and m.add_function(PyCFunction::new(py, wrap_function!(function))); .
|
I think we're getting there! I'd just like to add two points:
But I don't think there's a way around this unless |
impl PyCFunction {
pub fn new<'a>(name: &'static str, fun: PyCFunctionWithKeywords, doc: &'static str, m: &'a PyModule) -> PyResult<&'a PyCFunction> {
let py = m.py();
let def = crate::class::PyMethodDef {
ml_name: name,
ml_meth: crate::class::PyMethodType::PyCFunctionWithKeywords(fun),
ml_flags: ffi::METH_VARARGS | ffi::METH_KEYWORDS,
ml_doc: doc,
};
let def = def.as_method_def();
let mod_ptr = m.as_ptr();
let name = m.name()?;
let name = name.into_py(py);
unsafe {
py.from_owned_ptr_or_err::<PyCFunction>(
ffi::PyCFunction_NewEx(
Box::into_raw(Box::new(def)),
mod_ptr,
name.as_ptr()
)
)
}
}
} What's your take on an API like this? I'd imagine using it like: #[pyfunction]
fn some_fun() {}
#[pymodule]
fn some_mod(_py: Python, module: &PyModule) -> PyResult<()> {
let c_wrapper: PyCFunctionWithKeywords = wrap_pyfunction!(some_fun)?; // annotation again just for visualization
let fun = PyCFunction::new("fun_py_name", c_wrapper, "Documentation can be defined anywhere", module)?;
module.add_function(fun)?;
} There are probably ways to make |
I haven't made changes to I did not remove the old way of getting the edit CI failure doesn't seem to be related, looks like some network / pypi error. |
I prefer to pub fn new(py: &Python, method_def: PyMethodDef) -> PyResult<&Self> { ... } due to its simplicity.
|
edit Sorry for the noise, I didn't properly read the second part of your reply.
|
I thought a bit more about your proposal and it's really not clear to me how an API for What did you have in mind? |
FWIW I think the most efficient solution is for
Using the currently proposed I want to suggest it's enough in this PR to just change In my eyes this is is now the only blocker to the release of 0.12. So I'd like us to agree on a minimal API we expect to be stable, merge that ASAP, and design the rest in follow up PRs once 0.12 is out the door. |
I don't think it's a "just put the I feel like making
This should be identical to what the proc-macro is doing (apart from the two string copies), at least I copied the expanded code? Although, I might be misunderstanding what you are referring to. |
Currently Though I'm hoping we revisit this cost for 0.13. So maybe it's ok if we just take |
That cost should be negligible since it only happens once while initializing the module? I don't know enough about the differences between &PyX vs |
I just thought most users don't want to construct So I think it's better to begin from thinking of the use case of |
I have reverted the last commit adding the constructors, feel free to add an API that works for you. The motivation to have them was offering a transparent way of constructing a I don't agree that the constructors are too complex either, e.g. this would have been how to construct a #[pyfunction]
fn bar<'a>(fun: &'a PyCFunction) {
[...]
}
#[pymodule]
fn module_with_functions(_py: Python<'_>, module: &PyModule) -> PyResult<()> {
let doc = "some docs";
let fun = PyCFunction::new_with_keywords(pyo3::raw_pycfunction!(bar), "name", some_docs, module)?;
module.add(fun)
} I's clearer and allows for runtime interaction with the constructors. It was also simple to manually implement a |
OK, then let's go in that direction. Please push the reverted commit again. |
Good points. I think most users will still want the "standard" use case to be as short as possible to type, so perhaps we can introduce a two-argument form of
|
The implementation could be really complicated from the current status so I started some refactoring #1169 |
Hmm... 🤔 I think it still needs |
Fair enough, we probably don't want to keep every docstring twice, so we can leave it up to the caller to provide a |
Change add_function to take `&PyCFunction` instead of a wrapper fn and ensure that dostrings of functions are `&'static str`.
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 looks good to me now for inclusion in 0.12. @kngwyu just checking you're happy with this before I merge? I'd like to create the release PR this evening ideally :)
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 !
There's a room for improvements but it would require more internal changes...
m.add_function(wrap_pyfunction!(make_date, m)?)?; | ||
m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?; | ||
m.add_function(wrap_pyfunction!(date_from_timestamp, m)?)?; | ||
m.add_function(wrap_pyfunction!(make_time, m)?)?; | ||
m.add_function(wrap_pyfunction!(get_time_tuple, m)?)?; | ||
m.add_function(wrap_pyfunction!(make_delta, m)?)?; | ||
m.add_function(wrap_pyfunction!(get_delta_tuple, m)?)?; | ||
m.add_function(wrap_pyfunction!(make_datetime, m)?)?; | ||
m.add_function(wrap_pyfunction!(get_datetime_tuple, m)?)?; | ||
m.add_function(wrap_pyfunction!(datetime_from_timestamp, m)?)?; |
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.
Why did you change these?
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.
So they make use of the two-argument form of wrap_pyfunction. The other choices are using the soon-to-be-deprecated add_wrapped or calling the function returned by the single-argument form.
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.
The other choices are using the soon-to-be-deprecated add_wrapped or calling the function returned by the single-argument form.
But why passing module to PyCFunction_NewEx
is such important?
We can set module anywhen.
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.
What do we gain from passing Python instead of the pymodule? I'd say setting the module should be the default for PyCFunctions
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.
OK, I got it.
I looked around related APIs but there's no API for setting module other than the m_module
field itself. Maybe you're correct.
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.
We can expose this in the api of PyCFunction but I'd be careful with setting this in e.g. add_function. Otherwise a module could "steal" another modules function.
So imo that would need some more (careful) thought to figure out how to deep copy a function when adding it to a module instead of just taking another reference to a function already existing elsewhere.
For now I'd suggest to leave it as is and address this in 0.13
🚀 let's merge and I'll get on with writing release PR for 0.12 |
Add bindings for PyCFunction, PyFunction, PyClassMethod and
PyStaticMethod.
#1156
This probably requires some more work, especially I'm unsure whether we should bind
PyClassMethod
andPyStaticMethod
. Extraction only succeeds for those if they are not bound to a class, e.g.:Defining these methods in a class leads to errors like this:
I have not included any APIs for the Rust types. For
PyCFunction
we can probably offercall()
with the same signature asPyAny
but directly go throughPyCFunction_Call
. Another rather obvious functions are probablyname()
andmodule()
, perhaps alsoget_self()
or something along these lines.I'm expecting failures for pypy since I only guessed the names of the function type objects.