pythonpython-3.xenumsflags

Attributes on Python flag enums


I was very happy to learn Enum types can effectively carry named attributes:

from enum import Enum

class ActivityType(Enum):
    NEXT = 0, "N"
    JOIN = 3, "J"
    DIVIDE_REAR = 1, "DR"
    DIVIDE_FRONT = 2, "DF"

    def __init__(self, xml_code, label):
        self.xml_code = xml_code
        self.label = label

assert ActivityType.JOIN.label == "J"
assert ActivityType.DIVIDE_FRONT.xml_code == 2

How can I do something similar for Flag enums? Here, something can have a combination of PowerTypes, and the string representation has a code for each one which are combined in a specific order:

class PowerType(Flag):
    NONE = 0
    AC_OVERHEAD = auto()
    DC_3RAIL = auto()
    DIESEL = auto()

    def xml_code(self):
        powertype_subcode = {
            PowerType.AC_OVERHEAD: "O",
            PowerType.DC_3RAIL: "3",
            PowerType.DIESEL: "D",
        }
        return "".join(sc for pt, sc in powertype_subcode.items() if pt & self)

assert (PowerType.DIESEL | PowerType.DC_3RAIL).xml_code() == "3D"
assert (PowerType.AC_OVERHEAD | PowerType.DC_3RAIL).xml_code() == "O3"

I'd like to do something like this and eliminate that powertype_subcode dict:

class PowerType(Flag):
    NONE = 0, ""
    AC_OVERHEAD = auto(), "O"
    DC_3RAIL = auto(), "3"
    DIESEL = auto(), "D"

    def __init__(self, flag, xml_code):
        self.flag = flag
        self._xml_subcode = xml_code

    def xml_code(self):
        return "".join(pt._xml_subcode for pt in PowerType if pt & self)

However this causes problems with auto() and _generate_next_value_ and if I work around that, it then finds it can't do bitwise operations on tuple values.


Solution

  • Using @JackDeeth's answer as a basis, changing xml_code to a property, and saving _xml_subcode when absent, solves the problem:

    from enum import Flag, auto
    
    class PowerType(Flag):
        NONE = 0
        AC_OVERHEAD = auto(), "O"
        DC_3RAIL = auto(), "3"
        DIESEL = auto(), "D"
        #
        def __new__(cls, flag, xml_code = ""):
            obj = object.__new__(cls)
            obj._value_ = flag
            obj._xml_subcode = xml_code
            return obj
        #
        @property
        def xml_code(self):
            code = getattr(self, '_xml_subcode', None)
            if code is None:
                code = "".join(pt._xml_subcode for pt in self)
                self._xml_subcode = code
            return code
    
    # Assertions still pass (after removing parenthesis from xml_code)
    assert (PowerType.DIESEL | PowerType.DC_3RAIL).xml_code == "3D"
    assert (PowerType.AC_OVERHEAD | PowerType.DC_3RAIL).xml_code == "O3"
    assert (PowerType.DIESEL | PowerType.DC_3RAIL)._xml_subcode == "3D"
    assert (PowerType.AC_OVERHEAD | PowerType.DC_3RAIL)._xml_subcode == "O3"
    

    Note also that flags are themselves iterable, so instead of for pt in PowerType I'm using for pt in self.


    Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.