Skip to content

Commit

Permalink
Implement #[pyo3(warn)] and #[pyo3(deprecated)] attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
FlickerSoul committed Jul 30, 2024
1 parent e0bd22e commit 1408a37
Show file tree
Hide file tree
Showing 25 changed files with 1,233 additions and 7 deletions.
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 {
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_bound(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).

> 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 @@ -741,6 +745,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 @@ -765,6 +771,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 @@ -789,6 +796,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 @@ -812,6 +820,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 @@ -838,6 +847,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 @@ -1592,6 +1592,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 @@ -1646,6 +1647,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 @@ -1670,6 +1672,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

0 comments on commit 1408a37

Please sign in to comment.