18

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?

martineau
  • 119,623
  • 25
  • 170
  • 301
Tzahi T
  • 346
  • 3
  • 11
  • 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 May 15 '19 at 01:02
  • You are right, ALL is just for convenience (and forgot to add Charlie to it - fixed). – Tzahi T May 15 '19 at 10:42

3 Answers3

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'
martineau
  • 119,623
  • 25
  • 170
  • 301
  • 1
    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. – Ethan Furman May 14 '19 at 17:25
  • @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. – martineau May 14 '19 at 17:33
  • 1
    Changes made! Thank you. – Ethan Furman May 14 '19 at 18:29
  • @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. – martineau May 14 '19 at 20:07
  • The `@abstractclassmethod` decorator is [deprecated since Python 3.3](https://docs.python.org/3/library/abc.html#abc.abstractclassmethod) and this great answer is obviously written in Python 3.6+. Shouldn't it rather use the combined `@classmethod` and `@bstractmethod` notation? – shmee May 15 '19 at 06:04
  • @shmee: FWIW, in my initial answer that overrode `__call__()`, I had it that way — based on the [documentation](https://docs.python.org/3/library/abc.html#abc.abstractclassmethod) — 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. – martineau May 15 '19 at 09:35
  • 1
    @martineau `@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. – shmee May 15 '19 at 10:40
  • 1
    @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. – martineau May 15 '19 at 12:52
  • 2
    @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. – Ethan Furman May 15 '19 at 20:06
  • @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. – martineau May 15 '19 at 20:32
  • @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? – Ethan Furman May 15 '19 at 20:58
  • @Ethan: I was looking [here](https://docs.python.org/3/library/enum.html#module-enum) 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. – martineau May 15 '19 at 21:08
  • 1
    @martineau 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... ;) – matheburg Jun 08 '20 at 04:38
  • @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. – martineau Jun 08 '20 at 14:20
  • 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'`. – user443854 Sep 09 '20 at 02:12
  • 1
    @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. – Ethan Furman Oct 29 '21 at 19:12
6

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):
#        ...
user443854
  • 7,096
  • 13
  • 48
  • 63
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
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • 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. – Tzahi T May 16 '19 at 09:15
  • 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_)]`? – Mad Physicist Jul 08 '23 at 05:02