12
votes

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?

3
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? - Ethan Furman
You are right, ALL is just for convenience (and forgot to add Charlie to it - fixed). - Tzahi T

3 Answers

11
votes

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)
        try:  # Handle existence of undefined abstract methods.
            absmethods = list(abstract_enum_cls.__abstractmethods__)
            absmethods_str = ', '.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} {absmethods_str}")
        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"}


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

# Abstract method not defined - should raise TypeError.
#    @classmethod
#    def base(cls):
#        ...

Result:

Traceback (most recent call last):
  ...
TypeError: cannot instantiate abstract class 'TranslateableFlag' with abstract method 'base'
1
votes

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
1
votes

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