Skip to content

Commit 3a6296e

Browse files
davidhewittIcxolu
andauthored
silence deprecation warning for enum with custom __eq__ (#4692)
* silence deprecation warning for enum with custom `__eq__` * clippy * also support `__richcmp__` * fix ci --------- Co-authored-by: Icxolu <[email protected]>
1 parent de709e5 commit 3a6296e

File tree

8 files changed

+126
-7
lines changed

8 files changed

+126
-7
lines changed

newsfragments/4692.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix incorrect deprecation warning for `#[pyclass] enum`s with custom `__eq__` implementation.

pyo3-macros-backend/src/pyclass.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,12 +1908,7 @@ fn pyclass_richcmp_simple_enum(
19081908
let deprecation = (options.eq_int.is_none() && options.eq.is_none())
19091909
.then(|| {
19101910
quote! {
1911-
#[deprecated(
1912-
since = "0.22.0",
1913-
note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior."
1914-
)]
1915-
const DEPRECATION: () = ();
1916-
const _: () = DEPRECATION;
1911+
let _ = #pyo3_path::impl_::pyclass::DeprecationTest::<#cls>::new().autogenerated_equality();
19171912
}
19181913
})
19191914
.unwrap_or_default();

pyo3-macros-backend/src/pymethod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,13 +1348,22 @@ impl SlotDef {
13481348
)?;
13491349
let name = spec.name;
13501350
let holders = holders.init_holders(ctx);
1351+
let dep = if method_name == "__richcmp__" {
1352+
quote! {
1353+
#[allow(unknown_lints, non_local_definitions)]
1354+
impl #pyo3_path::impl_::pyclass::HasCustomRichCmp for #cls {}
1355+
}
1356+
} else {
1357+
TokenStream::default()
1358+
};
13511359
let associated_method = quote! {
13521360
#[allow(non_snake_case)]
13531361
unsafe fn #wrapper_ident(
13541362
py: #pyo3_path::Python<'_>,
13551363
_raw_slf: *mut #pyo3_path::ffi::PyObject,
13561364
#(#arg_idents: #arg_types),*
13571365
) -> #pyo3_path::PyResult<#ret_ty> {
1366+
#dep
13581367
let function = #cls::#name; // Shadow the method name to avoid #3017
13591368
let _slf = _raw_slf;
13601369
#holders

