from enum import unique, IntEnum
@unique
class UniqueIntEnum(IntEnum):
pass
class Mistake(UniqueIntEnum):
ONE = 1
TWO = 2
THREE = 3
FOUR = 3
print(Mistake.FOUR)
To my surprise, UniqueIntEnum does not enforce it on subclasses. It does work when I decortate Mistake with @unique directly but I want to make one UniqueIntEnum base and then inherit the rest from it, is that possible?
This is just because of how decorators work, isn’t it?
UniqueIntEnum = unique(IntEnum)
class Mistake(UniqueIntEnum):
ONE = 1
TWO = 2
THREE = 3
FOUR = 3
That plus the fact unique doesn’t create a subclass that messes arround with __new__ or __init__ to do its uniqueness checks on new instances.
As I understand it, all the metaclass machinery is called on instantiation. Without some custom override or hook that is called when subclassing, I can’t think how to create a subclass that always calls unique on its own subclasses.
A factory pattern solves this far more elegantly than inheritance.
Enumerated types are … weird. They are implemented in terms of classes, using a special metaclass, but they are decidedly not like other classes. In particular, you can create an enumerated type by inheriting from Enum, IntEnum, etc, but you aren’t really expected to create “subenums” by inheriting such enumerated types.
That said, the unique decorator does not add a uniqueness property that is checked for all subclasses; it just verifies that the immediate class has the uniqueness property. The resulting class is not “marked” as needing to be unique. Here’s the definition:
def unique(enumeration):
"""
Class decorator for enumerations ensuring unique member values.
"""
duplicates = []
for name, member in enumeration.__members__.items():
if name != member.name:
duplicates.append((name, member.name))
if duplicates:
alias_details = ', '.join(
["%s -> %s" % (alias, name) for (alias, name) in duplicates])
raise ValueError('duplicate values found in %r: %s' %
(enumeration, alias_details))
return enumeration
Notice that it does not modifyenumeration in anyway; it just raises an exception if any duplicates in the enumeration are found, and returns the original enumeration as is otherwise.
Whenever an instance of Mistake is required, a factory function must be called to get it, e.g.:
def get_mistake(*args, **kwargs):
@unique
class Mistake(IntEnum):
ONE = 1
TWO = 2
THREE = 3
FOUR = 3
return Mistake(*args, **kwargs)
The disadvantage is, the warning from @unique won’t trigger until get_mistake is called. But in Python you can do anything, pass in Mixins and all sorts of customisations (in this case that may not play nicely with Enums). Or return the class instead of an instance.
Using enum’s functional API is probably a better idea.
Okay that makes sense. Thank you. Although this doesn’t work for me, since I need a unique enforcing base class that can then be used create various enums.
I tried to do something with this but I cannot get it working:
from enum import IntEnum
from typing import Any
class UniqueEnum(IntEnum):
def __init_subclass__(cls, **kwargs: Any) -> None:
values = [member.value for member in cls]
print(f"Current values for {cls}: {values}")
if len(values) != len(set(values)):
raise ValueError
class Mistake(UniqueEnum):
ONE = 1
TWO = 2
THREE = 3
FOUR = 3
print(Mistake.ONE)
The output I get is:
Current values for <enum 'Mistake'>: []
Mistake.ONE
Forgot to post this but here’s the current working code (well working on 3.11+):
import sys
from enum import IntEnum, unique
from typing import Any
class UniqueEnum(IntEnum):
def __init_subclass__(cls, **kwargs: Any) -> None:
unique(cls)
class Mistake(UniqueEnum):
ONE = 1
TWO = 2
THREE = 3
FOUR = 3
print(sys.version_info)
print(Mistake.ONE)
It was added before, but apparently enum didn’t support it before. Although the changelog doesn’t quite match this, it might be worth investigating it a bit more.
I think the answer is “nothing easy”. IMO, dropping support for python 3.9 & 3.10 isn’t that bad of an option.