Automatically adding composite values to a Flag enum

The short version is that I have a Flag where every member (save one) should have a corresponding composite member of its value ORed with a special nonmember flag. Example:

from enum import Flag, auto, nonmember  # new in 3.11

class Planet(Flag):
    _BIZARRO = nonmember(1)  # flag to denote other version

    SUN = 0  # no bizarro version

    MERCURY = 2  # need to start with 2 to avoid _BIZARRO
    VENUS = auto()  # auto works from here
    EARTH = auto()
    MARS = auto()

    BIZARRO_MERCURY = MERCURY | _BIZARRO
    BIZARRO_VENUS = VENUS | _BIZARRO
    BIZARRO_EARTH = EARTH | _BIZARRO
    BIZARRO_MARS = MARS | _BIZARRO

    @property
    def BIZARRO(self):
        if self is Planet.SUN:
            return self
        return Planet(self.value ^ Planet._BIZARRO)

    # for nicer display
    def __repr__(self):
        return self.name

This is handy because it allows for stuff like

(
    Planet.MARS.BIZARRO,
    Planet.VENUS.BIZARRO is Planet.BIZARRO_VENUS,
    Planet.EARTH.BIZARRO.BIZARRO is Planet.EARTH,
)
outputs:
    (BIZARRO_MARS, True, True)

Which, in my (non-bizarro) application, makes a lot of stuff easier.

Can I define a subclass that would build this for me? I’d call MyEnum("Planet", ["VENUS", "MERCURY", "EARTH", "MARS"] and it produces a Flag with the above behavior, including the names. That’d save some boilerplate, and allow me to specify the list in a config if I wanted to.

I can avoid defining the composite types if I use boundary=KEEP, but that’s not quite ideal. It doesn’t display things nicely unless I customize __repr__ more, and the additional types aren’t available as members.

I’ve been reading through the Enum docs for a while, trying to figure out if this is possible. It feels like I should be able to subtype Enum or more likely EnumType to make this happen. But the level of metaprogramming is too much for me at the moment, so I figured I’d toss it out here.

Conceptually, the purpose of using Flag instead of an ordinary Enum is so that the values can be ORed together externally. Does MARS | VENUS actually make sense in your program? If so, what happens to BIZARRO_MARS | BIZARRO_VENUS where the bizarro flag is duplicated? Etc.?

Or did you really just want to make an enumeration where all the values except SUN are conceptually “paired” and thus have “normal” and “bizarro” versions?

Realistically this, but the flag semantics made that simple to express. If there’s a simpler way to define an Enum with paired values, that would work for me too. I think the core of the question is whether I can subclass Enum to generate pairs from a list of normal versions.

edit: theoretically the external combinations could have some meaning in my application, but the ordering also matters and it’s possible to have duplicates, so it doesn’t fit so well into an Enum form.

If you’re only looking for the functional syntax, a helper function should do the trick:

def MyEnum(name, values):
    values_dict = {name: 2**value for value, name in enumerate(values, start=1)}
    return BizarroFlag(
        name,
        {
            "SUN": 0,
            **values_dict,
            **{f"BIZARRO_{name}": value | BizarroFlag._BIZARRO for name, value in values_dict.items()}
        }
    )

(with BizarroFlag defined like Planed but without the members)

>>> Planet = MyEnum("Planet", ["VENUS", "MERCURY", "EARTH", "MARS"])
>>> (
...     Planet.MARS.BIZARRO,
...     Planet.VENUS.BIZARRO is Planet.BIZARRO_VENUS,
...     Planet.EARTH.BIZARRO.BIZARRO is Planet.EARTH,
... )
(BIZARRO_MARS, True, True)

If you need the class syntax, I believe you would need a custom metaclass to extend EnumType.__prepare__ method (or EnumType.__call__ for functional syntax), but I never played with these so not sure :smile:

1 Like

Enum being a metaclass, you can also call it to create new Enum classes. We can easily just make a wrapper to generate the enum names that we need, before creating it:

>>> def bizarro_enum(typename, unique, *paired):
...     pairing = (n for p in paired for n in (p, f'BIZARRO_{p}'))
...     return Enum(typename, (unique, *pairing))
... 
>>> planets = bizarro_enum('planets', 'SUN', 'MERCURY', 'VENUS', 'EARTH', 'MARS')
>>> planets
<enum 'planets'>
>>> list(planets)
[<planets.SUN: 1>, <planets.MERCURY: 2>, <planets.BIZARRO_MERCURY: 3>, <planets.VENUS: 4>, <planets.BIZARRO_VENUS: 5>, <planets.EARTH: 6>, <planets.BIZARRO_EARTH: 7>, <planets.MARS: 8>, <planets.BIZARRO_MARS: 9>]

Okay but this doesn’t any of the stuff I outlined in the OP. :stuck_out_tongue:

Having a property that cycles through the two versions is the main reason this is useful to me.

Because the numeric values still have the same relationship, the XOR trick will still work. You can also still subclass the result of a bizarro_enum call to add such a @property.

1 Like

Ah, I missed the sneaky reliance on ordering there. So a complete version would look like:

class BizarroEnum(Enum):
    @property
    def BIZARRO(self):
        if self.value == 1:
            return self
        return self.__class__(self.value ^ 1)

    def __repr__(self):
        return self.name
   
    # might as well keep this in the class 
    @staticmethod
    def from_names(typename, unique, *paired):
        pairing = (n for p in paired for n in (p, f"BIZARRO_{p}"))
        return BizarroEnum(typename, (unique, *pairing))

which allows

P = BizarroEnum.from_names("PLANET", "SUN", "MERCURY", "VENUS", "EARTH", "MARS")
list(P)
out: 
    [SUN,
     MERCURY,
     BIZARRO_MERCURY,
     VENUS,
     BIZARRO_VENUS,
     EARTH,
     BIZARRO_EARTH,
     MARS,
     BIZARRO_MARS]
edited for posterity: the final version I ended up with
class BizarroEnum(Enum):
    @property
    def BIZARRO(self):
        # unique values are negative
        if self.value < 0:
            return self
        return self.__class__(self.value ^ 1)

    def __repr__(self):
        return self.name
   
    # use two lists, one for unique values and one for paired
    @staticmethod
    def from_names(typename, unique: list[str], paired: list[str]):
        unique = ((n, -i) for i, n in enumerate(unique, start=1))
        pairing = (n for p in paired for n in (p, f"BIZARRO_{p}"))
        # this could start at 0 but traditionally enums avoid it
        pairing = ((n, i) for i, n in enumerate(pairing, start=2))
        return BizarroEnum(typename, (*unique, *pairing))