diff --git a/guide/src/class.md b/guide/src/class.md index 216838f779d..61a7a49160b 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -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: diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index c5ad847834f..6ba5456649a 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -424,6 +424,8 @@ cleared, as every cycle must contain at least one mutable reference. - `__traverse__(, pyo3::class::gc::PyVisit<'_>) -> Result<(), pyo3::class::gc::PyTraverseError>` - `__clear__() -> ()` +> Note: `__traverse__` does not work with [`#[pyo3(warn(...))]`](../function.md#warn) nor [`#[pyo3(deprecated)]`](../function.md#deprecated) attribute. + Example: ```rust diff --git a/guide/src/function.md b/guide/src/function.md index fd215a1550e..4858c2f9fdc 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -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) @@ -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)?) } ``` + - `#[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(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 + ``` + + - `#[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 diff --git a/newsfragments/4364.added.md b/newsfragments/4364.added.md new file mode 100644 index 00000000000..ef39fa41155 --- /dev/null +++ b/newsfragments/4364.added.md @@ -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]`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 94526e7dafc..8add1ac0569 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -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 { diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index c850f67b2b9..e213d126945 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -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}, @@ -410,6 +411,7 @@ pub struct FnSpec<'a> { pub text_signature: Option, pub asyncness: Option, pub unsafety: Option, + pub warnings: Vec, } pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { @@ -446,6 +448,7 @@ impl<'a> FnSpec<'a> { text_signature, name, signature, + warnings, .. } = options; @@ -489,6 +492,7 @@ impl<'a> FnSpec<'a> { text_signature, asyncness: sig.asyncness, unsafety: sig.unsafety, + warnings, }) } @@ -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(); @@ -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 } @@ -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 } @@ -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 } @@ -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) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 50c123a90fb..57341f7bccd 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -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) @@ -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) @@ -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 { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 3059025caf7..7d637ebc91b 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -1,4 +1,5 @@ -use crate::utils::Ctx; +use crate::attributes::KeywordAttribute; +use crate::utils::{Ctx, LitCStr}; use crate::{ attributes::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, @@ -7,9 +8,11 @@ use crate::{ method::{self, CallingConvention, FnArg}, pymethod::check_generic, }; -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use syn::{ext::IdentExt, spanned::Spanned, Result}; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use std::cmp::PartialEq; +use std::ffi::CString; +use syn::{ext::IdentExt, spanned::Spanned, LitStr, Path, Result, Token}; use syn::{ parse::{Parse, ParseStream}, token::Comma, @@ -83,6 +86,160 @@ impl PyFunctionArgPyO3Attributes { } } +type PyFunctionWarningMessageAttribute = KeywordAttribute; +type PyFunctionWarningCategoryAttribute = KeywordAttribute; +type PyFunctionDeprecatedWarningAttribute = KeywordAttribute; + +pub struct PyFunctionWarningAttribute { + pub message: PyFunctionWarningMessageAttribute, + pub category: Option, + pub span: Span, +} + +#[derive(PartialEq)] +pub enum PyFunctionWarningCategory { + Path(Path), + UserWarning, + DeprecationWarning, +} + +pub struct PyFunctionWarning { + pub message: LitStr, + pub category: PyFunctionWarningCategory, + pub span: Span, +} + +impl From for PyFunctionWarning { + fn from(value: PyFunctionWarningAttribute) -> Self { + Self { + message: value.message.value, + category: value + .category + .map_or(PyFunctionWarningCategory::UserWarning, |cat| { + PyFunctionWarningCategory::Path(cat.value) + }), + span: value.span, + } + } +} + +impl From for PyFunctionWarning { + fn from(value: PyFunctionDeprecatedWarningAttribute) -> Self { + Self { + span: value.span(), + message: value.value, + category: PyFunctionWarningCategory::DeprecationWarning, + } + } +} + +pub trait WarningFactory { + fn build_py_warning(&self, ctx: &Ctx) -> TokenStream; + fn span(&self) -> Span; +} + +impl WarningFactory for PyFunctionWarning { + fn build_py_warning(&self, ctx: &Ctx) -> TokenStream { + let message = &self.message.value(); + let c_message = LitCStr::new( + CString::new(message.clone()).unwrap(), + Spanned::span(&message), + ctx, + ); + let pyo3_path = &ctx.pyo3_path; + let category = match &self.category { + PyFunctionWarningCategory::Path(path) => quote! {#path}, + PyFunctionWarningCategory::UserWarning => { + quote! {#pyo3_path::exceptions::PyUserWarning} + } + PyFunctionWarningCategory::DeprecationWarning => { + quote! {#pyo3_path::exceptions::PyDeprecationWarning} + } + }; + quote! { + #pyo3_path::PyErr::warn_with_cstr_bound(py, &<#category as #pyo3_path::PyTypeInfo>::type_object_bound(py), #c_message, 1)?; + } + } + + fn span(&self) -> Span { + self.span + } +} + +impl WarningFactory for Vec { + fn build_py_warning(&self, ctx: &Ctx) -> TokenStream { + let warnings = self.iter().map(|warning| warning.build_py_warning(ctx)); + + quote! { + #(#warnings)* + } + } + + fn span(&self) -> Span { + self.iter() + .map(|val| val.span()) + .reduce(|acc, span| acc.join(span).unwrap_or(acc)) + .unwrap() + } +} + +impl Parse for PyFunctionWarningAttribute { + fn parse(input: ParseStream<'_>) -> Result { + let mut message: Option = None; + let mut category: Option = None; + + let span = input.parse::()?.span(); + + let content; + syn::parenthesized!(content in input); + + while !content.is_empty() { + let lookahead = content.lookahead1(); + + if lookahead.peek(attributes::kw::message) { + message = content + .parse::() + .map(Some)?; + } else if lookahead.peek(attributes::kw::category) { + category = content + .parse::() + .map(Some)?; + } else { + return Err(lookahead.error()); + } + + if content.peek(Token![,]) { + content.parse::()?; + } + } + + Ok(PyFunctionWarningAttribute { + message: message.ok_or(syn::Error::new( + content.span(), + "missing `message` in `warn` attribute", + ))?, + category, + span, + }) + } +} + +impl ToTokens for PyFunctionWarningAttribute { + fn to_tokens(&self, tokens: &mut TokenStream) { + let message_tokens = self.message.to_token_stream(); + let category_tokens = self + .category + .as_ref() + .map_or(quote! {}, |cat| cat.to_token_stream()); + + let token_stream = quote! { + warn(#message_tokens, #category_tokens) + }; + + tokens.extend(token_stream); + } +} + #[derive(Default)] pub struct PyFunctionOptions { pub pass_module: Option, @@ -90,6 +247,7 @@ pub struct PyFunctionOptions { pub signature: Option, pub text_signature: Option, pub krate: Option, + pub warnings: Vec, } impl Parse for PyFunctionOptions { @@ -125,6 +283,8 @@ pub enum PyFunctionOption { Signature(SignatureAttribute), TextSignature(TextSignatureAttribute), Crate(CrateAttribute), + Warning(PyFunctionWarningAttribute), + Deprecated(PyFunctionDeprecatedWarningAttribute), } impl Parse for PyFunctionOption { @@ -140,6 +300,10 @@ impl Parse for PyFunctionOption { input.parse().map(PyFunctionOption::TextSignature) } else if lookahead.peek(syn::Token![crate]) { input.parse().map(PyFunctionOption::Crate) + } else if lookahead.peek(attributes::kw::warn) { + input.parse().map(PyFunctionOption::Warning) + } else if lookahead.peek(attributes::kw::deprecated) { + input.parse().map(PyFunctionOption::Deprecated) } else { Err(lookahead.error()) } @@ -175,6 +339,22 @@ impl PyFunctionOptions { PyFunctionOption::Signature(signature) => set_option!(signature), PyFunctionOption::TextSignature(text_signature) => set_option!(text_signature), PyFunctionOption::Crate(krate) => set_option!(krate), + PyFunctionOption::Warning(warning) => { + self.warnings.push(warning.into()); + } + PyFunctionOption::Deprecated(deprecated) => { + if self + .warnings + .iter() + .filter(|w| w.category == PyFunctionWarningCategory::DeprecationWarning) + .count() + > 0 + { + bail_spanned!(deprecated.span() => "only one `deprecated` warning may be specified") + } + + self.warnings.push(deprecated.into()); + } } } Ok(()) @@ -202,6 +382,7 @@ pub fn impl_wrap_pyfunction( signature, text_signature, krate, + warnings, } = options; let ctx = &Ctx::new(&krate, Some(&func.sig)); @@ -251,6 +432,7 @@ pub fn impl_wrap_pyfunction( text_signature, asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, + warnings, }; let vis = &func.vis; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 78d7dac2330..ba1d8271f65 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -5,6 +5,7 @@ use crate::attributes::{NameAttribute, RenamingRule}; use crate::deprecations::deprecate_trailing_option_default; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{impl_regular_arg_param, Holders}; +use crate::pyfunction::WarningFactory; use crate::utils::PythonDoc; use crate::utils::{Ctx, LitCStr}; use crate::{ @@ -454,6 +455,11 @@ fn impl_traverse_slot( } } + ensure_spanned!( + spec.warnings.is_empty(), + spec.warnings.span() => "__traverse__ cannot be used with #[pyo3(warn)] nor #[pyo3(deprecated)]" + ); + let rust_fn_ident = spec.name; let associated_method = quote! { @@ -489,6 +495,12 @@ fn impl_py_class_attribute( args[0].ty().span() => "#[classattr] can only have one argument (of type pyo3::Python)" ); + ensure_spanned!( + spec.warnings.is_empty(), + spec.warnings.span() + => "#[classattr] cannot be used with #[pyo3(warn)] nor #[pyo3(deprecated)]" + ); + let name = &spec.name; let fncall = if py_arg.is_some() { quote!(function(py)) @@ -668,6 +680,12 @@ pub fn impl_py_setter_def( } } + let deprecated_warning = if let PropertyType::Function { spec, .. } = &property_type { + spec.warnings.build_py_warning(ctx) + } else { + quote!() + }; + let init_holders = holders.init_holders(ctx); let associated_method = quote! { #cfg_attrs @@ -683,6 +701,7 @@ pub fn impl_py_setter_def( })?; #init_holders #extract + #deprecated_warning let result = #setter_impl; #pyo3_path::callback::convert(py, result) } @@ -811,6 +830,8 @@ pub fn impl_py_getter_def( }; let init_holders = holders.init_holders(ctx); + let deprecated_warning = spec.warnings.build_py_warning(ctx); + let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( @@ -818,6 +839,7 @@ pub fn impl_py_getter_def( _slf: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { #init_holders + #deprecated_warning let result = #body; result } @@ -1344,13 +1366,19 @@ fn generate_method_body( let rust_name = spec.name; let args = extract_proto_arguments(spec, arguments, extract_error_mode, holders, ctx)?; let call = quote! { #cls::#rust_name(#self_arg #(#args),*) }; - Ok(if let Some(return_mode) = return_mode { + let body = if let Some(return_mode) = return_mode { return_mode.return_call_output(call, ctx) } else { quote! { let result = #call; #pyo3_path::callback::convert(py, result) } + }; + let deprecated_warning = spec.warnings.build_py_warning(ctx); + + Ok(quote! { + #deprecated_warning + #body }) } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index c4263a512d3..7703fbab06c 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -130,6 +130,8 @@ pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream { /// | `#[pyo3(name = "...")]` | Defines the name of the function in Python. | /// | `#[pyo3(text_signature = "...")]` | Defines the `__text_signature__` attribute of the function in Python. | /// | `#[pyo3(pass_module)]` | Passes the module containing the function as a `&PyModule` first argument to the function. | +/// | `#[pyo3(warn(message = "...", category = ...))]` | Generate warning given a message and a category | +/// | `#[pyo3(deprecated = "...")]` | Generate a deprecation warning given a message | /// /// For more on exposing functions see the [function section of the guide][1]. /// diff --git a/src/err/mod.rs b/src/err/mod.rs index b51b9defc38..2a607a5e3a2 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -10,7 +10,7 @@ use crate::{ use crate::{Borrowed, IntoPy, Py, PyAny, PyObject, Python, ToPyObject}; use std::borrow::Cow; use std::cell::UnsafeCell; -use std::ffi::CString; +use std::ffi::{CStr, CString}; mod err_state; mod impls; @@ -618,6 +618,39 @@ impl PyErr { }) } + /// Issues a warning message with CStr + /// + /// This is a variant of `warn_bound` that accepts a `CStr` message instead of a `&str` one. + /// + /// See [PyErr::warn_bound](crate::PyErr::warn_bound) for more information. + /// + /// Example: + /// ```rust + /// # use pyo3::prelude::*; + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let user_warning = py.get_type_bound::(); + /// PyErr::warn_with_cstr_bound(py, &user_warning, c"I am warning you", 0)?; + /// Ok(()) + /// }) + /// # } + /// ``` + #[doc(hidden)] + pub fn warn_with_cstr_bound<'py>( + py: Python<'py>, + category: &Bound<'py, PyAny>, + message: &CStr, + stacklevel: i32, + ) -> PyResult<()> { + error_on_minusone(py, unsafe { + ffi::PyErr_WarnEx( + category.as_ptr(), + message.as_ptr(), + stacklevel as ffi::Py_ssize_t, + ) + }) + } + /// Issues a warning message, with more control over the warning attributes. /// /// May return a `PyErr` if warnings-as-errors is enabled. @@ -1120,6 +1153,8 @@ mod tests { #[test] fn warnings() { + use std::ffi::CString; + use crate::types::any::PyAnyMethods; // Note: although the warning filter is interpreter global, keeping the // GIL locked should prevent effects to be visible to other testing @@ -1137,12 +1172,33 @@ mod tests { { PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap() }, [(exceptions::PyUserWarning, "I am warning you")] ); + // Test the same with a CStr + assert_warnings!( + py, + { + PyErr::warn_with_cstr_bound( + py, + &cls, + CString::new("I am warning you").unwrap().as_ref(), + 0, + ) + .unwrap() + }, + [(exceptions::PyUserWarning, "I am warning you")] + ); - // Test with raising + // Test with raising on both &str and &CStr warnings .call_method1("simplefilter", ("error", &cls)) .unwrap(); PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap_err(); + PyErr::warn_with_cstr_bound( + py, + &cls, + CString::new("I am warning you").unwrap().as_ref(), + 0, + ) + .unwrap_err(); // Test with error for an explicit module warnings.call_method0("resetwarnings").unwrap(); @@ -1156,6 +1212,19 @@ mod tests { { PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap() }, [(exceptions::PyUserWarning, "I am warning you")] ); + assert_warnings!( + py, + { + PyErr::warn_with_cstr_bound( + py, + &cls, + CString::new("I am warning you").unwrap().as_ref(), + 0, + ) + .unwrap() + }, + [(exceptions::PyUserWarning, "I am warning you")] + ); let err = PyErr::warn_explicit_bound( py, diff --git a/src/tests/common.rs b/src/tests/common.rs index c56249c2796..d4b0c9ba1b1 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -62,6 +62,57 @@ mod inner { }}; } + #[macro_export] + macro_rules! py_expect_warning { + ($py:expr, $($val:ident)+, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{ + use pyo3::types::IntoPyDict; + let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py); + py_expect_warning!($py, *d, $code, [$(($warning_msg, $warning_category)),+]) + }}; + ($py:expr, *$dict:expr, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{ + let code_lines: Vec<&str> = $code.lines().collect(); + let indented_code: String = code_lines.iter() + .map(|line| format!(" {}", line)) // add 4 spaces indentation + .collect::>() + .join("\n"); + + let wrapped_code = format!(r#" +import warnings +with warnings.catch_warnings(record=True) as warning_record: +{} +"#, indented_code); + + $py.run_bound(wrapped_code.as_str(), None, Some(&$dict.as_borrowed())).expect("Failed to run warning testing code"); + let expected_warnings = [$(($warning_msg, <$warning_category as pyo3::PyTypeInfo>::type_object_bound($py))),+]; + let warning_record: Bound<'_, pyo3::types::PyList> = $dict.get_item("warning_record").expect("Failed to capture warnings").expect("Failed to downcast to PyList").extract().unwrap(); + + assert_eq!(warning_record.len(), expected_warnings.len(), "Expecting {} warnings but got {}", expected_warnings.len(), warning_record.len()); + + for ((index, warning), (msg, category)) in warning_record.iter().enumerate().zip(expected_warnings.iter()) { + let actual_msg = warning.getattr("message").unwrap().str().unwrap().to_string_lossy().to_string(); + let actual_category = warning.getattr("category").unwrap(); + + assert_eq!(actual_msg, msg.to_string(), "Warning message mismatch at index {}, expecting `{}` but got `{}`", index, msg, actual_msg); + assert!(actual_category.is(category), "Warning category mismatch at index {}, expecting {:?} but got {:?}", index, category, actual_category); + } + }}; + } + + #[macro_export] + macro_rules! py_expect_warning_for_fn { + ($fn:ident, $($val:ident)+, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => { + pyo3::Python::with_gil(|py| { + let f = wrap_pyfunction!($fn)(py).unwrap(); + py_expect_warning!( + py, + f, + "f()", + [$(($warning_msg, $warning_category)),+] + ); + }); + }; + } + // sys.unraisablehook not available until Python 3.8 #[cfg(all(feature = "macros", Py_3_8))] #[pyclass(crate = "pyo3")] diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs index 8dcdc369c47..f3c4476a0bc 100644 --- a/src/tests/hygiene/pyfunction.rs +++ b/src/tests/hygiene/pyfunction.rs @@ -4,6 +4,29 @@ fn do_something(x: i32) -> crate::PyResult { ::std::result::Result::Ok(x) } +#[crate::pyfunction] +#[pyo3(crate = "crate")] +#[pyo3(warn(message = "This is a warning message"))] +fn function_with_warning() {} + +#[crate::pyfunction(crate = "crate")] +#[pyo3(warn(message = "This is a warning message with custom category", category = crate::exceptions::PyFutureWarning))] +fn function_with_warning_and_category() {} + +#[crate::pyfunction(crate = "crate")] +#[pyo3(deprecated = "This function is deprecated")] +fn deprecated_function() {} + +#[crate::pyfunction(crate = "crate")] +#[pyo3(warn(message = "This is a warning message"))] +#[pyo3(warn(message = "This is another warning message", category = crate::exceptions::PyFutureWarning))] +fn multiple_warning_function() {} + +#[crate::pyfunction(crate = "crate")] +#[pyo3(warn(message = "This is a warning message"))] +#[pyo3(deprecated = "This function is deprecated")] +fn deprecated_and_warning_function() {} + #[test] fn invoke_wrap_pyfunction() { crate::Python::with_gil(|py| { diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index 6a1a2a50d13..2a9d933347f 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -419,3 +419,115 @@ struct Dummy2; #[crate::pymethods(crate = "crate")] impl Dummy2 {} + +#[crate::pyclass(crate = "crate")] +struct WarningDummy { + value: i32, +} + +#[cfg(not(Py_LIMITED_API))] +#[crate::pyclass(crate = "crate", extends=crate::exceptions::PyWarning)] +pub struct UserDefinedWarning {} + +#[cfg(not(Py_LIMITED_API))] +#[crate::pymethods(crate = "crate")] +impl UserDefinedWarning { + #[new] + #[pyo3(signature = (*_args, **_kwargs))] + fn new( + _args: crate::Bound<'_, crate::PyAny>, + _kwargs: ::std::option::Option>, + ) -> Self { + Self {} + } +} + +#[crate::pymethods(crate = "crate")] +impl WarningDummy { + #[new] + #[pyo3(warn(message = "this __new__ method raises warning"))] + fn new() -> Self { + Self { value: 0 } + } + + #[pyo3(warn(message = "this method raises warning"))] + fn method_with_warning(_slf: crate::PyRef<'_, Self>) {} + + #[pyo3(warn(message = "this method raises warning", category = crate::exceptions::PyFutureWarning))] + fn method_with_warning_and_custom_category(_slf: crate::PyRef<'_, Self>) {} + + #[cfg(not(Py_LIMITED_API))] + #[pyo3(warn(message = "this method raises user-defined warning", category = UserDefinedWarning))] + fn method_with_warning_and_user_defined_category(&self) {} + + #[staticmethod] + #[pyo3(warn(message = "this static method raises warning"))] + fn static_method() {} + + #[staticmethod] + #[pyo3(warn(message = "this class method raises warning"))] + fn class_method() {} + + #[getter] + #[pyo3(warn(message = "this getter raises warning"))] + fn get_value(&self) -> i32 { + self.value + } + + #[setter] + #[pyo3(warn(message = "this setter raises warning"))] + fn set_value(&mut self, value: i32) { + self.value = value; + } + + #[pyo3(warn(message = "this subscript op method raises warning"))] + fn __getitem__(&self, _key: i32) -> i32 { + 0 + } + + #[pyo3(warn(message = "the + op method raises warning"))] + fn __add__(&self, other: crate::PyRef<'_, Self>) -> Self { + Self { + value: self.value + other.value, + } + } + + #[pyo3(warn(message = "this __call__ method raises warning"))] + fn __call__(&self) -> i32 { + self.value + } + + #[pyo3(warn(message = "this method raises warning 1"))] + #[pyo3(warn(message = "this method raises warning 2", category = crate::exceptions::PyFutureWarning))] + fn multiple_warn_method(&self) {} + + #[pyo3(warn(message = "this method raises warning 1"))] + #[pyo3(deprecated = "this method is deprecated")] + fn multiple_warn_deprecated_method(&self) {} +} + +#[crate::pyclass(crate = "crate")] +struct WarningDummy2; + +#[crate::pymethods(crate = "crate")] +impl WarningDummy2 { + #[new] + #[classmethod] + #[pyo3(warn(message = "this class-method __new__ method raises warning"))] + fn new(_cls: crate::Bound<'_, crate::types::PyType>) -> Self { + Self {} + } + + #[pyo3(warn(message = "this class-method raises warning 1"))] + #[pyo3(warn(message = "this class-method raises warning 2"))] + fn multiple_default_warnings_fn(&self) {} + + #[pyo3(warn(message = "this class-method raises warning"))] + #[pyo3(warn(message = "this class-method raises future warning", category = crate::exceptions::PyFutureWarning))] + fn multiple_warnings_fn(&self) {} + + #[cfg(not(Py_LIMITED_API))] + #[pyo3(warn(message = "this class-method raises future warning", category = crate::exceptions::PyFutureWarning))] + #[pyo3(warn(message = "this class-method raises user-defined warning", category = UserDefinedWarning))] + fn multiple_warnings_fn_with_custom_category(&self) {} +} diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index b1fcdc09fb7..19a6f9a6e27 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -62,4 +62,8 @@ fn test_compile_errors() { #[cfg(all(Py_LIMITED_API, not(Py_3_9)))] t.compile_fail("tests/ui/abi3_dict.rs"); t.compile_fail("tests/ui/duplicate_pymodule_submodule.rs"); + t.compile_fail("tests/ui/invalid_pyfunction_warn.rs"); + t.compile_fail("tests/ui/invalid_pymethods_warn.rs"); + t.compile_fail("tests/ui/invalid_pyfunction_deprecated.rs"); + t.compile_fail("tests/ui/invalid_pymethods_deprecated.rs"); } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 159cc210133..a77dc1ad311 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -1,9 +1,13 @@ #![cfg(feature = "macros")] +#[cfg(not(Py_LIMITED_API))] +use pyo3::exceptions::PyWarning; +use pyo3::exceptions::{PyDeprecationWarning, PyFutureWarning, PyUserWarning}; use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::PySequence; use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; +use pyo3_macros::pyclass; #[path = "../src/tests/common.rs"] mod common; @@ -1161,3 +1165,309 @@ fn test_issue_2988() { ) { } } + +#[cfg(not(Py_LIMITED_API))] +#[pyclass(extends=PyWarning)] +pub struct UserDefinedWarning {} + +#[cfg(not(Py_LIMITED_API))] +#[pymethods] +impl UserDefinedWarning { + #[new] + #[pyo3(signature = (*_args, **_kwargs))] + fn new(_args: Bound<'_, PyAny>, _kwargs: Option>) -> Self { + Self {} + } +} + +#[test] +fn test_pymethods_warn() { + // We do not test #[classattr] nor __traverse__ + // because it doesn't make sense to implement deprecated methods for them. + + #[pyclass] + struct WarningMethodContainer { + value: i32, + } + + #[pymethods] + impl WarningMethodContainer { + #[new] + #[pyo3(warn(message = "this __new__ method raises warning"))] + fn new() -> Self { + Self { value: 0 } + } + + #[pyo3(warn(message = "this method raises warning"))] + fn method_with_warning(_slf: PyRef<'_, Self>) {} + + #[pyo3(warn(message = "this method raises warning", category = PyFutureWarning))] + fn method_with_warning_and_custom_category(_slf: PyRef<'_, Self>) {} + + #[cfg(not(Py_LIMITED_API))] + #[pyo3(warn(message = "this method raises user-defined warning", category = UserDefinedWarning))] + fn method_with_warning_and_user_defined_category(&self) {} + + #[staticmethod] + #[pyo3(warn(message = "this static method raises warning"))] + fn static_method() {} + + #[staticmethod] + #[pyo3(warn(message = "this class method raises warning"))] + fn class_method() {} + + #[getter] + #[pyo3(warn(message = "this getter raises warning"))] + fn get_value(&self) -> i32 { + self.value + } + + #[setter] + #[pyo3(warn(message = "this setter raises warning"))] + fn set_value(&mut self, value: i32) { + self.value = value; + } + + #[pyo3(warn(message = "this subscript op method raises warning"))] + fn __getitem__(&self, _key: i32) -> i32 { + 0 + } + + #[pyo3(warn(message = "the + op method raises warning"))] + fn __add__(&self, other: PyRef<'_, Self>) -> Self { + Self { + value: self.value + other.value, + } + } + + #[pyo3(warn(message = "this __call__ method raises warning"))] + fn __call__(&self) -> i32 { + self.value + } + } + + Python::with_gil(|py| { + let typeobj = py.get_type_bound::(); + let obj = typeobj.call0().unwrap(); + + // FnType::Fn + py_expect_warning!( + py, + obj, + "obj.method_with_warning()", + [("this method raises warning", PyUserWarning)], + ); + + // FnType::Fn + py_expect_warning!( + py, + obj, + "obj.method_with_warning_and_custom_category()", + [("this method raises warning", PyFutureWarning)] + ); + + // FnType::Fn, user-defined warning + #[cfg(not(Py_LIMITED_API))] + py_expect_warning!( + py, + obj, + "obj.method_with_warning_and_user_defined_category()", + [( + "this method raises user-defined warning", + UserDefinedWarning + )] + ); + + // #[staticmethod], FnType::FnStatic + py_expect_warning!( + py, + typeobj, + "typeobj.static_method()", + [("this static method raises warning", PyUserWarning)] + ); + + // #[classmethod], FnType::FnClass + py_expect_warning!( + py, + typeobj, + "typeobj.class_method()", + [("this class method raises warning", PyUserWarning)] + ); + + // #[classmethod], FnType::FnClass + py_expect_warning!( + py, + obj, + "obj.class_method()", + [("this class method raises warning", PyUserWarning)] + ); + + // #[new], FnType::FnNew + py_expect_warning!( + py, + typeobj, + "typeobj()", + [("this __new__ method raises warning", PyUserWarning)] + ); + + // #[getter], FnType::Getter + py_expect_warning!( + py, + obj, + "val = obj.value", + [("this getter raises warning", PyUserWarning)] + ); + + // #[setter], FnType::Setter + py_expect_warning!( + py, + obj, + "obj.value = 10", + [("this setter raises warning", PyUserWarning)] + ); + + // PyMethodProtoKind::Slot + py_expect_warning!( + py, + obj, + "obj[0]", + [("this subscript op method raises warning", PyUserWarning)] + ); + + // PyMethodProtoKind::SlotFragment + py_expect_warning!( + py, + obj, + "obj + obj", + [("the + op method raises warning", PyUserWarning)] + ); + + // PyMethodProtoKind::Call + py_expect_warning!( + py, + obj, + "obj()", + [("this __call__ method raises warning", PyUserWarning)] + ); + }); + + #[pyclass] + struct WarningMethodContainer2 {} + + #[pymethods] + impl WarningMethodContainer2 { + #[new] + #[classmethod] + #[pyo3(warn(message = "this class-method __new__ method raises warning"))] + fn new(_cls: Bound<'_, PyType>) -> Self { + Self {} + } + } + + Python::with_gil(|py| { + let typeobj = py.get_type_bound::(); + + // #[new], #[classmethod], FnType::FnNewClass + py_expect_warning!( + py, + typeobj, + "typeobj()", + [( + "this class-method __new__ method raises warning", + PyUserWarning + )] + ); + }); +} + +#[test] +fn test_pymethods_deprecated() { + #[pyclass] + struct DeprecatedMethodContainer {} + + #[pymethods] + impl DeprecatedMethodContainer { + #[new] + fn new() -> Self { + Self {} + } + + #[pyo3(deprecated = "this method is deprecated")] + fn deprecated_method(_slf: PyRef<'_, Self>) {} + } + + Python::with_gil(|py| { + let typeobj = py.get_type_bound::(); + let obj = typeobj.call0().unwrap(); + + py_expect_warning!( + py, + obj, + "obj.deprecated_method()", + [("this method is deprecated", PyDeprecationWarning)] + ); + }); +} + +#[test] +fn test_py_methods_multiple_warn() { + #[pyclass] + struct MultipleWarnContainer {} + + #[pymethods] + impl MultipleWarnContainer { + #[new] + fn new() -> Self { + Self {} + } + + #[pyo3(warn(message = "this method raises warning 1"))] + #[pyo3(warn(message = "this method raises warning 2", category = pyo3::exceptions::PyFutureWarning))] + fn multiple_warn_method(&self) {} + + #[pyo3(warn(message = "this method raises warning 1"))] + #[pyo3(deprecated = "this method is deprecated")] + fn multiple_warn_deprecated_method(&self) {} + + #[cfg(not(Py_LIMITED_API))] + #[pyo3(warn(message = "this method raises FutureWarning", category = pyo3::exceptions::PyFutureWarning))] + #[pyo3(warn(message = "this method raises UserDefinedWarning", category = UserDefinedWarning))] + fn multiple_warn_custom_category_method(&self) {} + } + + Python::with_gil(|py| { + let typeobj = py.get_type_bound::(); + let obj = typeobj.call0().unwrap(); + + py_expect_warning!( + py, + obj, + "obj.multiple_warn_method()", + [ + ("this method raises warning 1", PyUserWarning), + ("this method raises warning 2", PyFutureWarning) + ] + ); + + py_expect_warning!( + py, + obj, + "obj.multiple_warn_deprecated_method()", + [ + ("this method raises warning 1", PyUserWarning), + ("this method is deprecated", PyDeprecationWarning) + ] + ); + + #[cfg(not(Py_LIMITED_API))] + py_expect_warning!( + py, + obj, + "obj.multiple_warn_custom_category_method()", + [ + ("this method raises FutureWarning", PyFutureWarning), + ("this method raises UserDefinedWarning", UserDefinedWarning) + ] + ); + }); +} diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 903db689527..ad1db7900d6 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -4,6 +4,9 @@ use std::collections::HashMap; #[cfg(not(Py_LIMITED_API))] use pyo3::buffer::PyBuffer; +#[cfg(not(Py_LIMITED_API))] +use pyo3::exceptions::PyWarning; +use pyo3::exceptions::{PyDeprecationWarning, PyFutureWarning, PyUserWarning}; use pyo3::ffi::c_str; use pyo3::prelude::*; #[cfg(not(Py_LIMITED_API))] @@ -11,6 +14,7 @@ use pyo3::types::PyDateTime; #[cfg(not(any(Py_LIMITED_API, PyPy)))] use pyo3::types::PyFunction; use pyo3::types::{self, PyCFunction}; +use pyo3_macros::pyclass; #[path = "../src/tests/common.rs"] mod common; @@ -603,3 +607,134 @@ fn test_pyfunction_raw_ident() { py_assert!(py, m, "m.enum()"); }) } + +#[cfg(not(Py_LIMITED_API))] +#[pyclass(extends=PyWarning)] +pub struct UserDefinedWarning {} + +#[cfg(not(Py_LIMITED_API))] +#[pymethods] +impl UserDefinedWarning { + #[new] + #[pyo3(signature = (*_args, **_kwargs))] + fn new(_args: Bound<'_, PyAny>, _kwargs: Option>) -> Self { + Self {} + } +} + +#[test] +fn test_pyfunction_warn() { + #[pyfunction] + #[pyo3(warn(message = "this function raises warning"))] + fn function_with_warning() {} + + py_expect_warning_for_fn!( + function_with_warning, + f, + [("this function raises warning", PyUserWarning)] + ); + + #[pyfunction] + #[pyo3(warn(message = "this function raises warning with category", category = PyFutureWarning))] + fn function_with_warning_with_category() {} + + py_expect_warning_for_fn!( + function_with_warning_with_category, + f, + [( + "this function raises warning with category", + PyFutureWarning + )] + ); + + #[pyfunction] + #[pyo3(warn(message = "custom deprecated category", category = pyo3::exceptions::PyDeprecationWarning))] + fn function_with_warning_with_custom_category() {} + + py_expect_warning_for_fn!( + function_with_warning_with_custom_category, + f, + [( + "custom deprecated category", + pyo3::exceptions::PyDeprecationWarning + )] + ); + + #[cfg(not(Py_LIMITED_API))] + #[pyfunction] + #[pyo3(warn(message = "this function raises user-defined warning", category = UserDefinedWarning))] + fn function_with_warning_and_user_defined_category() {} + + #[cfg(not(Py_LIMITED_API))] + py_expect_warning_for_fn!( + function_with_warning_and_user_defined_category, + f, + [( + "this function raises user-defined warning", + UserDefinedWarning + )] + ); +} + +#[test] +fn test_pyfunction_deprecated() { + #[pyfunction] + #[pyo3(deprecated = "this function is deprecated")] + fn deprecated_function() {} + + py_expect_warning_for_fn!( + deprecated_function, + f, + [("this function is deprecated", PyDeprecationWarning)] + ); +} + +#[test] +fn test_pyfunction_multiple_warnings() { + #[pyfunction] + #[pyo3(warn(message = "this function raises warning"))] + #[pyo3(warn(message = "this function raises FutureWarning", category = PyFutureWarning))] + fn function_with_multiple_warnings() {} + + py_expect_warning_for_fn!( + function_with_multiple_warnings, + f, + [ + ("this function raises warning", PyUserWarning), + ("this function raises FutureWarning", PyFutureWarning) + ] + ); + + #[cfg(not(Py_LIMITED_API))] + #[pyfunction] + #[pyo3(warn(message = "this function raises FutureWarning", category = PyFutureWarning))] + #[pyo3(warn(message = "this function raises user-defined warning", category = UserDefinedWarning))] + fn function_with_multiple_custom_warnings() {} + + #[cfg(not(Py_LIMITED_API))] + py_expect_warning_for_fn!( + function_with_multiple_custom_warnings, + f, + [ + ("this function raises FutureWarning", PyFutureWarning), + ( + "this function raises user-defined warning", + UserDefinedWarning + ) + ] + ); + + #[pyfunction] + #[pyo3(warn(message = "this function raises warning"))] + #[pyo3(deprecated = "this function is deprecated")] + fn function_with_warning_and_deprecated() {} + + py_expect_warning_for_fn!( + function_with_warning_and_deprecated, + f, + [ + ("this function raises warning", PyUserWarning), + ("this function is deprecated", PyDeprecationWarning) + ] + ); +} diff --git a/tests/ui/invalid_pyfunction_deprecated.rs b/tests/ui/invalid_pyfunction_deprecated.rs new file mode 100644 index 00000000000..4cc872ba67a --- /dev/null +++ b/tests/ui/invalid_pyfunction_deprecated.rs @@ -0,0 +1,16 @@ +use pyo3::prelude::*; + +#[pyfunction] +#[pyo3(deprecated)] +fn deprecated_function() {} + +#[pyfunction] +#[pyo3(deprecated = )] +fn deprecated_function2() {} + +#[pyfunction] +#[pyo3(deprecated = "first deprecated")] +#[pyo3(deprecated = "second deprecated")] +fn deprecated_function3() {} + +fn main() {} diff --git a/tests/ui/invalid_pyfunction_deprecated.stderr b/tests/ui/invalid_pyfunction_deprecated.stderr new file mode 100644 index 00000000000..68f61003256 --- /dev/null +++ b/tests/ui/invalid_pyfunction_deprecated.stderr @@ -0,0 +1,17 @@ +error: expected `=` + --> tests/ui/invalid_pyfunction_deprecated.rs:4:18 + | +4 | #[pyo3(deprecated)] + | ^ + +error: unexpected end of input, expected string literal + --> tests/ui/invalid_pyfunction_deprecated.rs:8:21 + | +8 | #[pyo3(deprecated = )] + | ^ + +error: only one `deprecated` warning may be specified + --> tests/ui/invalid_pyfunction_deprecated.rs:13:8 + | +13 | #[pyo3(deprecated = "second deprecated")] + | ^^^^^^^^^^ diff --git a/tests/ui/invalid_pyfunction_warn.rs b/tests/ui/invalid_pyfunction_warn.rs new file mode 100644 index 00000000000..fe1b72dbc3f --- /dev/null +++ b/tests/ui/invalid_pyfunction_warn.rs @@ -0,0 +1,47 @@ +use pyo3::prelude::*; + +#[pyfunction] +#[pyo3(warn)] +fn no_parenthesis_deprecated() {} + +#[pyfunction] +#[pyo3(warn())] +fn no_message_deprecated() {} + +#[pyfunction] +#[pyo3(warn(category = pyo3::exceptions::PyDeprecationWarning))] +fn no_message_deprecated_with_category() {} + +#[pyfunction] +#[pyo3(warn(category = pyo3::exceptions::PyDeprecationWarning, message = ,))] +fn empty_message_deprecated_with_category() {} + +#[pyfunction] +#[pyo3(warn(message = "deprecated function", category = ,))] +fn empty_category_deprecated_with_message() {} + +#[pyfunction] +#[pyo3(warn(message = "deprecated function", random_key))] +fn random_key_deprecated() {} + +#[pyclass] +struct DeprecatedMethodContainer {} + +#[pymethods] +impl DeprecatedMethodContainer { + #[classattr] + #[pyo3(warn(message = "deprecated class attr"))] + fn deprecated_class_attr() -> i32 { + 5 + } +} + +#[pymethods] +impl DeprecatedMethodContainer { + #[pyo3(warn(message = "deprecated __traverse__"))] + fn __traverse__(&self, _visit: pyo3::gc::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/ui/invalid_pyfunction_warn.stderr b/tests/ui/invalid_pyfunction_warn.stderr new file mode 100644 index 00000000000..70d90faa483 --- /dev/null +++ b/tests/ui/invalid_pyfunction_warn.stderr @@ -0,0 +1,47 @@ +error: unexpected end of input, expected parentheses + --> tests/ui/invalid_pyfunction_warn.rs:4:12 + | +4 | #[pyo3(warn)] + | ^ + +error: missing `message` in `warn` attribute + --> tests/ui/invalid_pyfunction_warn.rs:8:13 + | +8 | #[pyo3(warn())] + | ^ + +error: missing `message` in `warn` attribute + --> tests/ui/invalid_pyfunction_warn.rs:12:62 + | +12 | #[pyo3(warn(category = pyo3::exceptions::PyDeprecationWarning))] + | ^ + +error: expected string literal + --> tests/ui/invalid_pyfunction_warn.rs:16:74 + | +16 | #[pyo3(warn(category = pyo3::exceptions::PyDeprecationWarning, message = ,))] + | ^ + +error: expected identifier + --> tests/ui/invalid_pyfunction_warn.rs:20:57 + | +20 | #[pyo3(warn(message = "deprecated function", category = ,))] + | ^ + +error: expected `message` or `category` + --> tests/ui/invalid_pyfunction_warn.rs:24:46 + | +24 | #[pyo3(warn(message = "deprecated function", random_key))] + | ^^^^^^^^^^ + +error: #[classattr] cannot be used with #[pyo3(warn)] nor #[pyo3(deprecated)] + --> tests/ui/invalid_pyfunction_warn.rs:33:12 + | +33 | #[pyo3(warn(message = "deprecated class attr"))] + | ^^^^ + +error: __traverse__ cannot be used with #[pyo3(warn)] nor #[pyo3(deprecated)] + --> tests/ui/invalid_pyfunction_warn.rs:41:12 + | +41 | #[pyo3(warn(message = "deprecated __traverse__"))] + | ^^^^ diff --git a/tests/ui/invalid_pymethods_deprecated.rs b/tests/ui/invalid_pymethods_deprecated.rs new file mode 100644 index 00000000000..20c0a6ce1e8 --- /dev/null +++ b/tests/ui/invalid_pymethods_deprecated.rs @@ -0,0 +1,30 @@ +use pyo3::prelude::*; + +#[pyclass] +struct DeprecatedMethodContainer {} + +#[pymethods] +impl DeprecatedMethodContainer { + #[pyo3(deprecated = "deprecated __traverse__")] + fn __traverse__(&self, _visit: pyo3::gc::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + Ok(()) + } +} + +#[pymethods] +impl DeprecatedMethodContainer { + #[classattr] + #[pyo3(deprecated = "deprecated class attr")] + fn deprecated_class_attr() -> i32 { + 5 + } +} + +#[pymethods] +impl DeprecatedMethodContainer { + #[pyo3(deprecated = "first deprecatec")] + #[pyo3(deprecated = "second deprecated")] + fn function(&self) {} +} + +fn main() {} diff --git a/tests/ui/invalid_pymethods_deprecated.stderr b/tests/ui/invalid_pymethods_deprecated.stderr new file mode 100644 index 00000000000..3c8820fb864 --- /dev/null +++ b/tests/ui/invalid_pymethods_deprecated.stderr @@ -0,0 +1,17 @@ +error: __traverse__ cannot be used with #[pyo3(warn)] nor #[pyo3(deprecated)] + --> tests/ui/invalid_pymethods_deprecated.rs:8:12 + | +8 | #[pyo3(deprecated = "deprecated __traverse__")] + | ^^^^^^^^^^ + +error: #[classattr] cannot be used with #[pyo3(warn)] nor #[pyo3(deprecated)] + --> tests/ui/invalid_pymethods_deprecated.rs:17:12 + | +17 | #[pyo3(deprecated = "deprecated class attr")] + | ^^^^^^^^^^ + +error: only one `deprecated` warning may be specified + --> tests/ui/invalid_pymethods_deprecated.rs:26:12 + | +26 | #[pyo3(deprecated = "second deprecated")] + | ^^^^^^^^^^ diff --git a/tests/ui/invalid_pymethods_warn.rs b/tests/ui/invalid_pymethods_warn.rs new file mode 100644 index 00000000000..6c6f39f5dc0 --- /dev/null +++ b/tests/ui/invalid_pymethods_warn.rs @@ -0,0 +1,21 @@ +use pyo3::prelude::*; + +#[pyclass] +struct WarningMethodContainer {} + +#[pymethods] +impl WarningMethodContainer { + #[pyo3(warn(message = "warn on __traverse__"))] + fn __traverse__(&self) {} +} + +#[pymethods] +impl WarningMethodContainer { + #[classattr] + #[pyo3(warn(message = "warn for class attr"))] + fn a_class_attr(_py: pyo3::Python<'_>) -> i64 { + 5 + } +} + +fn main() {} diff --git a/tests/ui/invalid_pymethods_warn.stderr b/tests/ui/invalid_pymethods_warn.stderr new file mode 100644 index 00000000000..e1f26ed0db7 --- /dev/null +++ b/tests/ui/invalid_pymethods_warn.stderr @@ -0,0 +1,11 @@ +error: __traverse__ cannot be used with #[pyo3(warn)] nor #[pyo3(deprecated)] + --> tests/ui/invalid_pymethods_warn.rs:8:12 + | +8 | #[pyo3(warn(message = "warn on __traverse__"))] + | ^^^^ + +error: #[classattr] cannot be used with #[pyo3(warn)] nor #[pyo3(deprecated)] + --> tests/ui/invalid_pymethods_warn.rs:15:12 + | +15 | #[pyo3(warn(message = "warn for class attr"))] + | ^^^^