Create an abstract Enum class
Asked Answered
J

3

19

I'm trying to create an abstract enum (Flag actually) with an abstract method. My final goal is to be able to create a string representation of compound enums, based on the basic enums I defined. I'm able to get this functionality without making the class abstract.

This is the basic Flag class and an example implementation:

from enum import auto, Flag

class TranslateableFlag(Flag):
    @classmethod
    def base(cls):
        pass

    def translate(self):
        base = self.base()
        if self in base:
            return base[self]
        else:
            ret = []
            for basic in base:
                if basic in self:
                    ret.append(base[basic])
            return " | ".join(ret)

class Students(TranslateableFlag):
    ALICE = auto()
    BOB = auto()
    CHARLIE = auto()
    ALL = ALICE | BOB | CHARLIE

    @classmethod
    def base(cls):
        return {Students.ALICE: "Alice", Students.BOB: "Bob",
                Students.CHARLIE: "Charlie"}

An example usage is:

((Students.ALICE | Students.BOB).translate())
[Out]: 'Alice | Bob'

Switching to TranslateableFlag(Flag, ABC) fails due to MetaClass conflicts. (I didn't understand this post - Abstract Enum Class using ABCMeta and EnumMeta, so I'm not sure if it's answering my question).

I would like get a functionality like this somehow:

@abstractclassmethod
@classmethod
    def base(cls):
        pass

Is it possible to achieve this?

Jaunty answered 14/5, 2019 at 13:11 Comment(2)
It looks like you always want the individual names and not the conglomerate names (such as "Alice | Bob" instead of "ALL"); is that true? Or does your actual translate() method do more than that?Lepp
You are right, ALL is just for convenience (and forgot to add Charlie to it - fixed).Jaunty
F
15

Here's how to adapt the accepted answer to the question Abstract Enum Class using ABCMeta and EnumMeta to create the kind of abstract Enum class you want:

from abc import abstractmethod, ABC, ABCMeta
from enum import auto, Flag, EnumMeta


class ABCEnumMeta(ABCMeta, EnumMeta):

    def __new__(mcls, *args, **kw):
        abstract_enum_cls = super().__new__(mcls, *args, **kw)
        # Only check abstractions if members were defined.
        if abstract_enum_cls._member_map_:
            try:  # Handle existence of undefined abstract methods.
                absmethods = list(abstract_enum_cls.__abstractmethods__)
                if absmethods:
                    missing = ', '.join(f'{method!r}' for method in absmethods)
                    plural = 's' if len(absmethods) > 1 else ''
                    raise TypeError(
                       f"cannot instantiate abstract class {abstract_enum_cls.__name__!r}"
                       f" with abstract method{plural} {missing}")
            except AttributeError:
                pass
        return abstract_enum_cls


class TranslateableFlag(Flag, metaclass=ABCEnumMeta):

    @classmethod
    @abstractmethod
    def base(cls):
        pass

    def translate(self):
        base = self.base()
        if self in base:
            return base[self]
        else:
            ret = []
            for basic in base:
                if basic in self:
                    ret.append(base[basic])
            return " | ".join(ret)


class Students1(TranslateableFlag):
    ALICE = auto()
    BOB = auto()
    CHARLIE = auto()
    ALL = ALICE | BOB | CHARLIE

    @classmethod
    def base(cls):
        return {Students1.ALICE: "Alice", Students1.BOB: "Bob",
                Students1.CHARLIE: "Charlie"}


# Abstract method not defined - should raise TypeError.
class Students2(TranslateableFlag):
    ALICE = auto()
    BOB = auto()
    CHARLIE = auto()
    ALL = ALICE | BOB | CHARLIE

#    @classmethod
#    def base(cls):
#        ...

Result:

Traceback (most recent call last):
  ...
TypeError: cannot instantiate abstract class 'Students2' with abstract method 'base'
Furnish answered 14/5, 2019 at 16:49 Comment(16)
Instead of putting the check in __call__ can you put it in __new__? After EnumMeta's __new__ is done your __new__ can check if any members were defined, and if so that any abstract methods have also been defined. You would then get the error when you create the class instead of when you try to use it.Lepp
@Ethan: Good point…I agree that would be preferable. Originally I was checking in __new__, but that was preventing the creation of the TranslateableFlag class. However I may have been doing it wrong, so feel free to edit my answer or post your own.Furnish
Changes made! Thank you.Lepp
@Ethan: Actually, the thanks goes to you—not only did you improve the answer, I've learned some new things via the changes you made.Furnish
The @abstractclassmethod decorator is deprecated since Python 3.3 and this great answer is obviously written in Python 3.6+. Shouldn't it rather use the combined @classmethod and @bstractmethod notation?Communion
@shmee: FWIW, in my initial answer that overrode __call__(), I had it that way — based on the documentation — but when @Ethan made his changes he replaced them with @abstractclassmethod. While I understand the latter is now redundant, using it makes this code work for versions < Python 3.2 as well.Furnish
@Furnish @abstractclassmethod was introduced in Python 3.2 and deprecated right away in Python 3.3. While it's absolutely your call to make, this solution adds compatibility for just one Python minor version. Oh, and you had @classmethod and @abstractclassmethod in your original revision which Ethan probably just shortened to remove the redundant @classmethod decoration.Communion
@shmee: Since we don't know exactly what version of Python 3 the OP (or other readers) might be using…and my own preference to avoid hardcoding it, I put in a version check to handle version 3.2 as a special case.Furnish
@martineau: Since Enum didn't show up until 3.4, nor Flag until 3.6, I think you're safe dropping the 3.2 branch. ;-) Removing the deprecation of abstractclassmethod and friends is currently being discussed on Python Dev.Lepp
@Ethan: Right…forgot about that. Version check removed. Thanks again. P.S. IMO the docs should mention that enum.Flag was added in 3.6.Furnish
@martineau: I just checked both 3.6 and 3.7.3 and the docs do say "new in version 3.6" both at the end of the Module Contents section and after the Flag and IntFlag introductory paragraphs. Where are you looking?Lepp
@Ethan: I was looking here and expected the notice to be immediately below the class enum.Flag description, not further down following the complete list of the Module Contents. Sorry.Furnish
@Furnish I am trying the code snippet as currently posted in the answer with Python 3.8 and it just fails in both cases: in case base is defined on the subclass and in case it is not. Honestly, I also do not get the idea of throwing the TypeError in try and catch AttributeError. If no AttributeError occurs, we will always see the TypeError and I do not see where this AttributeError should come from. Would anyone enlighten me, please? It is my first battle with metaclasses... ;)Soy
@matheburg: You're right, it doesn't seem to work in Python 3.8.2. It's been over a year since I wrote this code and I don't recall the details — will look into it time permitting.Furnish
Yep, it does not work in 3.8.x. Deleting Students2 class entirely still gives: TypeError: cannot instantiate abstract class 'TranslateableFlag' with abstract method 'base'.Backsaw
@user443854: The issue is that TranslatableFlag should have had ABC in its header. Of course, I didn't realize that until I had already rewritten the ABCEnumMeta to not check unless members have been defined... which is probably the better solution in this case, anyway.Lepp
B
7

