3

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?

l0b0
  • 55,365
  • 30
  • 138
  • 223

3 Answers3

2

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.)

blhsing
  • 91,368
  • 6
  • 71
  • 106
  • Yes it does, since it overrides `__getitem__` of the meta class that generates `Enum` class instances. – blhsing Jun 06 '19 at 16:03
  • I've updated my answer with a solution to override `__getitem__` of just one specific class then. – blhsing Jun 10 '19 at 22:06
  • 1
    Fantastic work. I personally wouldn't trade my workaround for this amount of complexity, but others might. – l0b0 Jun 10 '19 at 22:14
  • The complexity of creating modified copies the `EnumMeta` and `Enum` classes can be hidden into a separate module if the goal is to make your user-facing subclass `Level` as clean as possible. – blhsing Jun 11 '19 at 06:01
  • While I don't encourage [subclassing `EnumMeta`](https://stackoverflow.com/q/43730305/208880), it is much easier to do that than what you are doing here. – Ethan Furman Jul 18 '19 at 19:34
  • @EthanFurman Thanks for your input. Subclassing `EnumMeta` was actually the first thing that came to my mind, but upon further inspection I realized that it would not be a viable approach because `Enum` specifically uses `EnumMeta` as a metaclass, which means that one would have to write a new `Enum` class to be able to use this new subclass of `EnumMeta` as a metaclass. Furthermore, the `EnumMeta._get_mixins` method has a validation check of `if not issubclass(base, Enum):` that hard-codes `Enum` as a required base class, so using a new `Enum` class would fail at this validation. – blhsing Jul 18 '19 at 21:19
  • @EthanFurman ...which is why I had to parse the source and modify the AST in order to make these hard-coded names to the names of the new classes. Please feel free to try implementing such a subclass yourself and let me know if you find a way to do it without having to modify the source or a copy of the AST. – blhsing Jul 18 '19 at 21:24
  • [Dynamic member creation](https://stackoverflow.com/q/43730305/208880); [Preventing attribute assignment](https://stackoverflow.com/q/54274002/208880); [Making an Abstract Enum](https://stackoverflow.com/q/56131308/208880). – Ethan Furman Jul 19 '19 at 00:49
  • 1
    As you'll notice from the above examples, creating a new `Enum` with a subclassed `EnumMeta` is as simple as `class MyEnum(Enum, metaclass=MyNewMetaclass)` – Ethan Furman Jul 19 '19 at 01:18
  • @EthanFurman Thanks. I did consider subclassing `Enum` with `metaclass` pointing to the new subclass of `EnumMeta` as well but did not actually try it only because I wrongly thought that it would result in this new subclass of `Enum` becoming just a class of enumerated constants. I've rewritten my answer as suggested. Thanks again. – blhsing Jul 19 '19 at 16:54
1
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 Enum1 (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 aenum2.

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

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • Level values are used as field values in an ORM and are returned from an API, so they shouldn't be integers. You can only create (an instance with) a design check level when there is (an instance with) a data check level, hence `Level.DESIGN_CHECK.prerequisite == Level.DATA_CHECK and Level.DATA_CHECK.dependent == Level.DESIGN_CHECK`. – l0b0 Jul 19 '19 at 01:46
  • @l0b0: Ah, okay. Updated my answer to better match your question. – Ethan Furman Jul 22 '19 at 17:40
  • @l0b0: Woops -- add `, name` into the `__init__`. – Ethan Furman Jul 22 '19 at 23:50
  • @l0b0: Did a little trimming -- updated the `_generate_next_value_` and removed the `__new__` method. – Ethan Furman Jul 23 '19 at 00:31
  • This might be asking too much, but why does `_generate_next_value_` not have `self` as the first argument? This breaks flake8 linting: "N805 first argument of a method should be named 'self'". Also, `mypy` seems to think `self.__class__._member_map_` has type `Level`. – l0b0 Jul 23 '19 at 00:54
  • @l0b0: `_generate_next_value_` is a static method -- when it's called `self` doesn't yet exist; try decorating it with `@staticmethod`. Not sure what's up with `mypy` -- try asking a question about that, and/or filing an issue on [`mypy`s issue tracker](https://github.com/python/mypy/issues). – Ethan Furman Jul 23 '19 at 15:47
-1

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', and
  • Level[1:3] returns ('design check', 'alert')
blhsing
  • 91,368
  • 6
  • 71
  • 106