From b8fb367582f2746323a35fa199cc8a9f1a14c2cc Mon Sep 17 00:00:00 2001 From: Michael Gilbert Date: Fri, 7 Jun 2024 12:08:53 -0700 Subject: [PATCH] feat: Add 'ord' option for PyClass and corresponding tests (#4202) * feat: Add 'ord' option for PyClass and corresponding tests Updated the macros back-end to include 'ord' as an option for PyClass allowing for Python-style ordering comparison of enum variants. Additionally, test cases to verify the proper functioning of this new feature have been introduced. * update: fix formatting with cargo fmt * update: documented added feature in newsfragments * update: updated saved errors for comparison test for invalid pyclass args * update: removed nested match arms and extended cases for ordering instead * update: alphabetically ordered entries * update: added section to class documentation with example for using ord argument. * refactor: reduced duplication of code using closure to process tokens. * update: used ensure_spanned macro to emit compile time errors for uses of ord on complex enums or structs, updated test errors for bad compile cases * fix: remove errant character * update: added note about PartialOrd being required. * feat: implemented ordering for structs and complex enums. Retained the equality logic for simple enums until PartialEq is deprecated. * update: adjusted compile time error checks for missing PartialOrd implementations. Refactored growing set of comparison tests for simple and complex enums and structs into separate test file. * fix: updated with clippy findings * update: added not to pyclass parameters on ord (assumes that eq will be implemented and merged first) * update: rebased on main after merging of `eq` feature * update: format update * update: update all test output and doc tests * Update guide/src/class.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update pyo3-macros-backend/src/pyclass.rs Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update newsfragments/4202.added.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update guide/pyclass-parameters.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * update: added note about `ord` implementation with example. * fix doc formatting --------- Co-authored-by: Michael Gilbert Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/pyclass-parameters.md | 1 + guide/src/class.md | 26 +++ guide/src/class/object.md | 10 + newsfragments/4202.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 48 ++++- tests/test_class_comparisons.rs | 287 ++++++++++++++++++++++++++ tests/test_enum.rs | 21 -- tests/ui/invalid_pyclass_args.rs | 5 + tests/ui/invalid_pyclass_args.stderr | 10 +- tests/ui/invalid_pyclass_enum.rs | 13 ++ tests/ui/invalid_pyclass_enum.stderr | 74 +++++++ 12 files changed, 465 insertions(+), 32 deletions(-) create mode 100644 newsfragments/4202.added.md create mode 100644 tests/test_class_comparisons.rs diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 1756e8dfb35..a3a4e1f0c7d 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -15,6 +15,7 @@ | `mapping` | Inform PyO3 that this class is a [`Mapping`][params-mapping], and so leave its implementation of sequence C-API slots empty. | | `module = "module_name"` | Python code will see the class as being defined in this module. Defaults to `builtins`. | | `name = "python_name"` | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. | +| `ord` | Implements `__lt__`, `__gt__`, `__le__`, & `__ge__` using the `PartialOrd` implementation of the underlying Rust datatype. *Requires `eq`* | | `rename_all = "renaming_rule"` | Applies renaming rules to every getters and setters of a struct, or every variants of an enum. Possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE". | | `sequence` | Inform PyO3 that this class is a [`Sequence`][params-sequence], and so leave its C-API mapping length slot empty. | | `set_all` | Generates setters for all fields of the pyclass. | diff --git a/guide/src/class.md b/guide/src/class.md index b72cae34e25..2bcfe75911e 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1160,6 +1160,32 @@ Python::with_gil(|py| { }) ``` +Ordering of enum variants is optionally added using `#[pyo3(ord)]`. +*Note: Implementation of the `PartialOrd` trait is required when passing the `ord` argument. If not implemented, a compile time error is raised.* + +```rust +# use pyo3::prelude::*; +#[pyclass(eq, ord)] +#[derive(PartialEq, PartialOrd)] +enum MyEnum{ + A, + B, + C, +} + +Python::with_gil(|py| { + let cls = py.get_type_bound::(); + let a = Py::new(py, MyEnum::A).unwrap(); + let b = Py::new(py, MyEnum::B).unwrap(); + let c = Py::new(py, MyEnum::C).unwrap(); + pyo3::py_run!(py, cls a b c, r#" + assert (a < b) == True + assert (c <= b) == False + assert (c > a) == True + "#) +}) +``` + You may not use enums as a base class or let enums inherit from other classes. ```rust,compile_fail diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 3b775c2b438..c0d25cd0597 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -249,6 +249,16 @@ To implement `__eq__` using the Rust [`PartialEq`] trait implementation, the `eq struct Number(i32); ``` +To implement `__lt__`, `__le__`, `__gt__`, & `__ge__` using the Rust `PartialOrd` trait implementation, the `ord` option can be used. *Note: Requires `eq`.* + +```rust +# use pyo3::prelude::*; +# +#[pyclass(eq, ord)] +#[derive(PartialEq, PartialOrd)] +struct Number(i32); +``` + ### Truthyness We'll consider `Number` to be `True` if it is nonzero: diff --git a/newsfragments/4202.added.md b/newsfragments/4202.added.md new file mode 100644 index 00000000000..d15b6ce810c --- /dev/null +++ b/newsfragments/4202.added.md @@ -0,0 +1 @@ +Add `#[pyclass(ord)]` to implement ordering based on `PartialOrd`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 52479552b34..02af17b618b 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -29,6 +29,7 @@ pub mod kw { syn::custom_keyword!(mapping); syn::custom_keyword!(module); syn::custom_keyword!(name); + syn::custom_keyword!(ord); syn::custom_keyword!(pass_module); syn::custom_keyword!(rename_all); syn::custom_keyword!(sequence); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 717fdfb3dea..403e5e8e9dc 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -70,6 +70,7 @@ pub struct PyClassPyO3Options { pub mapping: Option, pub module: Option, pub name: Option, + pub ord: Option, pub rename_all: Option, pub sequence: Option, pub set_all: Option, @@ -91,6 +92,7 @@ pub enum PyClassPyO3Option { Mapping(kw::mapping), Module(ModuleAttribute), Name(NameAttribute), + Ord(kw::ord), RenameAll(RenameAllAttribute), Sequence(kw::sequence), SetAll(kw::set_all), @@ -126,6 +128,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Module) } else if lookahead.peek(kw::name) { input.parse().map(PyClassPyO3Option::Name) + } else if lookahead.peek(attributes::kw::ord) { + input.parse().map(PyClassPyO3Option::Ord) } else if lookahead.peek(kw::rename_all) { input.parse().map(PyClassPyO3Option::RenameAll) } else if lookahead.peek(attributes::kw::sequence) { @@ -189,6 +193,7 @@ impl PyClassPyO3Options { PyClassPyO3Option::Mapping(mapping) => set_option!(mapping), PyClassPyO3Option::Module(module) => set_option!(module), PyClassPyO3Option::Name(name) => set_option!(name), + PyClassPyO3Option::Ord(ord) => set_option!(ord), PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all), PyClassPyO3Option::Sequence(sequence) => set_option!(sequence), PyClassPyO3Option::SetAll(set_all) => set_option!(set_all), @@ -1665,7 +1670,10 @@ fn impl_pytypeinfo( } } -fn pyclass_richcmp_arms(options: &PyClassPyO3Options, ctx: &Ctx) -> TokenStream { +fn pyclass_richcmp_arms( + options: &PyClassPyO3Options, + ctx: &Ctx, +) -> std::result::Result { let Ctx { pyo3_path } = ctx; let eq_arms = options @@ -1684,9 +1692,34 @@ fn pyclass_richcmp_arms(options: &PyClassPyO3Options, ctx: &Ctx) -> TokenStream }) .unwrap_or_default(); - // TODO: `ord` can be integrated here (#4202) - #[allow(clippy::let_and_return)] - eq_arms + if let Some(ord) = options.ord { + ensure_spanned!(options.eq.is_some(), ord.span() => "The `ord` option requires the `eq` option."); + } + + let ord_arms = options + .ord + .map(|ord| { + quote_spanned! { ord.span() => + #pyo3_path::pyclass::CompareOp::Gt => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val > other, py)) + }, + #pyo3_path::pyclass::CompareOp::Lt => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val < other, py)) + }, + #pyo3_path::pyclass::CompareOp::Le => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val <= other, py)) + }, + #pyo3_path::pyclass::CompareOp::Ge => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val >= other, py)) + }, + } + }) + .unwrap_or_else(|| quote! { _ => ::std::result::Result::Ok(py.NotImplemented()) }); + + Ok(quote! { + #eq_arms + #ord_arms + }) } fn pyclass_richcmp_simple_enum( @@ -1723,7 +1756,7 @@ fn pyclass_richcmp_simple_enum( return Ok((None, None)); } - let arms = pyclass_richcmp_arms(&options, ctx); + let arms = pyclass_richcmp_arms(&options, ctx)?; let eq = options.eq.map(|eq| { quote_spanned! { eq.span() => @@ -1732,7 +1765,6 @@ fn pyclass_richcmp_simple_enum( let other = &*other.borrow(); return match op { #arms - _ => ::std::result::Result::Ok(py.NotImplemented()) } } } @@ -1746,7 +1778,6 @@ fn pyclass_richcmp_simple_enum( }) { return match op { #arms - _ => ::std::result::Result::Ok(py.NotImplemented()) } } } @@ -1786,7 +1817,7 @@ fn pyclass_richcmp( bail_spanned!(eq_int.span() => "`eq_int` can only be used on simple enums.") } - let arms = pyclass_richcmp_arms(options, ctx); + let arms = pyclass_richcmp_arms(options, ctx)?; if options.eq.is_some() { let mut richcmp_impl = parse_quote! { fn __pyo3__generated____richcmp__( @@ -1799,7 +1830,6 @@ fn pyclass_richcmp( let other = &*#pyo3_path::types::PyAnyMethods::downcast::(other)?.borrow(); match op { #arms - _ => ::std::result::Result::Ok(py.NotImplemented()) } } }; diff --git a/tests/test_class_comparisons.rs b/tests/test_class_comparisons.rs new file mode 100644 index 00000000000..75bd6251aac --- /dev/null +++ b/tests/test_class_comparisons.rs @@ -0,0 +1,287 @@ +#![cfg(feature = "macros")] + +use pyo3::prelude::*; + +#[path = "../src/tests/common.rs"] +mod common; + +#[pyclass(eq)] +#[derive(Debug, Clone, PartialEq)] +pub enum MyEnum { + Variant, + OtherVariant, +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub enum MyEnumOrd { + Variant, + OtherVariant, +} + +#[test] +fn test_enum_eq_enum() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + let var2 = Py::new(py, MyEnum::Variant).unwrap(); + let other_var = Py::new(py, MyEnum::OtherVariant).unwrap(); + py_assert!(py, var1 var2, "var1 == var2"); + py_assert!(py, var1 other_var, "var1 != other_var"); + py_assert!(py, var1 var2, "(var1 != var2) == False"); + }) +} + +#[test] +fn test_enum_eq_incomparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + py_assert!(py, var1, "(var1 == 'foo') == False"); + py_assert!(py, var1, "(var1 != 'foo') == True"); + }) +} + +#[test] +fn test_enum_ord_comparable_opt_in_only() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + let var2 = Py::new(py, MyEnum::OtherVariant).unwrap(); + // ordering on simple enums if opt in only, thus raising an error below + py_expect_exception!(py, var1 var2, "(var1 > var2) == False", PyTypeError); + }) +} + +#[test] +fn test_simple_enum_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnumOrd::Variant).unwrap(); + let var2 = Py::new(py, MyEnumOrd::OtherVariant).unwrap(); + let var3 = Py::new(py, MyEnumOrd::OtherVariant).unwrap(); + py_assert!(py, var1 var2, "(var1 > var2) == False"); + py_assert!(py, var1 var2, "(var1 < var2) == True"); + py_assert!(py, var1 var2, "(var1 >= var2) == False"); + py_assert!(py, var2 var3, "(var3 >= var2) == True"); + py_assert!(py, var1 var2, "(var1 <= var2) == True"); + }) +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub enum MyComplexEnumOrd { + Variant(i32), + OtherVariant(String), +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub enum MyComplexEnumOrd2 { + Variant { msg: String, idx: u32 }, + OtherVariant { name: String, idx: u32 }, +} + +#[test] +fn test_complex_enum_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyComplexEnumOrd::Variant(-2)).unwrap(); + let var2 = Py::new(py, MyComplexEnumOrd::Variant(5)).unwrap(); + let var3 = Py::new(py, MyComplexEnumOrd::OtherVariant("a".to_string())).unwrap(); + let var4 = Py::new(py, MyComplexEnumOrd::OtherVariant("b".to_string())).unwrap(); + py_assert!(py, var1 var2, "(var1 > var2) == False"); + py_assert!(py, var1 var2, "(var1 < var2) == True"); + py_assert!(py, var1 var2, "(var1 >= var2) == False"); + py_assert!(py, var1 var2, "(var1 <= var2) == True"); + + py_assert!(py, var1 var3, "(var1 >= var3) == False"); + py_assert!(py, var1 var3, "(var1 <= var3) == True"); + + py_assert!(py, var3 var4, "(var3 >= var4) == False"); + py_assert!(py, var3 var4, "(var3 <= var4) == True"); + + let var5 = Py::new( + py, + MyComplexEnumOrd2::Variant { + msg: "hello".to_string(), + idx: 1, + }, + ) + .unwrap(); + let var6 = Py::new( + py, + MyComplexEnumOrd2::Variant { + msg: "hello".to_string(), + idx: 1, + }, + ) + .unwrap(); + let var7 = Py::new( + py, + MyComplexEnumOrd2::Variant { + msg: "goodbye".to_string(), + idx: 7, + }, + ) + .unwrap(); + let var8 = Py::new( + py, + MyComplexEnumOrd2::Variant { + msg: "about".to_string(), + idx: 0, + }, + ) + .unwrap(); + let var9 = Py::new( + py, + MyComplexEnumOrd2::OtherVariant { + name: "albert".to_string(), + idx: 1, + }, + ) + .unwrap(); + + py_assert!(py, var5 var6, "(var5 == var6) == True"); + py_assert!(py, var5 var6, "(var5 <= var6) == True"); + py_assert!(py, var6 var7, "(var6 <= var7) == False"); + py_assert!(py, var6 var7, "(var6 >= var7) == True"); + py_assert!(py, var5 var8, "(var5 > var8) == True"); + py_assert!(py, var8 var9, "(var9 > var8) == True"); + }) +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub struct Point { + x: i32, + y: i32, + z: i32, +} + +#[test] +fn test_struct_numeric_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, Point { x: 10, y: 2, z: 3 }).unwrap(); + let var2 = Py::new(py, Point { x: 2, y: 2, z: 3 }).unwrap(); + let var3 = Py::new(py, Point { x: 1, y: 22, z: 4 }).unwrap(); + let var4 = Py::new(py, Point { x: 1, y: 3, z: 4 }).unwrap(); + let var5 = Py::new(py, Point { x: 1, y: 3, z: 4 }).unwrap(); + py_assert!(py, var1 var2, "(var1 > var2) == True"); + py_assert!(py, var1 var2, "(var1 <= var2) == False"); + py_assert!(py, var2 var3, "(var3 < var2) == True"); + py_assert!(py, var3 var4, "(var3 > var4) == True"); + py_assert!(py, var4 var5, "(var4 == var5) == True"); + py_assert!(py, var3 var5, "(var3 != var5) == True"); + }) +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub struct Person { + surname: String, + given_name: String, +} + +#[test] +fn test_struct_string_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new( + py, + Person { + surname: "zzz".to_string(), + given_name: "bob".to_string(), + }, + ) + .unwrap(); + let var2 = Py::new( + py, + Person { + surname: "aaa".to_string(), + given_name: "sally".to_string(), + }, + ) + .unwrap(); + let var3 = Py::new( + py, + Person { + surname: "eee".to_string(), + given_name: "qqq".to_string(), + }, + ) + .unwrap(); + let var4 = Py::new( + py, + Person { + surname: "ddd".to_string(), + given_name: "aaa".to_string(), + }, + ) + .unwrap(); + + py_assert!(py, var1 var2, "(var1 > var2) == True"); + py_assert!(py, var1 var2, "(var1 <= var2) == False"); + py_assert!(py, var1 var3, "(var1 >= var3) == True"); + py_assert!(py, var2 var3, "(var2 >= var3) == False"); + py_assert!(py, var3 var4, "(var3 >= var4) == True"); + py_assert!(py, var3 var4, "(var3 != var4) == True"); + }) +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Record { + name: String, + title: String, + idx: u32, +} + +impl PartialOrd for Record { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.idx.partial_cmp(&other.idx).unwrap()) + } +} + +#[test] +fn test_struct_custom_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new( + py, + Record { + name: "zzz".to_string(), + title: "bbb".to_string(), + idx: 9, + }, + ) + .unwrap(); + let var2 = Py::new( + py, + Record { + name: "ddd".to_string(), + title: "aaa".to_string(), + idx: 1, + }, + ) + .unwrap(); + let var3 = Py::new( + py, + Record { + name: "vvv".to_string(), + title: "ggg".to_string(), + idx: 19, + }, + ) + .unwrap(); + let var4 = Py::new( + py, + Record { + name: "vvv".to_string(), + title: "ggg".to_string(), + idx: 19, + }, + ) + .unwrap(); + + py_assert!(py, var1 var2, "(var1 > var2) == True"); + py_assert!(py, var1 var2, "(var1 <= var2) == False"); + py_assert!(py, var1 var3, "(var1 >= var3) == False"); + py_assert!(py, var2 var3, "(var2 >= var3) == False"); + py_assert!(py, var3 var4, "(var3 == var4) == True"); + py_assert!(py, var2 var4, "(var2 != var4) == True"); + }) +} diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 4b72143539f..96a07c3fe41 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -52,27 +52,6 @@ fn test_enum_arg() { }) } -#[test] -fn test_enum_eq_enum() { - Python::with_gil(|py| { - let var1 = Py::new(py, MyEnum::Variant).unwrap(); - let var2 = Py::new(py, MyEnum::Variant).unwrap(); - let other_var = Py::new(py, MyEnum::OtherVariant).unwrap(); - py_assert!(py, var1 var2, "var1 == var2"); - py_assert!(py, var1 other_var, "var1 != other_var"); - py_assert!(py, var1 var2, "(var1 != var2) == False"); - }) -} - -#[test] -fn test_enum_eq_incomparable() { - Python::with_gil(|py| { - let var1 = Py::new(py, MyEnum::Variant).unwrap(); - py_assert!(py, var1, "(var1 == 'foo') == False"); - py_assert!(py, var1, "(var1 != 'foo') == True"); - }) -} - #[pyclass(eq, eq_int)] #[derive(Debug, PartialEq, Eq, Clone)] enum CustomDiscriminant { diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index 24842eb484a..f74fa49d8de 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -71,4 +71,9 @@ impl HashOptAndManualHash { } } +#[pyclass(ord)] +struct InvalidOrderedStruct { + inner: i32 +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 8f1b671dfd9..23d3c3bbc64 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`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `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`, `hash`, `mapping`, `module`, `name`, `ord`, `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`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `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`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:27:11 | 27 | #[pyclass(weakrev)] @@ -76,6 +76,12 @@ error: The `hash` option requires the `eq` option. 59 | #[pyclass(hash)] | ^^^^ +error: The `ord` option requires the `eq` option. + --> tests/ui/invalid_pyclass_args.rs:74:11 + | +74 | #[pyclass(ord)] + | ^^^ + error[E0592]: duplicate definitions with name `__pymethod___richcmp____` --> tests/ui/invalid_pyclass_args.rs:36:1 | diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index 73bc992571a..c490f9291e3 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -80,4 +80,17 @@ enum ComplexHashOptRequiresEq { B { msg: String }, } +#[pyclass(ord)] +enum InvalidOrderedComplexEnum { + VariantA (i32), + VariantB { msg: String } +} + +#[pyclass(eq,ord)] +#[derive(PartialEq)] +enum InvalidOrderedComplexEnum2 { + VariantA (i32), + VariantB { msg: String } +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index cfa3922ef62..98ca2d77bfa 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -60,6 +60,12 @@ error: The `hash` option requires the `eq` option. 76 | #[pyclass(hash)] | ^^^^ +error: The `ord` option requires the `eq` option. + --> tests/ui/invalid_pyclass_enum.rs:83:11 + | +83 | #[pyclass(ord)] + | ^^^ + error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq` --> tests/ui/invalid_pyclass_enum.rs:31:11 | @@ -151,3 +157,71 @@ help: consider annotating `ComplexHashOptRequiresHash` with `#[derive(Hash)]` 64 + #[derive(Hash)] 65 | enum ComplexHashOptRequiresHash { | + +error[E0369]: binary operation `>` cannot be applied to type `&InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:89:14 + | +89 | #[pyclass(eq,ord)] + | ^^^ + | +note: an implementation of `PartialOrd` might be missing for `InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:91:1 + | +91 | enum InvalidOrderedComplexEnum2 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd` +help: consider annotating `InvalidOrderedComplexEnum2` with `#[derive(PartialEq, PartialOrd)]` + | +91 + #[derive(PartialEq, PartialOrd)] +92 | enum InvalidOrderedComplexEnum2 { + | + +error[E0369]: binary operation `<` cannot be applied to type `&InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:89:14 + | +89 | #[pyclass(eq,ord)] + | ^^^ + | +note: an implementation of `PartialOrd` might be missing for `InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:91:1 + | +91 | enum InvalidOrderedComplexEnum2 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd` +help: consider annotating `InvalidOrderedComplexEnum2` with `#[derive(PartialEq, PartialOrd)]` + | +91 + #[derive(PartialEq, PartialOrd)] +92 | enum InvalidOrderedComplexEnum2 { + | + +error[E0369]: binary operation `<=` cannot be applied to type `&InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:89:14 + | +89 | #[pyclass(eq,ord)] + | ^^^ + | +note: an implementation of `PartialOrd` might be missing for `InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:91:1 + | +91 | enum InvalidOrderedComplexEnum2 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd` +help: consider annotating `InvalidOrderedComplexEnum2` with `#[derive(PartialEq, PartialOrd)]` + | +91 + #[derive(PartialEq, PartialOrd)] +92 | enum InvalidOrderedComplexEnum2 { + | + +error[E0369]: binary operation `>=` cannot be applied to type `&InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:89:14 + | +89 | #[pyclass(eq,ord)] + | ^^^ + | +note: an implementation of `PartialOrd` might be missing for `InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:91:1 + | +91 | enum InvalidOrderedComplexEnum2 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd` +help: consider annotating `InvalidOrderedComplexEnum2` with `#[derive(PartialEq, PartialOrd)]` + | +91 + #[derive(PartialEq, PartialOrd)] +92 | enum InvalidOrderedComplexEnum2 { + |