This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Incorrect behavior when subclassing enum.Enum
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.4
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: ethan.furman Nosy List: barry, eli.bendersky, ethan.furman, kissgyorgy
Priority: normal Keywords: patch

Created on 2014-09-05 00:50 by kissgyorgy, last changed 2022-04-11 14:58 by admin. This issue is now closed.

Files
File name Uploaded Description Edit
enum.patch kissgyorgy, 2014-09-05 00:50 enum.py patch
enum.patch2 kissgyorgy, 2014-09-06 05:03 enum.py line 457 using repr instead of str
Messages (11)
msg226392 - (view) Author: Kiss György (kissgyorgy) * Date: 2014-09-05 00:50
There is a small inconvenience in the ``enum`` module.
When I subclass ``enum.Enum`` and redefine the ``value`` dynamic attribute, the aliasing behavior doesn't work correctly, because ``member.value`` is used in some places instead of ``member._value_``.
I attached a patch where I fixed all these places. This causes no harm to the internal working, but makes subclassing behave correctly.
msg226393 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2014-09-05 00:56
Can you give an example of the code you were having problems with?
msg226395 - (view) Author: Kiss György (kissgyorgy) * Date: 2014-09-05 01:09
Yes, sorry I forgot about that. Here is a minimal example:


from enum import EnumMeta, Enum
from types import DynamicClassAttribute


class _MultiMeta(EnumMeta):
    def __init__(enum_class, cls, bases, classdict):
        # make sure we only have tuple values, not single values
        for member in enum_class.__members__.values():
            if not isinstance(member._value_, tuple):
                raise ValueError('{!r}, should be tuple'.format(member._value_))

    def __call__(cls, suit):
        for member in cls:
            if suit in member._value_:
                return member
        return super().__call__(suit)


class MultiValueEnum(Enum, metaclass=_MultiMeta):
    @DynamicClassAttribute
    def value(self):
        """The value of the Enum member."""
        return self._value_[0]

class IncorrectAliasBehavior(MultiValueEnum):
    first = 1, 2, 3
    second = 4, 5, 6
    alias_to_first = 1, 2, 3


When you call IncorrectAliasBehavior.alias_to_first, the documentation says it should return IncorrectAliasBehavior.first, but in this case it returns IncorrectAliasBehavior.alias_to_first, because canonical_member.value is referenced on line 162, and so it returns the redefined value, not the internally used one.
This was very confusing for me.
msg226460 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2014-09-05 22:24
My apologies for the confusion, and thanks for tracking it down.

I'll get the patch in, but I'm curious how you actually use these Enums?  Is this a way to easily handle multiple aliases?
msg226461 - (view) Author: Kiss György (kissgyorgy) * Date: 2014-09-06 00:43
> Is this a way to easily handle multiple aliases?

