Reputation: 588
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.
Upvotes: 21
Views: 6405
Reputation: 69288
There are a few ways to overcome this issue:
use a classproperty
(see Zero's answer
)
use a class decorator (see MSeifert's answer
)
use a mixin (currently buggy
)
create a new base class (see below)
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.NONE = none_mbr
enumeration.ALL = all_mbr
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>
Upvotes: 12
Reputation: 1008
An easy way to get all Flag
s is ~RefreshFlags(0)
which gives RefreshFlags.EVENTS|RESOURCES|BUILDINGS|DEFENSES|
(note sorted in declaration order).
Upvotes: 0
Reputation: 1206
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.
Upvotes: 4
Reputation: 59238
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>
Upvotes: 6
Reputation: 152840
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 :)
Upvotes: 6