Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I detect and invoke a function when a python enum member is accessed

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, a DeprecationWarning is issued using warnings.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") and Foo["BAZ"] should raise a DeprecationWarning.

Things I have tried, but failed:

  • Overwrite _missing_ and don't define BAZ. 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 accessing Foo.BAZ. I guess this could work if I could overwrite __getattr__ of EnumMeta and then make Enum 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.

like image 602
RunOrVeith Avatar asked Jun 10 '20 09:06

RunOrVeith


People also ask

Are enums iterable Python?

Printing enum as an iterable We can print the enum as an iterable list. In the below code we use a for loop to print all enum members.

What is Auto in enum Python?

Syntax : enum.auto() Automatically assign the integer value to the values of enum class attributes. Example #1 : In this example we can see that by using enum. auto() method, we are able to assign the numerical values automatically to the class attributes by using this method.

Should I use enums in Python?

Python enums are useful to represent data that represent a finite set of states such as days of the week, months of the year, etc. They were added to Python 3.4 via PEP 435. However, it is available all the way back to 2.4 via pypy. As such, you can expect them to be a staple as you explore Python programming.


1 Answers

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.

like image 97
Ethan Furman Avatar answered Oct 31 '22 18:10

Ethan Furman