To make an enum class fully 'read-only', all that is required is a meta class that uses the __setattr__
hook that prevents all attribute assignments. Because the metaclass is attached to the class after it is created, there is no issue with assigning the proper enumerated values.
Like Ethan's answer, I'm using the EnumMeta
class as a base for the custom metaclass:
from enum import EnumMeta, Enum
class FrozenEnumMeta(EnumMeta):
"Enum metaclass that freezes an enum entirely"
def __new__(mcls, name, bases, classdict):
classdict['__frozenenummeta_creating_class__'] = True
enum = super().__new__(mcls, name, bases, classdict)
del enum.__frozenenummeta_creating_class__
return enum
def __call__(cls, value, names=None, *, module=None, **kwargs):
if names is None: # simple value lookup
return cls.__new__(cls, value)
enum = Enum._create_(value, names, module=module, **kwargs)
enum.__class__ = type(cls)
return enum
def __setattr__(cls, name, value):
members = cls.__dict__.get('_member_map_', {})
if hasattr(cls, '__frozenenummeta_creating_class__') or name in members:
return super().__setattr__(name, value)
if hasattr(cls, name):
msg = "{!r} object attribute {!r} is read-only"
else:
msg = "{!r} object has no attribute {!r}"
raise AttributeError(msg.format(cls.__name__, name))
def __delattr__(cls, name):
members = cls.__dict__.get('_member_map_', {})
if hasattr(cls, '__frozenenummeta_creating_class__') or name in members:
return super().__delattr__(name)
if hasattr(cls, name):
msg = "{!r} object attribute {!r} is read-only"
else:
msg = "{!r} object has no attribute {!r}"
raise AttributeError(msg.format(cls.__name__, name))
class FrozenEnum(Enum, metaclass=FrozenEnumMeta):
pass
The above distinguishes between attributes that are already available and new attributes, for ease of diagnosing. It also blocks attribute deletion, which is probably just as important!
It also provides both the metaclass and a FrozenEnum
base class for enumerations; use this instead of Enum
.
To freeze a sample Color
enumeration:
>>> class Color(FrozenEnum):
... red = 1
... green = 2
... blue = 3
...
>>> list(Color)
[<Color.red: 1>, <Color.green: 2>, <Color.blue: 3>]
>>> Color.foo = 'bar'
Traceback (most recent call last):
# ...
AttributeError: 'Color' object has no attribute 'foo'
>>> Color.red = 42
Traceback (most recent call last):
# ...
Cannot reassign members.
>>> del Color.red
Traceback (most recent call last):
# ...
AttributeError: Color: cannot delete Enum member.
Note that all attribute changes are disallowed, no new attributes permitted, and deletions are blocked too. When names are enum members, we delegate to the original EnumMeta
handling to keep the error messages stable.
If your enum uses properties that alter attributes on the enum class, you'd either have to whitelist those, or allow for names starting with a single underscore to be set; in __setattr__
determine what names would be permissible to set and use super().__setattr__(name, value)
for those exceptions, just like the code now distinguishes between class construction and later alterations by using a flag attribute.
The above class can be used just like Enum()
to programmatically create an enumeration:
e = FrozenEnum('Things', [('foo',1), ('bar',2)]))
Demo:
>>> e = FrozenEnum('Things', [('foo',1), ('bar',2)])
>>> e
<enum 'Things'>
>>> e.foo = 'bar'
Traceback (most recent call last):
# ...
AttributeError: Cannot reassign members.