Skip to content

Commit ada3017

Browse files
authored
Merge pull request #2034 from b05902132/derive-enum
#[pyclass] for fieldless enums
2 parents 4442ff7 + 78f5afc commit ada3017

File tree

7 files changed

+374
-53
lines changed

7 files changed

+374
-53
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222

2323
- Add `Py::setattr` method. [#2009](https://github.com/PyO3/pyo3/pull/2009)
2424
- Add `PyCapsule`, exposing the [Capsule API](https://docs.python.org/3/c-api/capsule.html#capsules). [#1980](https://github.com/PyO3/pyo3/pull/1980)
25-
- All PyO3 proc-macros except the deprecated `#[pyproto]` now accept a supplemental attribute `#[pyo3(crate = "some::path")]` that specifies
26-
where to find the `pyo3` crate, in case it has been renamed or is re-exported and not found at the crate root. [#2022](https://github.com/PyO3/pyo3/pull/2022)
2725
- Expose `pyo3-build-config` APIs for cross-compiling and Python configuration discovery for use in other projects. [#1996](https://github.com/PyO3/pyo3/pull/1996)
26+
- All PyO3 proc-macros except the deprecated `#[pyproto]` now accept a supplemental attribute `#[pyo3(crate = "some::path")]` that specifies where to find the `pyo3` crate, in case it has been renamed or is re-exported and not found at the crate root. [#2022](https://github.com/PyO3/pyo3/pull/2022)
27+
- Enable `#[pyclass]` for fieldless (aka C-like) enums. [#2034](https://github.com/PyO3/pyo3/pull/2034)
2828
- Add buffer magic methods `__getbuffer__` and `__releasebuffer__` to `#[pymethods]`. [#2067](https://github.com/PyO3/pyo3/pull/2067)
2929
- Accept paths in `wrap_pyfunction` and `wrap_pymodule`. [#2081](https://github.com/PyO3/pyo3/pull/2081)
3030
- Add check for correct number of arguments on magic methods. [#2083](https://github.com/PyO3/pyo3/pull/2083)

guide/src/class.md

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
PyO3 exposes a group of attributes powered by Rust's proc macro system for defining Python classes as Rust structs.
44

5-
The main attribute is `#[pyclass]`, which is placed upon a Rust `struct` to generate a Python type for it. A struct will usually also have *one* `#[pymethods]`-annotated `impl` block for the struct, which is used to define Python methods and constants for the generated Python type. (If the [`multiple-pymethods`] feature is enabled each `#[pyclass]` is allowed to have multiple `#[pymethods]` blocks.) Finally, there may be multiple `#[pyproto]` trait implementations for the struct, which are used to define certain python magic methods such as `__str__`.
5+
The main attribute is `#[pyclass]`, which is placed upon a Rust `struct` or a fieldless `enum` (a.k.a. C-like enum) to generate a Python type for it. They will usually also have *one* `#[pymethods]`-annotated `impl` block for the struct, which is used to define Python methods and constants for the generated Python type. (If the [`multiple-pymethods`] feature is enabled each `#[pyclass]` is allowed to have multiple `#[pymethods]` blocks.) Finally, there may be multiple `#[pyproto]` trait implementations for the struct, which are used to define certain python magic methods such as `__str__`.
66

77
This chapter will discuss the functionality and configuration these attributes offer. Below is a list of links to the relevant section of this chapter for each:
88

@@ -20,9 +20,7 @@ This chapter will discuss the functionality and configuration these attributes o
2020

2121
## Defining a new class
2222

23-
To define a custom Python class, a Rust struct needs to be annotated with the
24-
`#[pyclass]` attribute.
25-
23+
To define a custom Python class, add the `#[pyclass]` attribute to a Rust struct or a fieldless enum.
2624
```rust
2725
# #![allow(dead_code)]
2826
# use pyo3::prelude::*;
@@ -31,11 +29,17 @@ struct MyClass {
3129
# #[pyo3(get)]
3230
num: i32,
3331
}
32+
33+
#[pyclass]
34+
enum MyEnum {
35+
Variant,
36+
OtherVariant = 30, // PyO3 supports custom discriminants.
37+
}
3438
```
3539

36-
Because Python objects are freely shared between threads by the Python interpreter, all structs annotated with `#[pyclass]` must implement `Send` (unless annotated with [`#[pyclass(unsendable)]`](#customizing-the-class)).
40+
Because Python objects are freely shared between threads by the Python interpreter, all types annotated with `#[pyclass]` must implement `Send` (unless annotated with [`#[pyclass(unsendable)]`](#customizing-the-class)).
3741

38-
The above example generates implementations for [`PyTypeInfo`], [`PyTypeObject`], and [`PyClass`] for `MyClass`. To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter.
42+
The above example generates implementations for [`PyTypeInfo`], [`PyTypeObject`], and [`PyClass`] for `MyClass` and `MyEnum`. To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter.
3943

4044
## Adding the class to a module
4145

@@ -140,8 +144,8 @@ so that they can benefit from a freelist. `XXX` is a number of items for the fre
140144
* `gc` - Classes with the `gc` parameter participate in Python garbage collection.
141145
If a custom class contains references to other Python objects that can be collected, the [`PyGCProtocol`]({{#PYO3_DOCS_URL}}/pyo3/class/gc/trait.PyGCProtocol.html) trait has to be implemented.
142146
* `weakref` - Adds support for Python weak references.
143-
* `extends=BaseType` - Use a custom base class. The base `BaseType` must implement `PyTypeInfo`.
144-
* `subclass` - Allows Python classes to inherit from this class.
147+
* `extends=BaseType` - Use a custom base class. The base `BaseType` must implement `PyTypeInfo`. `enum` pyclasses can't use a custom base class.
148+
* `subclass` - Allows Python classes to inherit from this class. `enum` pyclasses can't be inherited from.
145149
* `dict` - Adds `__dict__` support, so that the instances of this type have a dictionary containing arbitrary instance variables.
146150
* `unsendable` - Making it safe to expose `!Send` structs to Python, where all object can be accessed
147151
by multiple threads. A class marked with `unsendable` panics when accessed by another thread.
@@ -351,7 +355,7 @@ impl SubClass {
351355
## Object properties
352356

353357
PyO3 supports two ways to add properties to your `#[pyclass]`:
354-
- For simple fields with no side effects, a `#[pyo3(get, set)]` attribute can be added directly to the field definition in the `#[pyclass]`.
358+
- For simple struct fields with no side effects, a `#[pyo3(get, set)]` attribute can be added directly to the field definition in the `#[pyclass]`.
355359
- For properties which require computation you can define `#[getter]` and `#[setter]` functions in the [`#[pymethods]`](#instance-methods) block.
356360

357361
We'll cover each of these in the following sections.
@@ -802,6 +806,118 @@ impl MyClass {
802806
Note that `text_signature` on classes is not compatible with compilation in
803807
`abi3` mode until Python 3.10 or greater.
804808

809+
## #[pyclass] enums
810+
811+
Currently PyO3 only supports fieldless enums. PyO3 adds a class attribute for each variant, so you can access them in Python without defining `#[new]`. PyO3 also provides default implementations of `__richcmp__` and `__int__`, so they can be compared using `==`:
812+
813+
```rust
814+
# use pyo3::prelude::*;
815+
#[pyclass]
816+
enum MyEnum {
817+
Variant,
818+
OtherVariant,
819+
}
820+
821+
Python::with_gil(|py| {
822+
let x = Py::new(py, MyEnum::Variant).unwrap();
823+
let y = Py::new(py, MyEnum::OtherVariant).unwrap();
824+
let cls = py.get_type::<MyEnum>();
825+
pyo3::py_run!(py, x y cls, r#"
826+
assert x == cls.Variant
827+
assert y == cls.OtherVariant
828+
assert x != y
829+
"#)
830+
})
831+
```
832+
833+
You can also convert your enums into `int`:
834+
835+
```rust
836+
# use pyo3::prelude::*;
837+
#[pyclass]
838+
enum MyEnum {
839+
Variant,
840+
OtherVariant = 10,
841+
}
842+
843+
Python::with_gil(|py| {
844+
let cls = py.get_type::<MyEnum>();
845+
let x = MyEnum::Variant as i32; // The exact value is assigned by the compiler.
846+
pyo3::py_run!(py, cls x, r#"
847+
assert int(cls.Variant) == x
848+
assert int(cls.OtherVariant) == 10
849+
assert cls.OtherVariant == 10 # You can also compare against int.
850+
assert 10 == cls.OtherVariant
851+
"#)
852+
})
853+
```
854+
855+
PyO3 also provides `__repr__` for enums:
856+
857+
```rust
858+
# use pyo3::prelude::*;
859+
#[pyclass]
860+
enum MyEnum{
861+
Variant,
862+
OtherVariant,
863+
}
864+
865+
Python::with_gil(|py| {
866+
let cls = py.get_type::<MyEnum>();
867+
let x = Py::new(py, MyEnum::Variant).unwrap();
868+
pyo3::py_run!(py, cls x, r#"
869+
assert repr(x) == 'MyEnum.Variant'
870+
assert repr(cls.OtherVariant) == 'MyEnum.OtherVariant'
871+
"#)
872+
})
873+
```
874+
875+
All methods defined by PyO3 can be overriden. For example here's how you override `__repr__`:
876+
877+
```rust
878+
# use pyo3::prelude::*;
879+
#[pyclass]
880+
enum MyEnum {
881+
Answer = 42,
882+
}
883+
884+
#[pymethods]
885+
impl MyEnum {
886+
fn __repr__(&self) -> &'static str {
887+
"42"
888+
}
889+
}
890+
891+
Python::with_gil(|py| {
892+
let cls = py.get_type::<MyEnum>();
893+
pyo3::py_run!(py, cls, "assert repr(cls.Answer) == '42'")
894+
})
895+
```
896+
897+
You may not use enums as a base class or let enums inherit from other classes.
898+
899+
```rust,compile_fail
900+
# use pyo3::prelude::*;
901+
#[pyclass(subclass)]
902+
enum BadBase{
903+
Var1,
904+
}
905+
```
906+
907+
```rust,compile_fail
908+
# use pyo3::prelude::*;
909+
910+
#[pyclass(subclass)]
911+
struct Base;
912+
913+
#[pyclass(extends=Base)]
914+
enum BadSubclass{
915+
Var1,
916+
}
917+
```
918+
919+
`#[pyclass]` enums are currently not interoperable with `IntEnum` in Python.
920+
805921
## Implementation details
806922

807923
The `#[pyclass]` macros rely on a lot of conditional code generation: each `#[pyclass]` can optionally have a `#[pymethods]` block as well as several different possible `#[pyproto]` trait implementations.

pyo3-macros-backend/src/pyclass.rs

Lines changed: 126 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,54 @@ struct PyClassEnumVariant<'a> {
407407
/* currently have no more options */
408408
}
409409

410+
struct PyClassEnum<'a> {
411+
ident: &'a syn::Ident,
412+
// The underlying #[repr] of the enum, used to implement __int__ and __richcmp__.
413+
// This matters when the underlying representation may not fit in `isize`.
414+
repr_type: syn::Ident,
415+
variants: Vec<PyClassEnumVariant<'a>>,
416+
}
417+
418+
impl<'a> PyClassEnum<'a> {
419+
fn new(enum_: &'a syn::ItemEnum) -> syn::Result<Self> {
420+
fn is_numeric_type(t: &syn::Ident) -> bool {
421+
[
422+
"u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "u128", "i128", "usize",
423+
"isize",
424+
]
425+
.iter()
426+
.any(|&s| t == s)
427+
}
428+
let ident = &enum_.ident;
429+
// According to the [reference](https://doc.rust-lang.org/reference/items/enumerations.html),
430+
// "Under the default representation, the specified discriminant is interpreted as an isize
431+
// value", so `isize` should be enough by default.
432+
let mut repr_type = syn::Ident::new("isize", proc_macro2::Span::call_site());
433+
if let Some(attr) = enum_.attrs.iter().find(|attr| attr.path.is_ident("repr")) {
434+
let args =
435+
attr.parse_args_with(Punctuated::<TokenStream, Token![!]>::parse_terminated)?;
436+
if let Some(ident) = args
437+
.into_iter()
438+
.filter_map(|ts| syn::parse2::<syn::Ident>(ts).ok())
439+
.find(is_numeric_type)
440+
{
441+
repr_type = ident;
442+
}
443+
}
444+
445+
let variants = enum_
446+
.variants
447+
.iter()
448+
.map(extract_variant_data)
449+
.collect::<syn::Result<_>>()?;
450+
Ok(Self {
451+
ident,
452+
repr_type,
453+
variants,
454+
})
455+
}
456+
}
457+
410458
pub fn build_py_enum(
411459
enum_: &mut syn::ItemEnum,
412460
args: &PyClassArgs,
@@ -417,41 +465,37 @@ pub fn build_py_enum(
417465
if enum_.variants.is_empty() {
418466
bail_spanned!(enum_.brace_token.span => "Empty enums can't be #[pyclass].");
419467
}
420-
let variants: Vec<PyClassEnumVariant> = enum_
421-
.variants
422-
.iter()
423-
.map(extract_variant_data)
424-
.collect::<syn::Result<_>>()?;
425-
impl_enum(enum_, args, variants, method_type, options)
426-
}
427-
428-
fn impl_enum(
429-
enum_: &syn::ItemEnum,
430-
args: &PyClassArgs,
431-
variants: Vec<PyClassEnumVariant>,
432-
methods_type: PyClassMethodsType,
433-
options: PyClassPyO3Options,
434-
) -> syn::Result<TokenStream> {
435-
let enum_name = &enum_.ident;
436468
let doc = utils::get_doc(
437469
&enum_.attrs,
438470
options
439471
.text_signature
440472
.as_ref()
441473
.map(|attr| (get_class_python_name(&enum_.ident, args), attr)),
442474
);
475+
let enum_ = PyClassEnum::new(enum_)?;
476+
impl_enum(enum_, args, doc, method_type, options)
477+
}
478+
479+
fn impl_enum(
480+
enum_: PyClassEnum,
481+
args: &PyClassArgs,
482+
doc: PythonDoc,
483+
methods_type: PyClassMethodsType,
484+
options: PyClassPyO3Options,
485+
) -> syn::Result<TokenStream> {
443486
let krate = get_pyo3_crate(&options.krate);
444-
impl_enum_class(enum_name, args, variants, doc, methods_type, krate)
487+
impl_enum_class(enum_, args, doc, methods_type, krate)
445488
}
446489

447490
fn impl_enum_class(
448-
cls: &syn::Ident,
491+
enum_: PyClassEnum,
449492
args: &PyClassArgs,
450-
variants: Vec<PyClassEnumVariant>,
451493
doc: PythonDoc,
452494
methods_type: PyClassMethodsType,
453495
krate: syn::Path,
454496
) -> syn::Result<TokenStream> {
497+
let cls = enum_.ident;
498+
let variants = enum_.variants;
455499
let pytypeinfo = impl_pytypeinfo(cls, args, None);
456500
let pyclass_impls = PyClassImplsBuilder::new(
457501
cls,
@@ -476,13 +520,73 @@ fn impl_enum_class(
476520
fn __pyo3__repr__(&self) -> &'static str {
477521
match self {
478522
#(#variants_repr)*
479-
_ => unreachable!("Unsupported variant type."),
480523
}
481524
}
482525
}
483526
};
484527

485-
let default_impls = gen_default_items(cls, vec![default_repr_impl]);
528+
let repr_type = &enum_.repr_type;
529+
530+
let default_int = {
531+
// This implementation allows us to convert &T to #repr_type without implementing `Copy`
532+
let variants_to_int = variants.iter().map(|variant| {
533+
let variant_name = variant.ident;
534+
quote! { #cls::#variant_name => #cls::#variant_name as #repr_type, }
535+
});
536+
quote! {
537+
#[doc(hidden)]
538+
#[allow(non_snake_case)]
539+
#[pyo3(name = "__int__")]
540+
fn __pyo3__int__(&self) -> #repr_type {
541+
match self {
542+
#(#variants_to_int)*
543+
}
544+
}
545+
}
546+
};
547+
548+
let default_richcmp = {
549+
let variants_eq = variants.iter().map(|variant| {
550+
let variant_name = variant.ident;
551+
quote! {
552+
(#cls::#variant_name, #cls::#variant_name) =>
553+
Ok(true.to_object(py)),
554+
}
555+
});
556+
quote! {
557+
#[doc(hidden)]
558+
#[allow(non_snake_case)]
559+
#[pyo3(name = "__richcmp__")]
560+
fn __pyo3__richcmp__(
561+
&self,
562+
py: _pyo3::Python,
563+
other: &_pyo3::PyAny,
564+
op: _pyo3::basic::CompareOp
565+
) -> _pyo3::PyResult<_pyo3::PyObject> {
566+
use _pyo3::conversion::ToPyObject;
567+
use ::core::result::Result::*;
568+
match op {
569+
_pyo3::basic::CompareOp::Eq => {
570+
if let Ok(i) = other.extract::<#repr_type>() {
571+
let self_val = self.__pyo3__int__();
572+
return Ok((self_val == i).to_object(py));
573+
}
574+
let other = other.extract::<_pyo3::PyRef<Self>>()?;
575+
let other = &*other;
576+
match (self, other) {
577+
#(#variants_eq)*
578+
_ => Ok(false.to_object(py)),
579+
}
580+
}
581+
_ => Ok(py.NotImplemented()),
582+
}
583+
}
584+
}
585+
};
586+
587+
let default_items =
588+
gen_default_items(cls, vec![default_repr_impl, default_richcmp, default_int]);
589+
486590
Ok(quote! {
487591
const _: () = {
488592
use #krate as _pyo3;
@@ -491,7 +595,7 @@ fn impl_enum_class(
491595

492596
#pyclass_impls
493597

494-
#default_impls
598+
#default_items
495599
};
496600
})
497601
}
@@ -527,9 +631,6 @@ fn extract_variant_data(variant: &syn::Variant) -> syn::Result<PyClassEnumVarian
527631
Fields::Unit => &variant.ident,
528632
_ => bail_spanned!(variant.span() => "Currently only support unit variants."),
529633
};
530-
if let Some(discriminant) = variant.discriminant.as_ref() {
531-
bail_spanned!(discriminant.0.span() => "Currently does not support discriminats.")
532-
};
533634
Ok(PyClassEnumVariant { ident })
534635
}
535636

0 commit comments

Comments
 (0)