Skip to content

bpo-33976: support nested classes in Enum #7950

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from

Conversation

edwardcwang
Copy link

@edwardcwang edwardcwang commented Jun 27, 2018

Methods defined in Enums behave 'normally' but classes defined in Enums get mistaken for regular values and can't be used as classes out of the box. In the example code below, the nested class Outer.Inner gets mistakenly identified as an Enum value. This PR changes it so that nested classes are treated in the same way as methods, fixing this bug.

Sample code:

from enum import Enum

class Outer(Enum):
    a = 1
    b = 2
    class Inner(Enum):
        foo = 10
        bar = 11

Demonstration of bug:

>>> type(Outer)
<class 'enum.EnumMeta'>
>>> type(Outer.Inner)
<enum 'Outer'>
>>> print(Outer.Inner.foo)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Outer' object has no attribute 'foo'
>>> 

https://bugs.python.org/issue33976

@the-knights-who-say-ni
Copy link

Hello, and thanks for your contribution!

I'm a bot set up to make sure that the project can legally accept your contribution by verifying you have signed the PSF contributor agreement (CLA).

Unfortunately our records indicate you have not signed the CLA. For legal reasons we need you to sign this before we can look at your contribution. Please follow the steps outlined in the CPython devguide to rectify this issue.

When your account is ready, please add a comment in this pull request
and a Python core developer will remove the CLA not signed label
to make the bot check again.

Thanks again for your contribution, we look forward to reviewing it!

@edwardcwang
Copy link
Author

edwardcwang commented Jun 27, 2018

CLA is signed and pending.

@ethanfurman ethanfurman self-assigned this Jun 27, 2018
edwardcwang added a commit to ucb-bar/hammer that referenced this pull request Jun 27, 2018
Due to a Python bug/missing feature. Until python/cpython#7950 is merged (and Python 3.8) becomes widespread we'll have to avoid nested Enum classes.
edwardcwang added a commit to ucb-bar/hammer that referenced this pull request Jun 27, 2018
Due to a Python bug/missing feature. Until python/cpython#7950 is merged (and Python 3.8) becomes widespread we'll have to avoid nested Enum classes.
@eric-wieser
Copy link
Contributor

eric-wieser commented Jun 28, 2018

I don't think this is backwards-compatible- users might create an enum to refer to types deliberately:

class MyTypes(Enum):
	i = int
	f = float
	s = str
>>> MyTypes.i
<MyTypes.i: <class 'int'>>
>>> MyTypes.i.value
<class 'int'>

This is perhaps a little strange, but I don't think your use case is any less strange, and status quo wins.

@eric-wieser
Copy link
Contributor

Perhaps the check should be for classes actually declared within the enum, using __qual_name__

@edwardcwang edwardcwang force-pushed the enum-nested-classes branch from 5269f82 to 8c25ee3 Compare June 28, 2018 22:30
@edwardcwang
Copy link
Author

@eric-wieser Thanks for the catch. Added a unit test for your example and updated the PR to fix it.

Lib/enum.py Outdated
# accessible in _EnumDict.__setitem__().
for key, member in classdict.items():
if isinstance(member, type):
if member.__qualname__.startswith(enum_class_qualname):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catches

class MyEnumInner(object):
    pass

class MyEnum(Enum):
    oops = MyEnumInner

Should be .startswith(enum_class_qualname + '.')

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again - just fixed.

@edwardcwang
Copy link
Author

@ethanfurman Were you able to find a moment to look further into this PR? Thanks again in advance.

@ethanfurman
Copy link
Member

I have been thinking about it. At this point I'm still not sure how I want to resolve the issue. I'm thinking about having embedded/referenced classes be part of an Enum value, but also be directly callable.

For example:

MyTypes.i()

would result in 0.

For those times when one does not want a class as a member, it's easy enough to write an @skip decorater -- in fact, one is included in aenum.

edwardcwang added a commit to ucb-bar/hammer that referenced this pull request Feb 7, 2019
Due to a Python bug/missing feature. Until python/cpython#7950 is merged (and Python 3.8) becomes widespread we'll have to avoid nested Enum classes.
@ethanfurman
Copy link
Member

Edward, thanks for your work. I decided to go a different route where the work is done in _EnumDict instead (which you can find in my aenum project).

I'm closing this PR, but if you are up for a challenge I need a new PR that:

  • adds an @member decorator to force an object to become a member
  • adds an @nonmember decorator to protect an object from becoming a member
  • adds the _is_class() utility for deciding if an object is a class
  • updates EnumMeta to forward the class name to _EnumDict, and _EnumDict to store and forward the class name to _is_class()
  • issues a DeprecationWarning when a bare class is present in an Enum, and still convert it to a member (warning should mention coming change in behavior, and the @member decorator to maintain the current behavior)

@dignissimus
Copy link
Contributor

Edward, thanks for your work. I decided to go a different route where the work is done in _EnumDict instead (which you can find in my aenum project).

I'm closing this PR, but if you are up for a challenge I need a new PR that:

  • adds an @member decorator to force an object to become a member

  • adds an @nonmember decorator to protect an object from becoming a member

  • adds the _is_class() utility for deciding if an object is a class

  • updates EnumMeta to forward the class name to _EnumDict, and _EnumDict to store and forward the class name to _is_class()

  • issues a DeprecationWarning when a bare class is present in an Enum, and still convert it to a member (warning should mention coming change in behavior, and the @member decorator to maintain the current behavior)

@ethanfurman
Are these changes still required?

@ethanfurman
Copy link
Member

@edwardcwang Hey, I'm adding you to the ACKS file as "Edward C Wang" -- let me know if that should be something different.

@edwardcwang
Copy link
Author

"Edward Wang" would actually be fine too!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants