Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 10 additions & 49 deletions guide/src/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ This chapter of the guide explains full usage of the `#[pyfunction]` attribute.
- [`#[pyo3(warn(message = "...", category = ...))]`](#warn)
- [Per-argument options](#per-argument-options)
- [Advanced function patterns](#advanced-function-patterns)
- [`#[pyfn]` shorthand](#pyfn-shorthand)

There are also additional sections on the following topics:

Expand Down Expand Up @@ -95,11 +94,11 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
```
- <a id="warn"></a> `#[pyo3(warn(message = "...", category = ...))]`

This option is used to display a warning when the function is used in Python. It is equivalent to [`warnings.warn(message, category)`](https://docs.python.org/3/library/warnings.html#warnings.warn).
The `message` parameter is a string that will be displayed when the function is called, and the `category` parameter is optional and has to be a subclass of [`Warning`](https://docs.python.org/3/library/exceptions.html#Warning).
This option is used to display a warning when the function is used in Python. It is equivalent to [`warnings.warn(message, category)`](https://docs.python.org/3/library/warnings.html#warnings.warn).
The `message` parameter is a string that will be displayed when the function is called, and the `category` parameter is optional and has to be a subclass of [`Warning`](https://docs.python.org/3/library/exceptions.html#Warning).
When the `category` parameter is not provided, the warning will be defaulted to [`UserWarning`](https://docs.python.org/3/library/exceptions.html#UserWarning).

> Note: when used with `#[pymethods]`, this attribute does not work with `#[classattr]` nor `__traverse__` magic method.
> Note: when used with `#[pymethods]`, this attribute does not work with `#[classattr]` nor `__traverse__` magic method.

The following are examples of using the `#[pyo3(warn)]` attribute:

Expand All @@ -110,20 +109,20 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
mod raising_warning_fn {
use pyo3::prelude::pyfunction;
use pyo3::exceptions::PyFutureWarning;

#[pyfunction]
#[pyo3(warn(message = "This is a warning message"))]
fn function_with_warning() -> usize {
42
}

#[pyfunction]
#[pyo3(warn(message = "This function is warning with FutureWarning", category = PyFutureWarning))]
fn function_with_warning_and_custom_category() -> usize {
42
}
}

# use pyo3::exceptions::{PyFutureWarning, PyUserWarning};
# use pyo3::types::{IntoPyDict, PyList};
# use pyo3::PyTypeInfo;
Expand All @@ -142,7 +141,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
# .unwrap();
# Ok(())
# }
#
#
# macro_rules! assert_warnings {
# ($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {
# catch_warning($py, |list| {
Expand All @@ -159,7 +158,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
# }).unwrap();
# };
# }
#
#
# Python::attach(|py| {
# assert_warnings!(
# py,
Expand All @@ -181,7 +180,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
# });
```

When the functions are called as the following, warnings will be displayed.
When the functions are called as the following, warnings will be displayed.

```python
import warnings
Expand All @@ -197,7 +196,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
UserWarning: This is a warning message
FutureWarning: This function is warning with FutureWarning
```

## Per-argument options

The `#[pyo3]` attribute can be used on individual arguments to modify properties of them in the generated function. It can take any combination of the following options:
Expand Down Expand Up @@ -268,42 +267,4 @@ arguments from the input `PyObject`s.
The `wrap_pyfunction` macro can be used to directly get a `Bound<PyCFunction>` given a
`#[pyfunction]` and a `Bound<PyModule>`: `wrap_pyfunction!(rust_fun, module)`.

## `#[pyfn]` shorthand

There is a shorthand to `#[pyfunction]` and `wrap_pymodule!`: the function can be placed inside the module definition and
annotated with `#[pyfn]`. To simplify PyO3, it is expected that `#[pyfn]` may be removed in a future release (See [#694](https://github.com/PyO3/pyo3/issues/694)).

An example of `#[pyfn]` is below:

```rust,no_run
use pyo3::prelude::*;

#[pymodule]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
#[pyfn(m)]
fn double(x: usize) -> usize {
x * 2
}

Ok(())
}
```

`#[pyfn(m)]` is just syntactic sugar for `#[pyfunction]`, and takes all the same options
documented in the rest of this chapter. The code above is expanded to the following:

```rust,no_run
use pyo3::prelude::*;

#[pymodule]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
#[pyfunction]
fn double(x: usize) -> usize {
x * 2
}

m.add_function(wrap_pyfunction!(double, m)?)
}
```

[`inspect.signature`]: https://docs.python.org/3/library/inspect.html#inspect.signature
1 change: 1 addition & 0 deletions newsfragments/5384.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate `pyfn` attribute.
17 changes: 11 additions & 6 deletions pyo3-macros-backend/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,15 +558,20 @@ fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn

for mut stmt in func.block.stmts.drain(..) {
if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt {
if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? {
if let Some((pyfn_span, pyfn_args)) = get_pyfn_attr(&mut func.attrs)? {
let module_name = pyfn_args.modname;
let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?;
let name = &func.sig.ident;
let statements: Vec<syn::Stmt> = syn::parse_quote! {
let statements: Vec<syn::Stmt> = syn::parse_quote_spanned! {
pyfn_span =>
#wrapped_function
{
use #pyo3_path::types::PyModuleMethods;
#module_name.add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?;
#[deprecated(note = "`pyfn` will be removed in a future PyO3 version, use declarative `#[pymodule]` with `mod` instead")]
#[allow(dead_code)]
const PYFN_ATTRIBUTE: () = ();
const _: () = PYFN_ATTRIBUTE;
}
};
stmts.extend(statements);
Expand Down Expand Up @@ -607,23 +612,23 @@ impl Parse for PyFnArgs {
}

/// Extracts the data from the #[pyfn(...)] attribute of a function
fn get_pyfn_attr(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Option<PyFnArgs>> {
let mut pyfn_args: Option<PyFnArgs> = None;
fn get_pyfn_attr(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Option<(Span, PyFnArgs)>> {
let mut pyfn_args: Option<(Span, PyFnArgs)> = None;

take_attributes(attrs, |attr| {
if attr.path().is_ident("pyfn") {
ensure_spanned!(
pyfn_args.is_none(),
attr.span() => "`#[pyfn] may only be specified once"
);
pyfn_args = Some(attr.parse_args()?);
pyfn_args = Some((attr.path().span(), attr.parse_args()?));
Ok(true)
} else {
Ok(false)
}
})?;

if let Some(pyfn_args) = &mut pyfn_args {
if let Some((_, pyfn_args)) = &mut pyfn_args {
pyfn_args
.options
.add_attributes(take_pyo3_options(attrs)?)?;
Expand Down
1 change: 1 addition & 0 deletions tests/test_compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
fn test_compile_errors() {
let t = trybuild::TestCases::new();

t.compile_fail("tests/ui/deprecated_pyfn.rs");
#[cfg(not(feature = "experimental-inspect"))]
t.compile_fail("tests/ui/invalid_property_args.rs");
t.compile_fail("tests/ui/invalid_proto_pymethods.rs");
Expand Down
179 changes: 113 additions & 66 deletions tests/test_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use pyo3::prelude::*;
use pyo3::py_run;
use pyo3::types::PyString;
use pyo3::types::{IntoPyDict, PyDict, PyTuple};
use pyo3::BoundObject;
use pyo3_ffi::c_str;

mod test_utils;
Expand Down Expand Up @@ -35,41 +34,44 @@ fn double(x: usize) -> usize {
x * 2
}

/// This module is implemented in Rust.
#[pymodule(gil_used = false)]
fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> {
#[pyfn(m)]
#[pyo3(name = "no_parameters")]
fn function_with_name() -> usize {
42
}
#[test]
fn test_module_with_functions() {
use pyo3::wrap_pymodule;

#[pyfn(m)]
#[pyo3(pass_module)]
fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult<Bound<'py, PyString>> {
module.name()
}
/// This module is implemented in Rust.
#[pymodule(gil_used = false)]
mod module_with_functions {
use super::*;

#[pyfn(m)]
fn double_value(v: &ValueClass) -> usize {
v.value * 2
}
#[pymodule_export]
use super::{AnonClass, ValueClass};

m.add_class::<AnonClass>()?;
m.add_class::<ValueClass>()?;
m.add_class::<LocatedClass>()?;

m.add("foo", "bar")?;
#[pymodule_init]
fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<LocatedClass>()?;
m.add("foo", "bar")?;
m.add_function(wrap_pyfunction!(double, m)?)?;
m.add("also_double", wrap_pyfunction!(double, m)?)?;
Ok(())
}

m.add_function(wrap_pyfunction!(double, m)?)?;
m.add("also_double", wrap_pyfunction!(double, m)?)?;
#[pyfunction]
#[pyo3(name = "no_parameters")]
fn function_with_name() -> usize {
42
}

Ok(())
}
#[pyfunction]
#[pyo3(pass_module)]
fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult<Bound<'py, PyString>> {
module.name()
}

#[test]
fn test_module_with_functions() {
use pyo3::wrap_pymodule;
#[pyfunction]
fn double_value(v: &ValueClass) -> usize {
v.value * 2
}
}

Python::attach(|py| {
let d = [(
Expand Down Expand Up @@ -118,6 +120,87 @@ fn test_module_with_functions() {
});
}

#[test]
#[allow(deprecated)]
fn test_module_with_pyfn() {
use pyo3::wrap_pymodule;

/// This module is implemented in Rust.
#[pymodule(gil_used = false)]
fn module_with_pyfn(m: &Bound<'_, PyModule>) -> PyResult<()> {
#[pyfn(m)]
#[pyo3(name = "no_parameters")]
fn function_with_name() -> usize {
42
}

#[pyfn(m)]
#[pyo3(pass_module)]
fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult<Bound<'py, PyString>> {
module.name()
}

#[pyfn(m)]
fn double_value(v: &ValueClass) -> usize {
v.value * 2
}

m.add_class::<AnonClass>()?;
m.add_class::<ValueClass>()?;
m.add_class::<LocatedClass>()?;

m.add("foo", "bar")?;

m.add_function(wrap_pyfunction!(double, m)?)?;
m.add("also_double", wrap_pyfunction!(double, m)?)?;

Ok(())
}

Python::attach(|py| {
let d = [("module_with_pyfn", wrap_pymodule!(module_with_pyfn)(py))]
.into_py_dict(py)
.unwrap();

py_assert!(
py,
*d,
"module_with_pyfn.__doc__ == 'This module is implemented in Rust.'"
);
py_assert!(py, *d, "module_with_pyfn.no_parameters() == 42");
py_assert!(py, *d, "module_with_pyfn.foo == 'bar'");
py_assert!(py, *d, "module_with_pyfn.AnonClass != None");
py_assert!(py, *d, "module_with_pyfn.LocatedClass != None");
py_assert!(
py,
*d,
"module_with_pyfn.LocatedClass.__module__ == 'module'"
);
py_assert!(py, *d, "module_with_pyfn.double(3) == 6");
py_assert!(
py,
*d,
"module_with_pyfn.double.__doc__ == 'Doubles the given value'"
);
py_assert!(py, *d, "module_with_pyfn.also_double(3) == 6");
py_assert!(
py,
*d,
"module_with_pyfn.also_double.__doc__ == 'Doubles the given value'"
);
py_assert!(
py,
*d,
"module_with_pyfn.double_value(module_with_pyfn.ValueClass(1)) == 2"
);
py_assert!(
py,
*d,
"module_with_pyfn.with_module() == 'module_with_pyfn'"
);
});
}

/// This module uses a legacy two-argument module function.
#[pymodule]
fn module_with_explicit_py_arg(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
Expand Down Expand Up @@ -319,42 +402,6 @@ fn test_module_nesting() {
});
}

// Test that argument parsing specification works for pyfunctions

#[pyfunction(signature = (a=5, *args))]
fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyResult<Py<PyAny>> {
[
a.into_pyobject(py)?.into_any().into_bound(),
args.as_any().clone(),
]
.into_pyobject(py)
.map(Bound::unbind)
}

#[pymodule]
fn vararg_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
#[pyfn(m, signature = (a=5, *args))]
fn int_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyResult<Py<PyAny>> {
ext_vararg_fn(py, a, args)
}

m.add_function(wrap_pyfunction!(ext_vararg_fn, m)?).unwrap();
Ok(())
}

#[test]
fn test_vararg_module() {
Python::attach(|py| {
let m = pyo3::wrap_pymodule!(vararg_module)(py);

py_assert!(py, m, "m.ext_vararg_fn() == [5, ()]");
py_assert!(py, m, "m.ext_vararg_fn(1, 2) == [1, (2,)]");

py_assert!(py, m, "m.int_vararg_fn() == [5, ()]");
py_assert!(py, m, "m.int_vararg_fn(1, 2) == [1, (2,)]");
});
}

#[test]
fn test_module_with_constant() {
// Regression test for #1102
Expand Down
Loading
Loading