src/impl_/pyclass.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,8 @@ macro_rules! generate_pyclass_richcompare_slot {
878878
other: *mut $crate::ffi::PyObject,
879879
op: ::std::os::raw::c_int,
880880
) -> *mut $crate::ffi::PyObject {
881+
impl $crate::impl_::pyclass::HasCustomRichCmp for $cls {}
882+
881883
$crate::impl_::trampoline::richcmpfunc(slf, other, op, |py, slf, other, op| {
882884
use $crate::class::basic::CompareOp;
883885
use $crate::impl_::pyclass::*;
@@ -1519,6 +1521,50 @@ fn pyo3_get_value<
15191521
Ok((unsafe { &*value }).clone().into_py(py).into_ptr())
15201522
}
15211523

1524+
/// Marker trait whether a class implemented a custom comparison. Used to
1525+
/// silence deprecation of autogenerated `__richcmp__` for enums.
1526+
pub trait HasCustomRichCmp {}
1527+
1528+
/// Autoref specialization setup to emit deprecation warnings for autogenerated
1529+
/// pyclass equality.
1530+
pub struct DeprecationTest<T>(Deprecation, ::std::marker::PhantomData<T>);
1531+
pub struct Deprecation;
1532+
1533+
impl<T> DeprecationTest<T> {
1534+
#[inline]
1535+
#[allow(clippy::new_without_default)]
1536+
pub const fn new() -> Self {
1537+
DeprecationTest(Deprecation, ::std::marker::PhantomData)
1538+
}
1539+
}
1540+
1541+
impl<T> std::ops::Deref for DeprecationTest<T> {
1542+
type Target = Deprecation;
1543+
#[inline]
1544+
fn deref(&self) -> &Self::Target {
1545+
&self.0
1546+
}
1547+
}
1548+
1549+
impl<T> DeprecationTest<T>
1550+
where
1551+
T: HasCustomRichCmp,
1552+
{
1553+
/// For `HasCustomRichCmp` types; no deprecation warning.
1554+
#[inline]
1555+
pub fn autogenerated_equality(&self) {}
1556+
}
1557+
1558+
impl Deprecation {
1559+
#[deprecated(
1560+
since = "0.22.0",
1561+
note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior."
1562+
)]
1563+
/// For types which don't implement `HasCustomRichCmp`; emits deprecation warning.
1564+
#[inline]
1565+
pub fn autogenerated_equality(&self) {}
1566+
}
1567+
15221568
#[cfg(test)]
15231569
#[cfg(feature = "macros")]
15241570
mod tests {

tests/test_enum.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use pyo3::prelude::*;
44
use pyo3::py_run;
5+
use pyo3::types::PyString;
56

67
#[path = "../src/tests/common.rs"]
78
mod common;
@@ -357,3 +358,48 @@ mod deprecated {
357358
})
358359
}
359360
}
361+
362+
#[test]
363+
fn custom_eq() {
364+
#[pyclass(frozen)]
365+
#[derive(PartialEq)]
366+
pub enum CustomPyEq {
367+
A,
368+
B,
369+
}
370+
371+
#[pymethods]
372+
impl CustomPyEq {
373+
fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
374+
if let Ok(rhs) = other.downcast::<PyString>() {
375+
rhs.to_cow().map_or(false, |rhs| self.__str__() == rhs)
376+
} else if let Ok(rhs) = other.downcast::<Self>() {
377+
self == rhs.get()
378+
} else {
379+
false
380+
}
381+
}
382+
383+
fn __str__(&self) -> String {
384+
match self {
385+
CustomPyEq::A => "A".to_string(),
386+
CustomPyEq::B => "B".to_string(),
387+
}
388+
}
389+
}
390+
391+
Python::with_gil(|py| {
392+
let a = Bound::new(py, CustomPyEq::A).unwrap();
393+
let b = Bound::new(py, CustomPyEq::B).unwrap();
394+
395+
assert!(a.as_any().eq(&a).unwrap());
396+
assert!(a.as_any().eq("A").unwrap());
397+
assert!(a.as_any().ne(&b).unwrap());
398+
assert!(a.as_any().ne("B").unwrap());
399+
400+
assert!(b.as_any().eq(&b).unwrap());
401+
assert!(b.as_any().eq("B").unwrap());
402+
assert!(b.as_any().ne(&a).unwrap());
403+
assert!(b.as_any().ne("A").unwrap());
404+
})
405+
}

tests/ui/deprecations.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`:
2828
21 | fn pyfunction_option_4(
2929
| ^^^^^^^^^^^^^^^^^^^
3030

31-
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.
31+
error: use of deprecated method `pyo3::impl_::pyclass::Deprecation::autogenerated_equality`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior.
3232
--> tests/ui/deprecations.rs:28:1
3333
|
3434
28 | #[pyclass]

tests/ui/invalid_proto_pymethods.stderr

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ note: candidate #2 is defined in an impl for the type `EqAndRichcmp`
4040
| ^^^^^^^^^^^^
4141
= note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info)
4242

43+
error[E0119]: conflicting implementations of trait `HasCustomRichCmp` for type `EqAndRichcmp`
44+
--> tests/ui/invalid_proto_pymethods.rs:55:1
45+
|
46+
55 | #[pymethods]
47+
| ^^^^^^^^^^^^
48+
| |
49+
| first implementation here
50+
| conflicting implementation for `EqAndRichcmp`
51+
|
52+
= note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info)
53+
4354
error[E0592]: duplicate definitions with name `__pymethod___richcmp____`
4455
--> tests/ui/invalid_proto_pymethods.rs:55:1
4556
|

tests/ui/invalid_pyclass_args.stderr

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,17 @@ error: The format string syntax cannot be used with enums
162162
171 | #[pyclass(eq, str = "Stuff...")]
163163
| ^^^^^^^^^^
164164

165+
error[E0119]: conflicting implementations of trait `HasCustomRichCmp` for type `EqOptAndManualRichCmp`
166+
--> tests/ui/invalid_pyclass_args.rs:41:1
167+
|
168+
37 | #[pyclass(eq)]
169+
| -------------- first implementation here
170+
...
171+
41 | #[pymethods]
172+
| ^^^^^^^^^^^^ conflicting implementation for `EqOptAndManualRichCmp`
173+
|
174+
= note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info)
175+
165176
error[E0592]: duplicate definitions with name `__pymethod___richcmp____`
166177
--> tests/ui/invalid_pyclass_args.rs:37:1
167178
|

0 commit comments

Comments
 (0)