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