17

I would like to have a "ALL" flag in my python Flags enum for which

myenum.EVERY_MEMBER & myenum.ALL == myenum.EVERY_MEMBER

holds true. I currently have:

from enum import Flag, auto

class RefreshFlags(Flag):
    NONE = 0
    EVENTS = auto()
    RESOURCES = auto()
    BUILDINGS = auto()
    DEFENSES = auto()
    .....

Because this enum might grow at any state of development I would like to have something like

@property
def ALL(self):
    retval = self.NONE
    for member in self.__members__.values():
        retval |= member
    return retval

This does not work:

RefreshFlags.EVENTS  & RefreshFlags.ALL

TypeError: unsupported operand type(s) for &: 'RefreshFlags' and 'property'

Please note that this question currently only relates to python 3.6 or later.

Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
Sekuraz
  • 548
  • 3
  • 15

4 Answers4

10

There are a few ways to overcome this issue:


One thing to be aware of with the class property method is since the descriptor is defined on the class and not the metaclass the usual protections against setting and deleting are absent -- in other words:

>>> RefreshFlags.ALL
<RefreshFlags.DEFENSES|BUILDINGS|RESOURCES|EVENTS: 15>

>>> RefreshFlags.ALL = 'oops'
>>> RefreshFlags.ALL
'oops'

Creating a new base class:

# lightly tested
from enum import Flag, auto
from operator import or_ as _or_
from functools import reduce

class AllFlag(Flag):

    @classproperty
    def ALL(cls):
        cls_name = cls.__name__
        if not len(cls):
            raise AttributeError('empty %s does not have an ALL value' % cls_name)
        value = cls(reduce(_or_, cls))
        cls._member_map_['ALL'] = value
        return value

And in use:

class RefreshFlag(AllFlag):
    EVENTS = auto()
    RESOURCES = auto()
    BUILDINGS = auto()
    DEFENSES = auto()

>>> RefreshFlag.ALL
<RefreshFlag.DEFENSES|BUILDINGS|RESOURCES|EVENTS: 15>

The interesting difference in the ALL property is the setting of the name in _member_map_ -- this allows the same protections afforded to Enum members:

>>> RefreshFlag.ALL = 9
Traceback (most recent call last):
  ....
AttributeError: Cannot reassign members.

However, there is a race condition here: if RefreshFlag.ALL = ... occurs before RefreshFlag.ALL is activated the first time then it is clobbered; for this reason I would use a decorator in this instance, as the decorator will process the Enum before it can be clobbered.

# lightly tested

from enum import Flag, auto
from operator import or_ as _or_
from functools import reduce

def with_limits(enumeration):
    "add NONE and ALL psuedo-members to enumeration"
    none_mbr = enumeration(0)
    all_mbr = enumeration(reduce(_or_, enumeration))
    enumeration._member_map_['NONE'] = none_mbr
    enumeration._member_map_['ALL'] = all_mbr
    return enumeration

And in use:

@with_limits
class RefreshFlag(Flag):
    EVENTS = auto()
    RESOURCES = auto()
    BUILDINGS = auto()
    DEFENSES = auto()

>>> RefreshFlag.ALL = 99
Traceback (most recent call last):
  ...
AttributeError: Cannot reassign members.

>>> RefreshFlag.ALL 
<RefreshFlag.DEFENSES|BUILDINGS|RESOURCES|EVENTS: 15>

>>> RefreshFlag.NONE
<RefreshFlag.0: 0>
Community
  • 1
  • 1
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • @ZeroPiraeus: Yes, you have the reasoning correct. Thanks for catching that. A little more effort (on my part) shows that an `ALL` value on an empty Enum is non-sensical. – Ethan Furman Feb 16 '17 at 11:26
  • Nice! I've added a note to my answer, redirecting readers to yours. Maybe it's worth adding a guard against empty enums (which ISTM would be best off just omitting to define `ALL` in that case) to `@with_limits`, similar to the one in `AllFlag` ... – Zero Piraeus Feb 16 '17 at 14:36
  • Ideally, the decorator will only be applied to a populated Enum (a non-empty `_member_names_` is the clue). Whether that situation should raise an error or just decline to modify is something the user would need to decide, and modify the above code appropriately. – Ethan Furman Feb 16 '17 at 17:28
5

TL;DR Because property is only evaluated on instances of a class while the __members__ is only accessible on the class.


If you access a property on a class it just returns a property:

>>> RefreshFlags.ALL
<property at 0x2a5d93382c8>

To make this work however you need to either make it a classmethod:

from enum import Flag, auto

class RefreshFlags(Flag):
    NONE = 0
    EVENTS = auto()
    RESOURCES = auto()
    BUILDINGS = auto()
    DEFENSES = auto()

    @classmethod
    def ALL(cls):
        retval = self.NONE
        for member in cls.__members__.values():
            retval |= member
        return retval

>>> RefreshFlags.ALL()
<RefreshFlags.DEFENSES|BUILDINGS|RESOURCES|EVENTS: 15>

