问题
I have an enum for which some of the members are deprecated:
from enum import Enum
class Foo(Enum):
BAR = "bar"
BAZ = "baz" # deprecated
How do it get the following behavior:
- When somebody writes
Foo.BAR
, everything behaves normally - When somebody writes
Foo.BAZ
, aDeprecationWarning
is issued usingwarnings.warn("BAZ is deprecated", DeprecationWarning)
. Afterwards everything behaves normally. - The same behavior should apply when members are accessed in other ways, e.g.
Foo("baz")
andFoo["BAZ"]
should raise aDeprecationWarning
.
Things I have tried, but failed:
- Overwrite
_missing_
and don't defineBAZ
. Does not work, because in the end I still need to return an existing member for a while (until our DB is cleaned of the deprecated value). But I can not dynamically add members to an enum. If I define it,_missing_
is not called. - overwrite any of
__getattr__
,__getattribute__
. These are called when accessing attributes of a member, e.g.Foo.BAZ.boo
, not when accessingFoo.BAZ
. I guess this could work if I could overwrite__getattr__
ofEnumMeta
and then makeEnum
use the child meta class. However, I don't see how that can be done either - overwrite
__class_getitem__
: Reserved for static typing and not called anyways. - Abuse
_generate_next_value_
. This function is only called on class creation, so I can get a deprecation warning when the class is called once, regardless of whether the deprecated member is called or not. But that is not what I want. - Look at this question. It does not solve my problem, as the goal there is filtering of deprecated members during iteration.
TLDR: How can I detect and invoke a function when an enum member is accessed?
I am working with python 3.8, so new features are fine.
回答1:
This appears to be one of those times when subclassing EnumMeta is the right thing to do.
The new metaclass will run an _on_access
method, if it exists, whenever a member is accessed:
class OnAccess(EnumMeta):
"""
runs a user-specified function whenever member is accessed
"""
#
def __getattribute__(cls, name):
obj = super().__getattribute__(name)
if isinstance(obj, Enum) and obj._on_access:
obj._on_access()
return obj
#
def __getitem__(cls, name):
member = super().__getitem__(name)
if member._on_access:
member._on_access()
return member
#
def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1):
obj = super().__call__(value, names, module=module, qualname=qualname, type=type, start=start)
if isinstance(obj, Enum) and obj._on_access:
obj._on_access()
return obj
The new base Enum
treats any extra arguments on member creation as arguments for a deprecate
function, and sets the _on_access
attribute to that function only if extra arguments are given:
class DeprecatedEnum(Enum, metaclass=OnAccess):
#
def __new__(cls, value, *args):
member = object.__new__(cls)
member._value_ = value
member._args = args
member._on_access = member.deprecate if args else None
return member
#
def deprecate(self):
args = (self.name, ) + self._args
import warnings
warnings.warn(
"member %r is deprecated; %s" % args,
DeprecationWarning,
stacklevel=3,
)
And our example Enum
with deprecated members:
class Foo(DeprecatedEnum):
BAR = "bar"
BAZ = "baz", "use something else"
And the warnings (from a test script):
# no warning here
list(Foo)
# nor for non-deprecated members
Foo.BAR
# but direct use of deprecated members does generate warnings
Foo.BAZ
/home/ethan/test:74: DeprecationWarning: member 'BAZ' is deprecated; use something else
Foo.BAZ
Foo('baz')
/home/ethan/test:75: DeprecationWarning: member 'BAZ' is deprecated; use something else
Foo('baz')
Foo['BAZ']
/home/ethan/test:76: DeprecationWarning: member 'BAZ' is deprecated; use something else
Foo['BAZ']
And all the deprecated members in Foo
:
>>> print([m.name for m in Foo if m._args])
['BAZ']
Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.
来源:https://stackoverflow.com/questions/62299740/how-do-i-detect-and-invoke-a-function-when-a-python-enum-member-is-accessed