'Python: overloading the `__call__` dunder method of a class that inherits Enum

Consider the following:

from enum import Enum

class A(int, Enum):
   
    def __new__(cls, value):
        def call(cls, value):
            print("HELLO 1" + str(value))

        self = super().__new__(cls, value)
        self.__call__ = call
        return self

    def __call__(cls, value):
        print("HELLO 2" + str(value))


class B(A):

    NULL  = 0
    BOB   = 1
    ALICE = 2

    def __call__(cls, value):
        print("HELLO 3" + str(value))

print("int: " + str(B(    1)))
print("str: " + str(B("BOB")))

running this, you get:

int: B.BOB
Traceback (most recent call last):
  File "/obfuscated_path/playground.py", line 27, in <module>
    print("str: " + str(B("BOB")))
  File "/usr/lib/python3.10/enum.py", line 384, in __call__
    return cls.__new__(cls, value)
  File "/usr/lib/python3.10/enum.py", line 701, in __new__
    raise ve_exc
ValueError: 'BOB' is not a valid B

I don't understand why NONE of the alternative __call__ methods are actually called, and the parent's method (ie, EnumMeta.__call__()) is directly called instead.

How can I overload the __call__ method in a child class of Enum ?



Solution 1:[1]

Thanks to the lovely folks in the Python discord channel (in #help-pancakes), I understood what the problem was.

In a nutshell, the problem was confusing Enum as a subclass of EnumMeta, when Enum is actually an instance of EnumMeta. Here, that means that when writing B("BOB"), even though it looks like you're calling the B.__new__ constructor, or that you're calling a classmethod-like B.__call__(), you're actually calling A.__call__() (in fact, __call__ is only ever used for instances).

Taking things one step further, you can consider the following

class MetaClassName(type):

    def __new__(cls, clsname, superclasses, attributedict):
        return super(MetaClassName, cls).__new__(cls,
            clsname,
            superclasses,
            attributedict,
        )

    def __call__(cls, value):
        print("MetaClassName.__call__(): " + str(cls))

class ClassName(metaclass=MetaClassName):

    def __new__(cls, value):
        print("ClassName.__new__(): " + str(cls))
        return int.__new__(cls, value + 1)

my_instance = ClassName(3)
print(my_instance)

which prints:

MetaClassName.__call__(): <class '__main__.ClassName'>
None

As you can see, ClassName.__new__() is never called. In the case of EnumMeta and Enum, the only reason why Enum.__new__() is even called is because EnumMeta.__call__() explicitly calls cls.__new__(cls, value).

Additionally, in the case of Enum, A.__new__() is only called when instantiating the enum instances (one for NULL, one for BOB and one for ALICE), so trying to overload Enum.__new__ using A.__new__ or B.__new__ in the hope of changing B("BOB")'s (failing) behavior is useless. What needs to change to allow B("BOB") to be valid syntax is the Enum.__new__() method itself.

What I did was (removing the stdlib comments to make my change more visible):

def __fixed_new(cls, value):
    if type(value) is cls:
        return value
    try:
        return cls._value2member_map_[value]
    except KeyError:
        pass
    except TypeError:
        for member in cls._member_map_.values():
            if member._value_ == value:
                return member
    try:
        exc = None
        ######
        #NB: the following is literally the only line that changes compared to the standard enum.py file
        result = cls._missing_(value) if not isinstance(value, str) else cls[value]
        ######
    except Exception as e:
        exc = e
        result = None
    if isinstance(result, cls):
        return result
    else:
        ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
        if result is None and exc is None:
            raise ve_exc
        elif exc is None:
            exc = TypeError(
                    'error in %s._missing_: returned %r instead of None or a valid member'
                    % (cls.__name__, result)
                    )
        exc.__context__ = ve_exc
        raise exc

Enum.__new__ = __fixed_new

With this change, any class inherit from enum can "construct new enum instances" (actually, call and get preconstructed enum values) from the corresponding string value, and the original snippet prints:

int: B.BOB
str: B.BOB

Solution 2:[2]

The typical way to look up an enum member by name is with the __getitem__ syntax:

>>> B["BOB"]
<B.BOB: 1>

If you need to be able to do value lookups using the name, you can define _missing_:

class A(Enum):
    #
    @classmethod
    def _missing_(cls, name):
        return cls[name]

class B(A):
    BOB = 1

and in use:

>>> B("BOB")
<B.BOB: 1>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Tristan Duquesne
Solution 2 Ethan Furman