From d1a7cf400a7e09b48df8bac4efe6679d987b303c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 31 May 2024 16:13:30 +0200 Subject: [PATCH] add pyclass `eq` option (#4210) * add pyclass `eq` option * prevent manual impl of `__richcmp__` or `__eq__` with `#[pyclass(eq)]` * add simple enum `eq_int` option * rearrange names to fix deprecation warning * add newsfragment and migration * update docs --------- Co-authored-by: David Hewitt --- guide/pyclass-parameters.md | 2 + guide/src/class.md | 23 ++- guide/src/class/object.md | 11 ++ guide/src/migration.md | 34 ++++ newsfragments/4210.added.md | 2 + newsfragments/4210.changed.md | 1 + pyo3-macros-backend/src/attributes.rs | 2 + pyo3-macros-backend/src/pyclass.rs | 242 +++++++++++++++++++++----- pytests/src/enums.rs | 3 +- src/tests/hygiene/pyclass.rs | 3 +- tests/test_declarative_module.rs | 3 +- tests/test_default_impls.rs | 6 +- tests/test_enum.rs | 22 ++- tests/ui/deprecations.rs | 6 + tests/ui/deprecations.stderr | 8 + tests/ui/invalid_pyclass_args.rs | 22 +++ tests/ui/invalid_pyclass_args.stderr | 91 +++++++++- tests/ui/invalid_pyclass_enum.rs | 18 ++ tests/ui/invalid_pyclass_enum.stderr | 74 ++++++++ 19 files changed, 501 insertions(+), 72 deletions(-) create mode 100644 newsfragments/4210.added.md create mode 100644 newsfragments/4210.changed.md diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 9bd0534e..77750e36 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -5,6 +5,8 @@ | `constructor` | This is currently only allowed on [variants of complex enums][params-constructor]. It allows customization of the generated class constructor for each variant. It uses the same syntax and supports the same options as the `signature` attribute of functions and methods. | | `crate = "some::path"` | Path to import the `pyo3` crate, if it's not accessible at `::pyo3`. | | `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. | +| `eq` | Implements `__eq__` using the `PartialEq` implementation of the underlying Rust datatype. | +| `eq_int` | Implements `__eq__` using `__int__` for simple enums. | | `extends = BaseType` | Use a custom baseclass. Defaults to [`PyAny`][params-1] | | `freelist = N` | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. | | `frozen` | Declares that your pyclass is immutable. It removes the borrow checker overhead when retrieving a shared reference to the Rust struct, but disables the ability to get a mutable reference. | diff --git a/guide/src/class.md b/guide/src/class.md index 57a5cf6d..b72cae34 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -37,14 +37,16 @@ struct Number(i32); // PyO3 supports unit-only enums (which contain only unit variants) // These simple enums behave similarly to Python's enumerations (enum.Enum) -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum { Variant, OtherVariant = 30, // PyO3 supports custom discriminants. } // PyO3 supports custom discriminants in unit-only enums -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum HttpResponse { Ok = 200, NotFound = 404, @@ -1053,7 +1055,8 @@ PyO3 adds a class attribute for each variant, so you can access them in Python w ```rust # use pyo3::prelude::*; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum { Variant, OtherVariant, @@ -1075,7 +1078,8 @@ You can also convert your simple enums into `int`: ```rust # use pyo3::prelude::*; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum { Variant, OtherVariant = 10, @@ -1087,8 +1091,6 @@ Python::with_gil(|py| { pyo3::py_run!(py, cls x, r#" assert int(cls.Variant) == x assert int(cls.OtherVariant) == 10 - assert cls.OtherVariant == 10 # You can also compare against int. - assert 10 == cls.OtherVariant "#) }) ``` @@ -1097,7 +1099,8 @@ PyO3 also provides `__repr__` for enums: ```rust # use pyo3::prelude::*; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum{ Variant, OtherVariant, @@ -1117,7 +1120,8 @@ All methods defined by PyO3 can be overridden. For example here's how you overri ```rust # use pyo3::prelude::*; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum { Answer = 42, } @@ -1139,7 +1143,8 @@ Enums and their variants can also be renamed using `#[pyo3(name)]`. ```rust # use pyo3::prelude::*; -#[pyclass(name = "RenamedEnum")] +#[pyclass(eq, eq_int, name = "RenamedEnum")] +#[derive(PartialEq)] enum MyEnum { #[pyo3(name = "UPPERCASE")] Variant, diff --git a/guide/src/class/object.md b/guide/src/class/object.md index db6cc7d3..e7a36687 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -226,6 +226,16 @@ impl Number { # } ``` +To implement `__eq__` using the Rust [`PartialEq`] trait implementation, the `eq` option can be used. + +```rust +# use pyo3::prelude::*; +# +#[pyclass(eq)] +#[derive(PartialEq)] +struct Number(i32); +``` + ### Truthyness We'll consider `Number` to be `True` if it is nonzero: @@ -305,3 +315,4 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { [`Hasher`]: https://doc.rust-lang.org/std/hash/trait.Hasher.html [`DefaultHasher`]: https://doc.rust-lang.org/std/collections/hash_map/struct.DefaultHasher.html [SipHash]: https://en.wikipedia.org/wiki/SipHash +[`PartialEq`]: https://doc.rust-lang.org/stable/std/cmp/trait.PartialEq.html diff --git a/guide/src/migration.md b/guide/src/migration.md index f56db2a5..31e912a6 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -47,6 +47,40 @@ However, take care to note that the behaviour is different from previous version Related to this, we also added a `pyo3_disable_reference_pool` conditional compilation flag which removes the infrastructure necessary to apply delayed reference count decrements implied by `impl Drop for Py`. They do not appear to be a soundness hazard as they should lead to memory leaks in the worst case. However, the global synchronization adds significant overhead to cross the Python-Rust boundary. Enabling this feature will remove these costs and make the `Drop` implementation abort the process if called without the GIL being held instead. +### Require explicit opt-in for comparison for simple enums +
+Click to expand + +With `pyo3` 0.22 the new `#[pyo3(eq)]` options allows automatic implementation of Python equality using Rust's `PartialEq`. Previously simple enums automatically implemented equality in terms of their discriminants. To make PyO3 more consistent, this automatic equality implementation is deprecated in favour of having opt-ins for all `#[pyclass]` types. Similarly, simple enums supported comparison with integers, which is not covered by Rust's `PartialEq` derive, so has been split out into the `#[pyo3(eq_int)]` attribute. + +To migrate, place a `#[pyo3(eq, eq_int)]` attribute on simple enum classes. + +Before: + +```rust +# #![allow(deprecated, dead_code)] +# use pyo3::prelude::*; +#[pyclass] +enum SimpleEnum { + VariantA, + VariantB = 42, +} +``` + +After: + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] +enum SimpleEnum { + VariantA, + VariantB = 42, +} +``` +
+ ## from 0.20.* to 0.21
Click to expand diff --git a/newsfragments/4210.added.md b/newsfragments/4210.added.md new file mode 100644 index 00000000..dae8cd8d --- /dev/null +++ b/newsfragments/4210.added.md @@ -0,0 +1,2 @@ +Added `#[pyclass(eq)]` option to generate `__eq__` based on `PartialEq`. +Added `#[pyclass(eq_int)]` for simple enums to implement equality based on their discriminants. \ No newline at end of file diff --git a/newsfragments/4210.changed.md b/newsfragments/4210.changed.md new file mode 100644 index 00000000..a69e3c3a --- /dev/null +++ b/newsfragments/4210.changed.md @@ -0,0 +1 @@ +Deprecate implicit integer comparision for simple enums in favor of `#[pyclass(eq_int)]`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index d9c805aa..3bccf0ae 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -14,6 +14,8 @@ pub mod kw { syn::custom_keyword!(cancel_handle); syn::custom_keyword!(constructor); syn::custom_keyword!(dict); + syn::custom_keyword!(eq); + syn::custom_keyword!(eq_int); syn::custom_keyword!(extends); syn::custom_keyword!(freelist); syn::custom_keyword!(from_py_with); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 47c52c84..5838f7c5 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -18,7 +18,7 @@ use crate::utils::Ctx; use crate::utils::{self, apply_renaming_rule, PythonDoc}; use crate::PyFunctionOptions; use proc_macro2::{Ident, Span, TokenStream}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, quote_spanned}; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; @@ -59,6 +59,8 @@ impl PyClassArgs { pub struct PyClassPyO3Options { pub krate: Option, pub dict: Option, + pub eq: Option, + pub eq_int: Option, pub extends: Option, pub get_all: Option, pub freelist: Option, @@ -77,6 +79,8 @@ pub struct PyClassPyO3Options { enum PyClassPyO3Option { Crate(CrateAttribute), Dict(kw::dict), + Eq(kw::eq), + EqInt(kw::eq_int), Extends(ExtendsAttribute), Freelist(FreelistAttribute), Frozen(kw::frozen), @@ -99,6 +103,10 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Crate) } else if lookahead.peek(kw::dict) { input.parse().map(PyClassPyO3Option::Dict) + } else if lookahead.peek(kw::eq) { + input.parse().map(PyClassPyO3Option::Eq) + } else if lookahead.peek(kw::eq_int) { + input.parse().map(PyClassPyO3Option::EqInt) } else if lookahead.peek(kw::extends) { input.parse().map(PyClassPyO3Option::Extends) } else if lookahead.peek(attributes::kw::freelist) { @@ -166,6 +174,8 @@ impl PyClassPyO3Options { match option { PyClassPyO3Option::Crate(krate) => set_option!(krate), PyClassPyO3Option::Dict(dict) => set_option!(dict), + PyClassPyO3Option::Eq(eq) => set_option!(eq), + PyClassPyO3Option::EqInt(eq_int) => set_option!(eq_int), PyClassPyO3Option::Extends(extends) => set_option!(extends), PyClassPyO3Option::Freelist(freelist) => set_option!(freelist), PyClassPyO3Option::Frozen(frozen) => set_option!(frozen), @@ -350,6 +360,12 @@ fn impl_class( let Ctx { pyo3_path } = ctx; let pytypeinfo_impl = impl_pytypeinfo(cls, args, None, ctx); + let (default_richcmp, default_richcmp_slot) = + pyclass_richcmp(&args.options, &syn::parse_quote!(#cls), ctx)?; + + let mut slots = Vec::new(); + slots.extend(default_richcmp_slot); + let py_class_impl = PyClassImplsBuilder::new( cls, args, @@ -361,7 +377,7 @@ fn impl_class( field_options, ctx, )?, - vec![], + slots, ) .doc(doc) .impl_all(ctx)?; @@ -372,6 +388,12 @@ fn impl_class( #pytypeinfo_impl #py_class_impl + + #[doc(hidden)] + #[allow(non_snake_case)] + impl #cls { + #default_richcmp + } }) } @@ -723,7 +745,6 @@ fn impl_simple_enum( methods_type: PyClassMethodsType, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; let cls = simple_enum.ident; let ty: syn::Type = syn::parse_quote!(#cls); let variants = simple_enum.variants; @@ -775,50 +796,11 @@ fn impl_simple_enum( (int_impl, int_slot) }; - let (default_richcmp, default_richcmp_slot) = { - let mut richcmp_impl: syn::ImplItemFn = syn::parse_quote! { - fn __pyo3__richcmp__( - &self, - py: #pyo3_path::Python, - other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, - op: #pyo3_path::basic::CompareOp - ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { - use #pyo3_path::conversion::ToPyObject; - use #pyo3_path::types::PyAnyMethods; - use ::core::result::Result::*; - match op { - #pyo3_path::basic::CompareOp::Eq => { - let self_val = self.__pyo3__int__(); - if let Ok(i) = other.extract::<#repr_type>() { - return Ok((self_val == i).to_object(py)); - } - if let Ok(other) = other.extract::<#pyo3_path::PyRef>() { - return Ok((self_val == other.__pyo3__int__()).to_object(py)); - } + let (default_richcmp, default_richcmp_slot) = + pyclass_richcmp_simple_enum(&args.options, &ty, repr_type, ctx); - return Ok(py.NotImplemented()); - } - #pyo3_path::basic::CompareOp::Ne => { - let self_val = self.__pyo3__int__(); - if let Ok(i) = other.extract::<#repr_type>() { - return Ok((self_val != i).to_object(py)); - } - if let Ok(other) = other.extract::<#pyo3_path::PyRef>() { - return Ok((self_val != other.__pyo3__int__()).to_object(py)); - } - - return Ok(py.NotImplemented()); - } - _ => Ok(py.NotImplemented()), - } - } - }; - let richcmp_slot = - generate_default_protocol_slot(&ty, &mut richcmp_impl, &__RICHCMP__, ctx).unwrap(); - (richcmp_impl, richcmp_slot) - }; - - let default_slots = vec![default_repr_slot, default_int_slot, default_richcmp_slot]; + let mut default_slots = vec![default_repr_slot, default_int_slot]; + default_slots.extend(default_richcmp_slot); let pyclass_impls = PyClassImplsBuilder::new( cls, @@ -857,6 +839,8 @@ fn impl_complex_enum( ctx: &Ctx, ) -> Result { let Ctx { pyo3_path } = ctx; + let cls = complex_enum.ident; + let ty: syn::Type = syn::parse_quote!(#cls); // Need to rig the enum PyClass options let args = { @@ -873,7 +857,10 @@ fn impl_complex_enum( let variants = complex_enum.variants; let pytypeinfo = impl_pytypeinfo(cls, &args, None, ctx); - let default_slots = vec![]; + let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &ty, ctx)?; + + let mut default_slots = vec![]; + default_slots.extend(default_richcmp_slot); let impl_builder = PyClassImplsBuilder::new( cls, @@ -978,7 +965,9 @@ fn impl_complex_enum( #[doc(hidden)] #[allow(non_snake_case)] - impl #cls {} + impl #cls { + #default_richcmp + } #(#variant_cls_zsts)* @@ -1276,6 +1265,23 @@ fn gen_complex_enum_variant_class_ident(enum_: &syn::Ident, variant: &syn::Ident format_ident!("{}_{}", enum_, variant) } +fn generate_protocol_slot( + cls: &syn::Type, + method: &mut syn::ImplItemFn, + slot: &SlotDef, + name: &str, + ctx: &Ctx, +) -> syn::Result { + let spec = FnSpec::parse( + &mut method.sig, + &mut Vec::new(), + PyFunctionOptions::default(), + ctx, + ) + .unwrap(); + slot.generate_type_slot(&syn::parse_quote!(#cls), &spec, name, ctx) +} + fn generate_default_protocol_slot( cls: &syn::Type, method: &mut syn::ImplItemFn, @@ -1637,6 +1643,146 @@ fn impl_pytypeinfo( } } +fn pyclass_richcmp_arms(options: &PyClassPyO3Options, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; + + let eq_arms = options + .eq + .map(|eq| { + quote_spanned! { eq.span() => + #pyo3_path::pyclass::CompareOp::Eq => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val == other, py)) + }, + #pyo3_path::pyclass::CompareOp::Ne => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val != other, py)) + }, + } + }) + .unwrap_or_default(); + + // TODO: `ord` can be integrated here (#4202) + #[allow(clippy::let_and_return)] + eq_arms +} + +fn pyclass_richcmp_simple_enum( + options: &PyClassPyO3Options, + cls: &syn::Type, + repr_type: &syn::Ident, + ctx: &Ctx, +) -> (Option, Option) { + let Ctx { pyo3_path } = ctx; + + let arms = pyclass_richcmp_arms(options, ctx); + + let deprecation = options + .eq_int + .map(|_| TokenStream::new()) + .unwrap_or_else(|| { + quote! { + #[deprecated( + since = "0.22.0", + note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior." + )] + const DEPRECATION: () = (); + const _: () = DEPRECATION; + } + }); + + let mut options = options.clone(); + options.eq_int = Some(parse_quote!(eq_int)); + + if options.eq.is_none() && options.eq_int.is_none() { + return (None, None); + } + + let eq = options.eq.map(|eq| { + quote_spanned! { eq.span() => + let self_val = self; + if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::downcast::(other) { + let other = &*other.borrow(); + return match op { + #arms + _ => ::std::result::Result::Ok(py.NotImplemented()) + } + } + } + }); + + let eq_int = options.eq_int.map(|eq_int| { + quote_spanned! { eq_int.span() => + let self_val = self.__pyo3__int__(); + if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::extract::<#repr_type>(other).or_else(|_| { + #pyo3_path::types::PyAnyMethods::downcast::(other).map(|o| o.borrow().__pyo3__int__()) + }) { + return match op { + #arms + _ => ::std::result::Result::Ok(py.NotImplemented()) + } + } + } + }); + + let mut richcmp_impl = parse_quote! { + fn __pyo3__generated____richcmp__( + &self, + py: #pyo3_path::Python, + other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, + op: #pyo3_path::pyclass::CompareOp + ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + #deprecation + + #eq + + #eq_int + + ::std::result::Result::Ok(py.NotImplemented()) + } + }; + let richcmp_slot = if options.eq.is_some() { + generate_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, "__richcmp__", ctx).unwrap() + } else { + generate_default_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, ctx).unwrap() + }; + (Some(richcmp_impl), Some(richcmp_slot)) +} + +fn pyclass_richcmp( + options: &PyClassPyO3Options, + cls: &syn::Type, + ctx: &Ctx, +) -> Result<(Option, Option)> { + let Ctx { pyo3_path } = ctx; + if let Some(eq_int) = options.eq_int { + bail_spanned!(eq_int.span() => "`eq_int` can only be used on simple enums.") + } + + let arms = pyclass_richcmp_arms(options, ctx); + if options.eq.is_some() { + let mut richcmp_impl = parse_quote! { + fn __pyo3__generated____richcmp__( + &self, + py: #pyo3_path::Python, + other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, + op: #pyo3_path::pyclass::CompareOp + ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + let self_val = self; + let other = &*#pyo3_path::types::PyAnyMethods::downcast::(other)?.borrow(); + match op { + #arms + _ => ::std::result::Result::Ok(py.NotImplemented()) + } + } + }; + let richcmp_slot = + generate_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, "__richcmp__", ctx) + .unwrap(); + Ok((Some(richcmp_impl), Some(richcmp_slot))) + } else { + Ok((None, None)) + } +} + /// Implements most traits used by `#[pyclass]`. /// /// Specifically, it implements traits that only depend on class name, diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 964f0d43..80d7550e 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -18,7 +18,8 @@ pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] pub enum SimpleEnum { Sunday, Monday, diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 34b30a8c..27a6b388 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -29,8 +29,9 @@ pub struct Bar { c: ::std::option::Option>, } -#[crate::pyclass] +#[crate::pyclass(eq, eq_int)] #[pyo3(crate = "crate")] +#[derive(PartialEq)] pub enum Enum { Var0, } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 2e46f4a6..820cf638 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -89,7 +89,8 @@ mod declarative_module { } } - #[pyclass] + #[pyclass(eq, eq_int)] + #[derive(PartialEq)] enum Enum { A, B, diff --git a/tests/test_default_impls.rs b/tests/test_default_impls.rs index 526f88e8..67077236 100644 --- a/tests/test_default_impls.rs +++ b/tests/test_default_impls.rs @@ -6,7 +6,8 @@ use pyo3::prelude::*; mod common; // Test default generated __repr__. -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum TestDefaultRepr { Var, } @@ -23,7 +24,8 @@ fn test_default_slot_exists() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum OverrideSlot { Var, } diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 63492b8d..148520dd 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -6,7 +6,7 @@ use pyo3::py_run; #[path = "../src/tests/common.rs"] mod common; -#[pyclass] +#[pyclass(eq, eq_int)] #[derive(Debug, PartialEq, Eq, Clone)] pub enum MyEnum { Variant, @@ -73,7 +73,8 @@ fn test_enum_eq_incomparable() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Eq, Clone)] enum CustomDiscriminant { One = 1, Two = 2, @@ -121,7 +122,8 @@ fn test_enum_compare_int() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Eq, Clone)] #[repr(u8)] enum SmallEnum { V = 1, @@ -135,7 +137,8 @@ fn test_enum_compare_int_no_throw_when_overflow() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Eq, Clone)] #[repr(usize)] #[allow(clippy::enum_clike_unportable_variant)] enum BigEnum { @@ -147,12 +150,14 @@ fn test_big_enum_no_overflow() { Python::with_gil(|py| { let usize_max = usize::MAX; let v = Py::new(py, BigEnum::V).unwrap(); + py_assert!(py, usize_max v, "v == usize_max"); py_assert!(py, usize_max v, "int(v) == usize_max"); }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Eq, Clone)] #[repr(u16, align(8))] enum TestReprParse { V, @@ -163,7 +168,7 @@ fn test_repr_parse() { assert_eq!(std::mem::align_of::(), 8); } -#[pyclass(name = "MyEnum")] +#[pyclass(eq, eq_int, name = "MyEnum")] #[derive(Debug, PartialEq, Eq, Clone)] pub enum RenameEnum { Variant, @@ -177,7 +182,7 @@ fn test_rename_enum_repr_correct() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] #[derive(Debug, PartialEq, Eq, Clone)] pub enum RenameVariantEnum { #[pyo3(name = "VARIANT")] @@ -192,7 +197,8 @@ fn test_rename_variant_repr_correct() { }) } -#[pyclass(rename_all = "SCREAMING_SNAKE_CASE")] +#[pyclass(eq, eq_int, rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(Debug, PartialEq, Eq, Clone)] #[allow(clippy::enum_variant_names)] enum RenameAllVariantsEnum { VariantOne, diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index fc9e8687..dbd0f8aa 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -196,3 +196,9 @@ fn test_wrap_pyfunction(py: Python<'_>, m: &Bound<'_, PyModule>) { let _ = wrap_pyfunction_bound!(double, py); let _ = wrap_pyfunction_bound!(double)(py); } + +#[pyclass] +pub enum SimpleEnumWithoutEq { + VariamtA, + VariantB, +} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index d014a06b..e0813986 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -34,6 +34,14 @@ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: 138 | fn pyfunction_option_4( | ^^^^^^^^^^^^^^^^^^^ +error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior. + --> tests/ui/deprecations.rs:200:1 + | +200 | #[pyclass] + | ^^^^^^^^^^ + | + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound`, use that instead; see the migration guide for more info --> tests/ui/deprecations.rs:23:30 | diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index fac21db0..6e359f61 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -30,4 +30,26 @@ struct InvalidArg {} #[pyclass(mapping, sequence)] struct CannotBeMappingAndSequence {} +#[pyclass(eq)] +struct EqOptRequiresEq {} + +#[pyclass(eq)] +#[derive(PartialEq)] +struct EqOptAndManualRichCmp {} + +#[pymethods] +impl EqOptAndManualRichCmp { + fn __richcmp__( + &self, + _py: Python, + _other: Bound<'_, PyAny>, + _op: pyo3::pyclass::CompareOp, + ) -> PyResult { + todo!() + } +} + +#[pyclass(eq_int)] +struct NoEqInt {} + fn main() {} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 5b2bd24d..72da8238 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -1,4 +1,4 @@ -error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:3:11 | 3 | #[pyclass(extend=pyo3::types::PyDict)] @@ -46,7 +46,7 @@ error: expected string literal 24 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:27:11 | 27 | #[pyclass(weakrev)] @@ -57,3 +57,90 @@ error: a `#[pyclass]` cannot be both a `mapping` and a `sequence` | 31 | struct CannotBeMappingAndSequence {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: `eq_int` can only be used on simple enums. + --> tests/ui/invalid_pyclass_args.rs:52:11 + | +52 | #[pyclass(eq_int)] + | ^^^^^^ + +error[E0592]: duplicate definitions with name `__pymethod___richcmp____` + --> tests/ui/invalid_pyclass_args.rs:36:1 + | +36 | #[pyclass(eq)] + | ^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___richcmp____` +... +40 | #[pymethods] + | ------------ other definition for `__pymethod___richcmp____` + | + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0369]: binary operation `==` cannot be applied to type `&EqOptRequiresEq` + --> tests/ui/invalid_pyclass_args.rs:33:11 + | +33 | #[pyclass(eq)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `EqOptRequiresEq` + --> tests/ui/invalid_pyclass_args.rs:34:1 + | +34 | struct EqOptRequiresEq {} + | ^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `EqOptRequiresEq` with `#[derive(PartialEq)]` + | +34 + #[derive(PartialEq)] +35 | struct EqOptRequiresEq {} + | + +error[E0369]: binary operation `!=` cannot be applied to type `&EqOptRequiresEq` + --> tests/ui/invalid_pyclass_args.rs:33:11 + | +33 | #[pyclass(eq)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `EqOptRequiresEq` + --> tests/ui/invalid_pyclass_args.rs:34:1 + | +34 | struct EqOptRequiresEq {} + | ^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `EqOptRequiresEq` with `#[derive(PartialEq)]` + | +34 + #[derive(PartialEq)] +35 | struct EqOptRequiresEq {} + | + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:36:1 + | +36 | #[pyclass(eq)] + | ^^^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found + | +note: candidate #1 is defined in an impl for the type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:36:1 + | +36 | #[pyclass(eq)] + | ^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:40:1 + | +40 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:40:1 + | +40 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found + | +note: candidate #1 is defined in an impl for the type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:36:1 + | +36 | #[pyclass(eq)] + | ^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:40:1 + | +40 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index e98010fe..3c6f08da 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -28,4 +28,22 @@ enum SimpleNoSignature { B, } +#[pyclass(eq, eq_int)] +enum SimpleEqOptRequiresPartialEq { + A, + B, +} + +#[pyclass(eq)] +enum ComplexEqOptRequiresPartialEq { + A(i32), + B { msg: String }, +} + +#[pyclass(eq_int)] +enum NoEqInt { + A(i32), + B { msg: String }, +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index 7e3b6ffa..551d920e 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -29,3 +29,77 @@ error: `constructor` can't be used on a simple enum variant | 26 | #[pyo3(constructor = (a, b))] | ^^^^^^^^^^^ + +error: `eq_int` can only be used on simple enums. + --> tests/ui/invalid_pyclass_enum.rs:43:11 + | +43 | #[pyclass(eq_int)] + | ^^^^^^ + +error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:31:11 + | +31 | #[pyclass(eq, eq_int)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `SimpleEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:32:1 + | +32 | enum SimpleEqOptRequiresPartialEq { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `SimpleEqOptRequiresPartialEq` with `#[derive(PartialEq)]` + | +32 + #[derive(PartialEq)] +33 | enum SimpleEqOptRequiresPartialEq { + | + +error[E0369]: binary operation `!=` cannot be applied to type `&SimpleEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:31:11 + | +31 | #[pyclass(eq, eq_int)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `SimpleEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:32:1 + | +32 | enum SimpleEqOptRequiresPartialEq { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `SimpleEqOptRequiresPartialEq` with `#[derive(PartialEq)]` + | +32 + #[derive(PartialEq)] +33 | enum SimpleEqOptRequiresPartialEq { + | + +error[E0369]: binary operation `==` cannot be applied to type `&ComplexEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:37:11 + | +37 | #[pyclass(eq)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `ComplexEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:38:1 + | +38 | enum ComplexEqOptRequiresPartialEq { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `ComplexEqOptRequiresPartialEq` with `#[derive(PartialEq)]` + | +38 + #[derive(PartialEq)] +39 | enum ComplexEqOptRequiresPartialEq { + | + +error[E0369]: binary operation `!=` cannot be applied to type `&ComplexEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:37:11 + | +37 | #[pyclass(eq)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `ComplexEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:38:1 + | +38 | enum ComplexEqOptRequiresPartialEq { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `ComplexEqOptRequiresPartialEq` with `#[derive(PartialEq)]` + | +38 + #[derive(PartialEq)] +39 | enum ComplexEqOptRequiresPartialEq { + |