Skip to content

Commit

Permalink
add pyclass eq option (#4210)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Icxolu and davidhewitt authored May 31, 2024
1 parent cb34737 commit d1a7cf4
Show file tree
Hide file tree
Showing 19 changed files with 501 additions and 72 deletions.
2 changes: 2 additions & 0 deletions guide/pyclass-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
| <span style="white-space: pre">`crate = "some::path"`</span> | 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. |
| <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][params-1] |
| <span style="white-space: pre">`freelist = N`</span> | 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. |
| <span style="white-space: pre">`frozen`</span> | 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. |
Expand Down
23 changes: 14 additions & 9 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
"#)
})
```
Expand All @@ -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,
Expand All @@ -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,
}
Expand All @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions guide/src/class/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
34 changes: 34 additions & 0 deletions guide/src/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> Drop for Py<T>`. 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.
</details>

### Require explicit opt-in for comparison for simple enums
<details open>
<summary><small>Click to expand</small></summary>

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,
}
```
</details>

## from 0.20.* to 0.21
<details open>
<summary><small>Click to expand</small></summary>
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/4210.added.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions newsfragments/4210.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate implicit integer comparision for simple enums in favor of `#[pyclass(eq_int)]`.
2 changes: 2 additions & 0 deletions pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit d1a7cf4

Please sign in to comment.