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

Add #[pyo3(deprecated(...))] to #[pyfunction] (#4316) #4364

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,8 @@ Python::with_gil(|py| {
> Note: if the method has a `Result` return type and returns an `Err`, PyO3 will panic during
class creation.

> Note: `#[classattr]` does not work with [`#[pyo3(warn(...))]`](./function.md#warn) attribute nor [`#[pyo3(deprecated)]`](./function.md#deprecated) attribute.

If the class attribute is defined with `const` code only, one can also annotate associated
constants:

Expand Down
2 changes: 2 additions & 0 deletions guide/src/class/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ cleared, as every cycle must contain at least one mutable reference.
- `__traverse__(<self>, pyo3::class::gc::PyVisit<'_>) -> Result<(), pyo3::class::gc::PyTraverseError>`
- `__clear__(<self>) -> ()`

> Note: `__traverse__` does not work with [`#[pyo3(warn(...))]`](../function.md#warn) nor [`#[pyo3(deprecated)]`](../function.md#deprecated) attribute.

Example:

```rust
Expand Down
116 changes: 116 additions & 0 deletions guide/src/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ This chapter of the guide explains full usage of the `#[pyfunction]` attribute.
- [`#[pyo3(signature = (...))]`](#signature)
- [`#[pyo3(text_signature = "...")]`](#text_signature)
- [`#[pyo3(pass_module)]`](#pass_module)
- [`#[pyo3(warn(message = "...", category = ...))]`](#warn)
- [`#[pyo3(deprecated = "...")]`](#deprecated)
- [Per-argument options](#per-argument-options)
- [Advanced function patterns](#advanced-function-patterns)
- [`#[pyfn]` shorthand](#pyfn-shorthand)
Expand Down Expand Up @@ -96,6 +98,120 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)
}
```
- <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.12/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.12/library/exceptions.html#Warning).
When the `category` parameter is not provided, the warning will be defaulted to [`UserWarning`](https://docs.python.org/3.12/library/exceptions.html#UserWarning).

> 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:

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

#[pymodule]
mod raising_warning_fn {
FlickerSoul marked this conversation as resolved.
Show resolved Hide resolved
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;
#
# fn catch_warning(py: Python<'_>, f: impl FnOnce(&Bound<'_, PyList>) -> ()) -> PyResult<()> {
# let warnings = py.import_bound("warnings")?;
# let kwargs = [("record", true)].into_py_dict(py);
# let catch_warnings = warnings
# .getattr("catch_warnings")?
# .call((), Some(&kwargs))?;
# let list = catch_warnings.call_method0("__enter__")?.downcast_into()?;
# warnings.getattr("simplefilter")?.call1(("always",))?; // show all warnings
# f(&list);
# catch_warnings
# .call_method1("__exit__", (py.None(), py.None(), py.None()))
# .unwrap();
# Ok(())
# }
#
# macro_rules! assert_warnings {
# ($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {
# catch_warning($py, |list| {
# $body;
# let expected_warnings = [$((<$category as PyTypeInfo>::type_object_bound($py), $message)),+];
# assert_eq!(list.len(), expected_warnings.len());
# for (warning, (category, message)) in list.iter().zip(expected_warnings) {
# assert!(warning.getattr("category").unwrap().is(&category));
# assert_eq!(
# warning.getattr("message").unwrap().str().unwrap().to_string_lossy(),
# message
# );
# }
# }).unwrap();
# };
# }
#
# Python::with_gil(|py| {
# assert_warnings!(
# py,
# {
# let m = pyo3::wrap_pymodule!(raising_warning_fn)(py);
# let f1 = m.getattr(py, "function_with_warning").unwrap();
# let f2 = m.getattr(py, "function_with_warning_and_custom_category").unwrap();
# f1.call0(py).unwrap();
# f2.call0(py).unwrap();
# },
# [
# (PyUserWarning, "This is a warning message"),
# (
# PyFutureWarning,
# "This function is warning with FutureWarning"
# )
# ]
# );
# });
```

When the functions are called, warnings will be displayed:

```python
import warnings
from raising_warning_fn import function_with_warning, function_with_warning_and_custom_category

function_with_warning()
function_with_warning_and_custom_category()
```

The output will be:

```plaintext
UserWarning: This is a warning message
FutureWarning: This function is warning with FutureWarning
```

- <a id="deprecated"></a> `#[pyo3(deprecated = "...")]`

Set this option to display deprecation warning when the function is called in Python.
This is equivalent to [`#[pyo3(warn(message = "...", category = PyDeprecationWarning))]`](#warn) or [`warnings.warn(message, DeprecationWarning)`](https://docs.python.org/3.12/library/warnings.html#warnings.warn).
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to guarantee that these are equivalent? Especially with Pep 702 support, which we might only be able to properly provide for #[pyo3(deprecated = "...")]. Maybe we should formulate this a bit more loosely to give us some wiggle room in a future where these might need to diverge a bit? cc @davidhewitt

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I agree this should be changed. I wrote this before reading the PEP 702. The semantics of deprecated in the PEP doesn't seem to match what I'm promising here. Using the same name thus could cause future confusion.


> Note: this attribute does not deprecate the rust function but only raises DeprecationWarning when the function is called from Python. To deprecate the rust function, please add `#[deprecated]` attribute to the function.

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


## Per-argument options

Expand Down
3 changes: 3 additions & 0 deletions newsfragments/4364.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added `#[pyo3(warn(message = "...", category = ...))]` attribute for automatic warnings generation for `#[pyfunction]` and `#[pymethods]`.

Added `#[pyo3(deprecated = "...")]` attribute for automatic deprecation warnings generation for `#[pyfunction]` and `#[pymethods]`.
4 changes: 4 additions & 0 deletions pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub mod kw {
syn::custom_keyword!(transparent);
syn::custom_keyword!(unsendable);
syn::custom_keyword!(weakref);
syn::custom_keyword!(warn);
syn::custom_keyword!(message);
syn::custom_keyword!(category);
syn::custom_keyword!(deprecated);
}

fn take_int(read: &mut &str, tracker: &mut usize) -> String {
Expand Down
10 changes: 10 additions & 0 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::{ext::IdentExt, spanned::Spanned, Ident, Result};

use crate::deprecations::deprecate_trailing_option_default;
use crate::pyfunction::{PyFunctionWarning, WarningFactory};
use crate::utils::{Ctx, LitCStr};
use crate::{
attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue},
Expand Down Expand Up @@ -410,6 +411,7 @@ pub struct FnSpec<'a> {
pub text_signature: Option<TextSignatureAttribute>,
pub asyncness: Option<syn::Token![async]>,
pub unsafety: Option<syn::Token![unsafe]>,
pub warnings: Vec<PyFunctionWarning>,
}

pub fn parse_method_receiver(arg: &syn::FnArg) -> Result<SelfType> {
Expand Down Expand Up @@ -446,6 +448,7 @@ impl<'a> FnSpec<'a> {
text_signature,
name,
signature,
warnings,
..
} = options;

Expand Down Expand Up @@ -489,6 +492,7 @@ impl<'a> FnSpec<'a> {
text_signature,
asyncness: sig.asyncness,
unsafety: sig.unsafety,
warnings,
})
}

Expand Down Expand Up @@ -744,6 +748,8 @@ impl<'a> FnSpec<'a> {

let deprecation = deprecate_trailing_option_default(self);

let deprecated_warning = self.warnings.build_py_warning(ctx);

Ok(match self.convention {
CallingConvention::Noargs => {
let mut holders = Holders::new();
Expand All @@ -768,6 +774,7 @@ impl<'a> FnSpec<'a> {
let _slf_ref = &_slf;
let function = #rust_name; // Shadow the function name to avoid #3017
#init_holders
#deprecated_warning
let result = #call;
result
}
Expand All @@ -792,6 +799,7 @@ impl<'a> FnSpec<'a> {
let function = #rust_name; // Shadow the function name to avoid #3017
#arg_convert
#init_holders
#deprecated_warning
let result = #call;
result
}
Expand All @@ -815,6 +823,7 @@ impl<'a> FnSpec<'a> {
let function = #rust_name; // Shadow the function name to avoid #3017
#arg_convert
#init_holders
#deprecated_warning
let result = #call;
result
}
Expand All @@ -841,6 +850,7 @@ impl<'a> FnSpec<'a> {
let function = #rust_name; // Shadow the function name to avoid #3017
#arg_convert
#init_holders
#deprecated_warning
let result = #call;
let initializer: #pyo3_path::PyClassInitializer::<#cls> = result.convert(py)?;
#pyo3_path::impl_::pymethods::tp_new_impl(py, initializer, _slf)
Expand Down
3 changes: 3 additions & 0 deletions pyo3-macros-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,7 @@ fn complex_enum_struct_variant_new<'a>(
text_signature: None,
asyncness: None,
unsafety: None,
warnings: vec![],
};

crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx)
Expand Down Expand Up @@ -1676,6 +1677,7 @@ fn complex_enum_tuple_variant_new<'a>(
text_signature: None,
asyncness: None,
unsafety: None,
warnings: vec![],
};

crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx)
Expand All @@ -1700,6 +1702,7 @@ fn complex_enum_variant_field_getter<'a>(
text_signature: None,
asyncness: None,
unsafety: None,
warnings: vec![],
};

let property_type = crate::pymethod::PropertyType::Function {
Expand Down
Loading
Loading