Here is a fix of the accepted answer for python 3.8. The only change is to the ABCEnumMeta. The rest is copy-pasted from the original answer to provide a runnable example. Also tested on python 3.6.2.

from abc import abstractmethod, ABC, ABCMeta
from enum import auto, Flag, EnumMeta


class ABCEnumMeta(EnumMeta, ABCMeta):
    pass


class TranslateableFlag(Flag, metaclass=ABCEnumMeta):

    @classmethod
    @abstractmethod
    def base(cls):
        pass

    def translate(self):
        base = self.base()
        if self in base:
            return base[self]
        else:
            ret = []
            for basic in base:
                if basic in self:
                    ret.append(base[basic])
            return " | ".join(ret)


class Students1(TranslateableFlag):
    ALICE = auto()
    BOB = auto()
    CHARLIE = auto()
    ALL = ALICE | BOB | CHARLIE

    @classmethod
    def base(cls):
        return {Students1.ALICE: "Alice", Students1.BOB: "Bob",
                Students1.CHARLIE: "Charlie"}


class Students2(TranslateableFlag):
    ALICE = auto()
    BOB = auto()
    CHARLIE = auto()
    ALL = ALICE | BOB | CHARLIE
    
# Abstract method not defined - should raise TypeError.
#    @classmethod
#    def base(cls):
#        ...
Backsaw answered 9/9, 2020 at 4:41 Comment(0)
L
2

If the goal is simply to change the __str__ output of Students1, you do not need to use abstract classes:

from enum import auto, Flag
from functools import reduce

class TranslateableFlag(Flag):

    def __init__(self, value):
        self.translated = self.name.title()

    def __str__(self):
        cls = self.__class__
        total = self._value_
        i = 1
        bits = set()
        while i <= total:
            bits.add(i)
            i *= 2
        members = [m for m in cls if m._value_ in bits]
        return '%s' % (
                ' | '.join([str(m.translated) for m in members]),
                )

class Students1(TranslateableFlag):
    ALICE = auto()
    BOB = auto()
    CHARLIE = auto()
    ALL = ALICE | BOB | CHARLIE

and in use:

>>> print(Students1.ALICE | Students1.BOB)
Alice | Bob

>>> print(Students1.ALL)
Alice | Bob | Charlie
Lepp answered 16/5, 2019 at 0:17 Comment(2)
I actually did want the translate function - this question shows a toy example but in my original problem I need to get less human readable string representations for filtering columns in a pandas DF - so I don't want to go and change __str__. But this is a good option for general. Thanks for all the previous discussion and improvements.Jaunty
Why did you make a set here instead of doing normal bit-twiddling. E.g., drop bits and do members = [m for m in cls if (m._value_ & self._value_)]?Almeta

© 2022 - 2024 — McMap. All rights reserved.