or access the property on an instance:

from enum import Flag, auto

class RefreshFlags(Flag):
    NONE = 0
    EVENTS = auto()
    RESOURCES = auto()
    BUILDINGS = auto()
    DEFENSES = auto()

    @property
    def ALL(self):
        retval = self.NONE
        # One needs to access .__class__ here!
        for member in self.__class__.__members__.values():
            retval |= member
        return retval

>>> RefreshFlags.EVENTS.ALL
<RefreshFlags.DEFENSES|BUILDINGS|RESOURCES|EVENTS: 15>

In both cases you can do your comparison later:

>>> RefreshFlags.EVENTS & RefreshFlags.EVENTS.ALL
<RefreshFlags.EVENTS: 1>

You stated in the comments that you want the ALL member to behave like the others, in that case I suggest using a class decorator:

def with_ALL_member(enumeration):
    retval = enumeration(0)  # in case NONE is not defined
    for name, member in enumeration.__members__.items():
        retval |= member
    enumeration.ALL = retval
    return enumeration

@with_ALL_member
class RefreshFlags(Flag):
    NONE = 0
    EVENTS = auto()
    RESOURCES = auto()
    BUILDINGS = auto()
    DEFENSES = auto()

>>> RefreshFlags.EVENTS & RefreshFlags.ALL
<RefreshFlags.EVENTS: 1>

>>> RefreshFlags.DEFENSES & RefreshFlags.ALL
<RefreshFlags.DEFENSES: 8>

The class decorator can also be used on other enums :)

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • Thank you, forgot that this was the class itself and not an instance But neither of those yield the RefreshFlags.ALL that should fit in nicely with the other values. – Sekuraz Feb 15 '17 at 14:38
  • I want a flag value containing the union of all values, the intersection with this "ALL" value should be the original value. it should be used like that: def func(flags): if flags & RefreshFlags.EVENTS: – Sekuraz Feb 15 '17 at 14:55
  • ok, could you elaborate what you mean by "fit in nicely with the other values" means? – MSeifert Feb 15 '17 at 15:48
  • No special syntax like "myenum.ALL()" or "myenum.some_member.ALL". Just a plain "myenum.ALL" – Sekuraz Feb 15 '17 at 15:51
  • 1
    @Sekuraz Ah, now I got it. I've added a class-decorator method that adds an `ALL` attribute to your enums. – MSeifert Feb 15 '17 at 16:37
  • 1
    I like the decorator idea (not so much on the others ;) -- changed my vote. – Ethan Furman Feb 16 '17 at 02:20
4

Following up on MSeifert's answer, it's possible to write a @classproperty decorator which allows you to access RefreshFlags.ALL directly as a property (rather than as a conventional method or a property on an instance):

from enum import Flag, auto
from operator import or_
from functools import reduce


class classproperty:

    def __init__(self, func):
        self._func = func

    def __get__(self, obj, owner):
        return self._func(owner)


class RefreshFlags(Flag):

    NONE = 0
    EVENTS = auto()
    RESOURCES = auto()
    BUILDINGS = auto()
    DEFENSES = auto()

    @classproperty
    def ALL(cls):
        return reduce(or_, cls)

You can of course write ALL() with an explicit for loop as in your example; the above is merely offered as an alternative.

>>> RefreshFlags.ALL
<RefreshFlags.DEFENSES|BUILDINGS|RESOURCES|EVENTS: 15>
>>> RefreshFlags.ALL & RefreshFlags.BUILDINGS
<RefreshFlags.BUILDINGS: 4>
Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
  • 1
    Thank you very much for this solution. I am using the for loop because I am not that accustomed to functional programming. – Sekuraz Feb 15 '17 at 15:06
  • Beat me to it! Include the warning about being able to overwrite `ALL` and I'll remove my answer. – Ethan Furman Feb 15 '17 at 15:30
  • @EthanFurman Added ... but because you're right and people might not read past the accepted answer, not because I expect you to delete anything. Let a thousand flowers bloom, etc. – Zero Piraeus Feb 15 '17 at 15:46
  • Well, having one answer be a complete subset of another seems silly... so I updated mine to be more comprehensive of all our approaches. After doing that it seems that a decorator (mine, to be exact ;) is a more robust way to go in this case. And I found a bug in enum, so thank you to everyone for that! – Ethan Furman Feb 16 '17 at 13:49
2

Here is a slightly more compact solution, using a slimmer descriptor which makes it easier to reuse.

class _all:
    def __get__(self, instance, cls):
        return ~cls(0)


class RefreshFlags(Flag):
    EVENTS = auto()
    RESOURCES = auto()
    BUILDINGS = auto()
    DEFENSES = auto()

    ALL = _all()

RefreshFlags.ALL
>>> <RefreshFlags.DEFENSES|BUILDINGS|RESOURCES|EVENTS: 15>

As already pointed out, a member defined this way doesn't get to be included in the namespace's _member_map_ dictionary, therefore it is not protected against overwriting it.

avikam
  • 1,018
  • 9
  • 11