pyenum_derive/lib.rs
1//! `#[derive(PyEnum)]` procedural macro.
2//!
3//! Parsing, validation, and codegen live in the submodules and are wired
4//! together by [`derive_pyenum`]. The generated code only names items
5//! re-exported from `pyenum::__private`, so the output is stable across
6//! internal refactors of the runtime crate.
7//!
8//! See the [`pyenum`](https://docs.rs/pyenum) crate docs for usage,
9//! worked examples, and scope.
10
11use proc_macro::TokenStream;
12
13mod codegen;
14mod parse;
15mod reserved;
16mod validate;
17
18/// Derive a [`pyenum::PyEnum`](../pyenum/trait.PyEnumTrait.html)
19/// implementation for a unit-variant Rust enum.
20///
21/// The derive also emits PyO3 0.28 `IntoPyObject<'py>` (for `T` and `&T`)
22/// and `FromPyObject<'a, 'py>` impls, so the Rust enum can appear directly
23/// in `#[pyfunction]`, `#[pymethods]`, and `#[pyclass]` field signatures.
24///
25/// # Attributes
26///
27/// Enum-level (all optional):
28///
29/// * `#[pyenum(base = "Enum" | "IntEnum" | "StrEnum" | "Flag" | "IntFlag")]`
30/// — select the Python base class. Defaults to `"Enum"`.
31/// * `#[pyenum(name = "...")]` — override the Python class name. Defaults
32/// to the Rust enum identifier.
33///
34/// Variant-level (optional, mutually exclusive with a Rust discriminant):
35///
36/// * `#[pyenum(value = "...")]` — explicit Python string value. Valid on
37/// `StrEnum` (and `Enum`). Without it, `StrEnum` variants default to
38/// Python's `auto()` semantics, which lowercase the variant name.
39///
40/// # Example
41///
42/// ```rust,ignore
43/// use pyenum::{PyEnum, PyModuleExt};
44/// use pyo3::prelude::*;
45///
46/// #[derive(Clone, Copy, PyEnum)]
47/// #[pyenum(base = "IntEnum")]
48/// pub enum HttpStatus {
49/// Ok = 200,
50/// NotFound = 404,
51/// }
52///
53/// #[pyfunction]
54/// fn classify(s: HttpStatus) -> HttpStatus { s }
55///
56/// #[pymodule]
57/// fn demo(m: &Bound<'_, PyModule>) -> PyResult<()> {
58/// m.add_enum::<HttpStatus>()?;
59/// m.add_function(wrap_pyfunction!(classify, m)?)
60/// }
61/// ```
62///
63/// # Rejected at compile time
64///
65/// The macro emits spanned `compile_error!` diagnostics for:
66///
67/// * Tuple / struct variants, generics, lifetimes, and empty enums.
68/// * Variant names colliding with Python keywords, dunders, or
69/// `enum.Enum`-reserved attributes.
70/// * Base/value shape mismatches (e.g. `#[pyenum(value = "x")]` on
71/// `IntEnum`, or an integer discriminant on `StrEnum`).
72/// * Duplicate resolved values — includes the `auto()` vs. explicit
73/// collision on every integer-shaped base (`enum { A, B = 1 }` with
74/// base `IntEnum`, `enum { Read, Write = 1 }` with base `Flag`, etc.)
75/// and auto-lowercased name collisions on `StrEnum` (`Hello` + `HELLO`).
76/// * Duplicate or unknown `#[pyenum(...)]` keys.
77#[proc_macro_derive(PyEnum, attributes(pyenum))]
78pub fn derive_pyenum(input: TokenStream) -> TokenStream {
79 let spec = match parse::parse_derive_input(input.into()) {
80 Ok(spec) => spec,
81 Err(err) => return err.to_compile_error().into(),
82 };
83 let spec = match validate::run(spec) {
84 Ok(spec) => spec,
85 Err(err) => return err.to_compile_error().into(),
86 };
87 codegen::emit(&spec).into()
88}