Reputation: 58988
I've got a file like this:
class Level(Enum):
prerequisite_level: Optional["Level"]
dependent_level: Optional["Level"]
lower_priority_levels: List["Level"]
greater_priority_levels: List["Level"]
DATA_CHECK = "data check"
DESIGN_CHECK = "design check"
ALERT = "alert"
The enum values are in a specific order, and based on each of those levels I need to be able to get the previous one, the next one, and all the previous and next ones. I believe I need to be able to index the levels numerically to get these values, so I've added a constant to be able to do this:
INCREASING_PRIORITY_LEVELS: List[Level] = list(Level)
for priority_level_index, threshold_level in enumerate(Level):
if priority_level_index > 0:
threshold_level.prerequisite_level = Level[priority_level_index - 1]
else:
threshold_level.prerequisite_level = None
if priority_level_index < len(Level) - 1:
threshold_level.dependent_level = Level[priority_level_index + 1]
else:
threshold_level.dependent_level = None
threshold_level.lower_priority_levels = Level[:priority_level_index]
threshold_level.greater_priority_levels = Level[priority_level_index + 1:]
This is clunky, and I'd like to get rid of this constant. Do I need to implement __getitem__
or something to make this possible?
Upvotes: 4
Views: 1039
Reputation: 87
Another version building on @blhsing's answer with types and support for finding the index of a member. Unfortunately mypy does not fully support the metaclass so the type variable is overly generic.
import enum
import itertools
from typing import TypeVar
_EnumMemberT = TypeVar("_EnumMemberT")
class _IndexableEnumType(enum.EnumType):
def __getitem__(self: type[_EnumMemberT], name: str | int) -> _EnumMemberT:
if isinstance(name, int):
try:
return next(itertools.islice(enum.EnumType.__iter__(self), name, None))
except StopIteration:
raise IndexError("enum index out of range") from None
return enum.EnumType.__getitem__(self, name)
def index(self: type[_EnumMemberT], name: str) -> int:
for index, member in enumerate(enum.EnumType.__iter__(self)):
if member.name == name: # type: ignore[attr-defined]
return index
raise ValueError(f"'{name}' is not in enum")
class IndexableEnum(enum.Enum, metaclass=_IndexableEnumType):
pass
Then one can declare an IndexableEnum
as per usual and use indices as you would with a list:
>>> class MyEnum(IndexableEnum):
... Foo = "bar"
... Baz = "qux"
...
>>>
>>> MyEnum[0]
<MyEnum.Foo: 'bar'>
>>>
>>> MyEnum.index('Foo')
0
Upvotes: 0
Reputation: 69288
class Level(Enum):
prerequisite_level: Optional["Level"]
dependent_level: Optional["Level"]
lower_priority_levels: List["Level"]
greater_priority_levels: List["Level"]
DATA_CHECK = "data check"
DESIGN_CHECK = "design check"
ALERT = "alert"
I'm having a hard time understanding the above: ... [comments clarified that the first four should be attributes, and prequisite
and dependent
are the previous and following members, respectively].
The solution is to modify previous members as the current member is being initialized (the trick being that the current member isn't added to the parent Enum
until after the member's creation and initialization). Here is the solution using the stdlib's Enum
1 (Python 3.6 and later):
from enum import Enum, auto
class Level(str, Enum):
#
def __init__(self, name):
# create priority level lists
self.lower_priority_levels = list(self.__class__._member_map_.values())
self.greater_priority_levels = []
# update previous members' greater priority list
for member in self.lower_priority_levels:
member.greater_priority_levels.append(self)
# and link prereq and dependent
self.prerequisite = None
self.dependent = None
if self.lower_priority_levels:
self.prerequisite = self.lower_priority_levels[-1]
self.prerequisite.dependent = self
#
def _generate_next_value_(name, start, count, last_values, *args, **kwds):
return (name.lower().replace('_',' '), ) + args
#
DATA_CHECK = auto()
DESIGN_CHECK = auto()
ALERT = auto()
and in use:
>>> list(Level)
[<Level.DATA_CHECK: 'data check'>, <Level.DESIGN_CHECK: 'design check'>, <Level.ALERT: 'alert'>]
>>> Level.DATA_CHECK.prerequisite
None
>>> Level.DATA_CHECK.dependent
<Level.DESIGN_CHECK: 'design check'>
>>> Level.DESIGN_CHECK.prerequisite
<Level.DATA_CHECK: 'data check'>
>>> Level.DESIGN_CHECK.dependent
<Level.ALERT: 'alert'>
>>> Level.ALERT.prerequisite
<Level.DESIGN_CHECK: 'design check'>
>>> Level.ALERT.dependent
None
Note: If you don't want to see the name twice, a custom __repr__
can show just the enum and member names:
def __repr__(self):
return '<%s.%s>' % (self.__class__.__name__, self.name)
then you'll see:
>>> Level.DESIGN_CHECK
<Level.DESIGN_CHECK>
1If using Python 3.5 or older you need to use aenum
2.
2 Disclosure: I am the author of the Python stdlib Enum
, the enum34
backport, and the Advanced Enumeration (aenum
) library.
Upvotes: 1
Reputation: 107124
You can subclass EnumMeta
to override the __getitem__
method with additional conditions to return a list of Enum
values or a specific Enum
value based on the given index, and create a subclass of Enum
with the aforementioned subclass of EnumMeta
as the metaclass, so that any subclass of this new subclass of Enum
can be indexed as desired:
from itertools import islice
from enum import Enum, EnumMeta
class IndexableEnumMeta(EnumMeta):
def __getitem__(cls, index):
if isinstance(index, slice):
return [cls._member_map_[i] for i in islice(cls._member_map_, index.start, index.stop, index.step)]
if isinstance(index, int):
return cls._member_map_[next(islice(cls._member_map_, index, index + 1))]
return cls._member_map_[index]
class IndexableEnum(Enum, metaclass=IndexableEnumMeta):
pass
class Level(IndexableEnum):
DATA_CHECK = "data check"
DESIGN_CHECK = "design check"
ALERT = "alert"
so that Level[1:3]
returns:
[<Level.DESIGN_CHECK: 'design check'>, <Level.ALERT: 'alert'>]
and Level[1]
returns:
Level.DESIGN_CHECK
(Credit goes to @EthanFurman for pointing out the viability of subclassing EnumMeta
.)
Upvotes: 3
Reputation: 107124
A possible alternative to achieve the same result in terms of usage would be to use collections.namedtuple
instead:
from collections import namedtuple
LevelSequence = namedtuple('Level', ('DATA_CHECK', 'DESIGN_CHECK', 'ALERT'))
Level = LevelSequence('data check', 'design check', 'alert')
So that:
Level.DESIGN_CHECK
and Level[1]
both return 'design check'
, andLevel[1:3]
returns ('design check', 'alert')
Upvotes: -1