Skip to content

Commit a32afdd

Browse files
authored
cfg features for enum variants (#4509)
* handle feature gated simple enum variants * test case for feature gated enum variants * add towncrier newsfragment to pull request * rename `attrs` to `cfg_attrs` * generate a compiler error if cfg attributes disable all variants of enum * test compiler error when all variants of enum disabled * spanned compiler error when cfg features disable enum variants
1 parent d14bfd0 commit a32afdd

File tree

6 files changed

+119
-10
lines changed

6 files changed

+119
-10
lines changed

newsfragments/4509.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix compile failure when using `#[cfg]` attributes for simple enum variants.

pyo3-macros-backend/src/pyclass.rs

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::attributes::{
1717
use crate::konst::{ConstAttributes, ConstSpec};
1818
use crate::method::{FnArg, FnSpec, PyArg, RegularArg};
1919
use crate::pyfunction::ConstructorAttribute;
20-
use crate::pyimpl::{gen_py_const, PyClassMethodsType};
20+
use crate::pyimpl::{gen_py_const, get_cfg_attributes, PyClassMethodsType};
2121
use crate::pymethod::{
2222
impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType,
2323
SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, __STR__,
@@ -533,7 +533,12 @@ impl<'a> PyClassSimpleEnum<'a> {
533533
_ => bail_spanned!(variant.span() => "Must be a unit variant."),
534534
};
535535
let options = EnumVariantPyO3Options::take_pyo3_options(&mut variant.attrs)?;
536-
Ok(PyClassEnumUnitVariant { ident, options })
536+
let cfg_attrs = get_cfg_attributes(&variant.attrs);
537+
Ok(PyClassEnumUnitVariant {
538+
ident,
539+
options,
540+
cfg_attrs,
541+
})
537542
}
538543

539544
let ident = &enum_.ident;
@@ -693,6 +698,7 @@ impl<'a> EnumVariant for PyClassEnumVariant<'a> {
693698
struct PyClassEnumUnitVariant<'a> {
694699
ident: &'a syn::Ident,
695700
options: EnumVariantPyO3Options,
701+
cfg_attrs: Vec<&'a syn::Attribute>,
696702
}
697703

698704
impl<'a> EnumVariant for PyClassEnumUnitVariant<'a> {
@@ -877,20 +883,23 @@ fn impl_simple_enum(
877883
ensure_spanned!(variant.options.constructor.is_none(), variant.options.constructor.span() => "`constructor` can't be used on a simple enum variant");
878884
}
879885

886+
let variant_cfg_check = generate_cfg_check(&variants, cls);
887+
880888
let (default_repr, default_repr_slot) = {
881889
let variants_repr = variants.iter().map(|variant| {
882890
let variant_name = variant.ident;
891+
let cfg_attrs = &variant.cfg_attrs;
883892
// Assuming all variants are unit variants because they are the only type we support.
884893
let repr = format!(
885894
"{}.{}",
886895
get_class_python_name(cls, args),
887896
variant.get_python_name(args),
888897
);
889-
quote! { #cls::#variant_name => #repr, }
898+
quote! { #(#cfg_attrs)* #cls::#variant_name => #repr, }
890899
});
891900
let mut repr_impl: syn::ImplItemFn = syn::parse_quote! {
892901
fn __pyo3__repr__(&self) -> &'static str {
893-
match self {
902+
match *self {
894903
#(#variants_repr)*
895904
}
896905
}
@@ -908,11 +917,12 @@ fn impl_simple_enum(
908917
// This implementation allows us to convert &T to #repr_type without implementing `Copy`
909918
let variants_to_int = variants.iter().map(|variant| {
910919
let variant_name = variant.ident;
911-
quote! { #cls::#variant_name => #cls::#variant_name as #repr_type, }
920+
let cfg_attrs = &variant.cfg_attrs;
921+
quote! { #(#cfg_attrs)* #cls::#variant_name => #cls::#variant_name as #repr_type, }
912922
});
913923
let mut int_impl: syn::ImplItemFn = syn::parse_quote! {
914924
fn __pyo3__int__(&self) -> #repr_type {
915-
match self {
925+
match *self {
916926
#(#variants_to_int)*
917927
}
918928
}
@@ -936,7 +946,9 @@ fn impl_simple_enum(
936946
methods_type,
937947
simple_enum_default_methods(
938948
cls,
939-
variants.iter().map(|v| (v.ident, v.get_python_name(args))),
949+
variants
950+
.iter()
951+
.map(|v| (v.ident, v.get_python_name(args), &v.cfg_attrs)),
940952
ctx,
941953
),
942954
default_slots,
@@ -945,6 +957,8 @@ fn impl_simple_enum(
945957
.impl_all(ctx)?;
946958

947959
Ok(quote! {
960+
#variant_cfg_check
961+
948962
#pytypeinfo
949963

950964
#pyclass_impls
@@ -1474,7 +1488,13 @@ fn generate_default_protocol_slot(
14741488

14751489
fn simple_enum_default_methods<'a>(
14761490
cls: &'a syn::Ident,
1477-
unit_variant_names: impl IntoIterator<Item = (&'a syn::Ident, Cow<'a, syn::Ident>)>,
1491+
unit_variant_names: impl IntoIterator<
1492+
Item = (
1493+
&'a syn::Ident,
1494+
Cow<'a, syn::Ident>,
1495+
&'a Vec<&'a syn::Attribute>,
1496+
),
1497+
>,
14781498
ctx: &Ctx,
14791499
) -> Vec<MethodAndMethodDef> {
14801500
let cls_type = syn::parse_quote!(#cls);
@@ -1490,7 +1510,25 @@ fn simple_enum_default_methods<'a>(
14901510
};
14911511
unit_variant_names
14921512
.into_iter()
1493-
.map(|(var, py_name)| gen_py_const(&cls_type, &variant_to_attribute(var, &py_name), ctx))
1513+
.map(|(var, py_name, attrs)| {
1514+
let method = gen_py_const(&cls_type, &variant_to_attribute(var, &py_name), ctx);
1515+
let associated_method_tokens = method.associated_method;
1516+
let method_def_tokens = method.method_def;
1517+
1518+
let associated_method = quote! {
1519+
#(#attrs)*
1520+
#associated_method_tokens
1521+
};
1522+
let method_def = quote! {
1523+
#(#attrs)*
1524+
#method_def_tokens
1525+
};
1526+
1527+
MethodAndMethodDef {
1528+
associated_method,
1529+
method_def,
1530+
}
1531+
})
14941532
.collect()
14951533
}
14961534

@@ -2395,6 +2433,37 @@ fn define_inventory_class(inventory_class_name: &syn::Ident, ctx: &Ctx) -> Token
23952433
}
23962434
}
23972435

2436+
fn generate_cfg_check(variants: &[PyClassEnumUnitVariant<'_>], cls: &syn::Ident) -> TokenStream {
2437+
if variants.is_empty() {
2438+
return quote! {};
2439+
}
2440+
2441+
let mut conditions = Vec::new();
2442+
2443+
for variant in variants {
2444+
let cfg_attrs = &variant.cfg_attrs;
2445+
2446+
if cfg_attrs.is_empty() {
2447+
// There's at least one variant of the enum without cfg attributes,
2448+
// so the check is not necessary
2449+
return quote! {};
2450+
}
2451+
2452+
for attr in cfg_attrs {
2453+
if let syn::Meta::List(meta) = &attr.meta {
2454+
let cfg_tokens = &meta.tokens;
2455+
conditions.push(quote! { not(#cfg_tokens) });
2456+
}
2457+
}
2458+
}
2459+
2460+
quote_spanned! {
2461+
cls.span() =>
2462+
#[cfg(all(#(#conditions),*))]
2463+
::core::compile_error!(concat!("#[pyclass] can't be used on enums without any variants - all variants of enum `", stringify!(#cls), "` have been configured out by cfg attributes"));
2464+
}
2465+
}
2466+
23982467
const UNIQUE_GET: &str = "`get` may only be specified once";
23992468
const UNIQUE_SET: &str = "`set` may only be specified once";
24002469
const UNIQUE_NAME: &str = "`name` may only be specified once";

pyo3-macros-backend/src/pyimpl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ fn submit_methods_inventory(
330330
}
331331
}
332332

333-
fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> {
333+
pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> {
334334
attrs
335335
.iter()
336336
.filter(|attr| attr.path().is_ident("cfg"))

tests/test_field_cfg.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ struct CfgClass {
1717
pub b: u32,
1818
}
1919

20+
#[pyclass(eq, eq_int)]
21+
#[derive(PartialEq)]
22+
enum CfgSimpleEnum {
23+
#[cfg(any())]
24+
DisabledVariant,
25+
#[cfg(not(any()))]
26+
EnabledVariant,
27+
}
28+
2029
#[test]
2130
fn test_cfg() {
2231
Python::with_gil(|py| {
@@ -27,3 +36,18 @@ fn test_cfg() {
2736
assert_eq!(b, 3);
2837
});
2938
}
39+
40+
#[test]
41+
fn test_cfg_simple_enum() {
42+
Python::with_gil(|py| {
43+
let simple = py.get_type::<CfgSimpleEnum>();
44+
pyo3::py_run!(
45+
py,
46+
simple,
47+
r#"
48+
assert hasattr(simple, "EnabledVariant")
49+
assert not hasattr(simple, "DisabledVariant")
50+
"#
51+
);
52+
})
53+
}

tests/ui/invalid_pyclass_enum.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,13 @@ enum InvalidOrderedComplexEnum2 {
9393
VariantB { msg: String }
9494
}
9595

96+
#[pyclass(eq)]
97+
#[derive(PartialEq)]
98+
enum AllEnumVariantsDisabled {
99+
#[cfg(any())]
100+
DisabledA,
101+
#[cfg(not(all()))]
102+
DisabledB,
103+
}
104+
96105
fn main() {}

tests/ui/invalid_pyclass_enum.stderr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ error: The `ord` option requires the `eq` option.
6666
83 | #[pyclass(ord)]
6767
| ^^^
6868

69+
error: #[pyclass] can't be used on enums without any variants - all variants of enum `AllEnumVariantsDisabled` have been configured out by cfg attributes
70+
--> tests/ui/invalid_pyclass_enum.rs:98:6
71+
|
72+
98 | enum AllEnumVariantsDisabled {
73+
| ^^^^^^^^^^^^^^^^^^^^^^^
74+
6975
error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq`
7076
--> tests/ui/invalid_pyclass_enum.rs:31:11
7177
|

0 commit comments

Comments
 (0)