You mean this MultiValueEnum implementation? (If not I don't understand the question, could you elaborate?)

No, this is the opposite of aliases, because an alias is when the same value have multiple names, but this is when the values are used to lookup the same name.

I use it when very similar values have the same meaning like:

    class Suit(MultiValueEnum):
        CLUBS =    '♣', 'c', 'C', 'clubs', 'club'
        DIAMONDS = '♦', 'd', 'D', 'diamonds', 'diamond'
        HEARTS =   '♥', 'h', 'H', 'hearts', 'heart'
        SPADES =   '♠', 's', 'S', 'spades', 'spade'


Also it can be used for ECP testing (Equivalence class testing) like this:

    from http.client import responses
    class ResponseEnum(MultiValueEnum):
        GOOD = [status for status in responses if 200 <= status <= 299]
        BAD = [status for status in responses if not (200 <= status <= 299)]

I did not think about this use case, but I think it's very interesting and useful.
msg226468 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2014-09-06 02:54
Right.  We can still use the alias machinery to accomplish this task for us, and avoid the metaclass hacking:

-- python2 sample code ------------------------------------
# -*- coding: utf-8 -*-
from enum import Enum

class MultiValueEnum(Enum):
    def __new__(cls, *values):
        obj = object.__new__(cls)
        obj._value_ = values[0]
        obj._all_values = values
        for alias in values[1:]:
            cls._value2member_map_[alias] = obj
        return obj
    def __repr__(self):
        return "<%s.%s: %s>" % (
             self.__class__.__name__,
             self._name_,
             ', '.join(["'%s'" % v for v in self._all_values])
             )

class Suits(MultiValueEnum):
    CLUBS =    '♣', 'c', 'C', 'clubs', 'club'
    DIAMONDS = '♦', 'd', 'D', 'diamonds', 'diamond'
    HEARTS =   '♥', 'h', 'H', 'hearts', 'heart'
    SPADES =   '♠', 's', 'S', 'spades', 'spade'

print(Suits.HEARTS)
print(repr(Suits.HEARTS))
print(Suits('d'))
print(Suits('club'))
print(Suits('S'))
print(Suits('hearts'))

----------------------------------------------------------------

And the output:

Suits.HEARTS
<Suits.HEARTS: '♥', 'h', 'H', 'hearts', 'heart'>
Suits.DIAMONDS
Suits.CLUBS
Suits.SPADES
Suits.HEARTS

----------------------------------------------------------------

I'm still going to fix the bug, though.  :)

Oh, the above does not fix the IncorrectAliasBehavior class, but honestly I'm not sure what you are trying to accomplish there.
msg226470 - (view) Author: Kiss György (kissgyorgy) * Date: 2014-09-06 05:03
Oh, wow. I never really understood what _find_new_ did, now I do.

> I'm not sure what you are trying to accomplish there.
Exactly that, I'm just not as good. Oh well at least I found a bug! :) Thanks for the enlightenment!

If the patch goes in, I also would like one more minor change. On line 457, the missing value is represented as simple str(). In case of user defined types, it would make debugging easier if repr() would be used instead. I attached the patch.
msg226491 - (view) Author: Kiss György (kissgyorgy) * Date: 2014-09-06 14:26
I found one thing which you can't do subclassing Enum what you can with metaclasses:
enforcing type checking at class creation time. Values are passed to __new__ as positional arguments, so it's impossible to tell the difference between these two:

class SingleValue(MultiVAlueEnum):
    one = 1, 'one'
    two = 2


class Tuple(MultiVAlueEnum):
    one = 1, 'one'
    two = 2,

because in both cases (2,) would be passed. It's not a big deal, but "Explicit is better than implicit." and also I would like to avoid typos, which I often make like this:

class StrValues(MultiValueEnum):
    one = ('One'
          'one')
    two = ('two',
          'Two')

In this case, the first member would be accepted as 'Oneone' instead of ('One', 'one') and I see no way to check that without metaclasses. Do you?
msg226501 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2014-09-06 18:34
You could do the same kind of check in __new__, but consider this:

class StrValues(MultiValueEnum):
    one = ('One'
          'one',
           '1')
    two = ('two',
          'Two',
           '2')

In this scenario the 'Oneone' mistake would still not be automatically caught.  There are the two ways I deal with this type of problem:

  - unit tests
  - formatting

The formatting looks like this:

class StrValues(MultiValueEnum):
    one = (
        'One'
        'one',
        '1',
        )
    two = (
        'two',
        'Two',
        '2',
        )

This style of format does several things for us:

  - easier to read the code:
     - each value is on it's own line
     - each value is lined up
     - there is white space between the values of one attribute and
       the values of the next attribute

  - easier to read diffs, as we don't see extraneous stuff like

-  '2'
+  '2',

  - easier to spot mistakes, since we get used to seeing that trailing
    comma and it's absence will stand out.
msg226584 - (view) Author: Kiss György (kissgyorgy) * Date: 2014-09-08 15:25
Thanks for the tip! That looks much better.
msg226973 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2014-09-17 02:07
http://hg.python.org/cpython/rev/4135f3929b35
http://hg.python.org/cpython/rev/cdd412347827
History
Date User Action Args
2022-04-11 14:58:07adminsetgithub: 66535
2014-09-17 02:07:13ethan.furmansetstatus: open -> closed
resolution: fixed
messages: + msg226973

stage: patch review -> resolved
2014-09-08 15:25:51kissgyorgysetmessages: + msg226584
2014-09-06 18:34:54ethan.furmansetmessages: + msg226501
2014-09-06 14:26:25kissgyorgysetmessages: + msg226491
2014-09-06 05:03:39kissgyorgysetfiles: + enum.patch2

messages: + msg226470
2014-09-06 02:54:19ethan.furmansettype: enhancement -> behavior
messages: + msg226468
stage: patch review
2014-09-06 00:43:35kissgyorgysetmessages: + msg226461
2014-09-05 22:24:32ethan.furmansetmessages: + msg226460
2014-09-05 01:09:03kissgyorgysetmessages: + msg226395
2014-09-05 00:57:14ethan.furmansetnosy: + barry, eli.bendersky
2014-09-05 00:56:55ethan.furmansetassignee: ethan.furman

messages: + msg226393
nosy: + ethan.furman
2014-09-05 00:50:11kissgyorgycreate