Skip to content
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

3.11.5 Regression: Flags with custom __new__ can't be iterated #108682

Closed
2 tasks done
ziima opened this issue Aug 30, 2023 · 5 comments
Closed
2 tasks done

3.11.5 Regression: Flags with custom __new__ can't be iterated #108682

ziima opened this issue Aug 30, 2023 · 5 comments
Assignees
Labels
3.11 only security fixes 3.12 bugs and security fixes 3.13 bugs and security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@ziima
Copy link
Contributor

ziima commented Aug 30, 2023

Bug report

Checklist

  • I am confident this is a bug in CPython, not a bug in a third-party project
  • I have searched the CPython issue tracker,
    and am confident this bug has not been reported before

CPython versions tested on:

3.11

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.11.5 (main, Aug 25 2023, 23:47:33) [GCC 12.2.0]

A clear and concise description of the bug:

A behavior of Flag changed in python 3.11.5. When Flag with custom __new__ is used in combination with functional API, it's members can't be retrieved by iteration over the class.

MWE:

from enum import IntFlag

Perm = IntFlag('Perm', {'R': 4, 'W': 2, 'X': 1})
print(tuple(Perm))


class LabeledFlag(IntFlag):
    def __new__(cls, value: int, label: str):
        obj = super().__new__(cls, value)
        obj._value_ = value
        obj.label = label
        return obj


LabeledPerm = LabeledFlag('LabeledPerm', {'R': (4, 'Read'), 'W': (2, 'Write'), 'X': (1, 'Exec')})
print(tuple(LabeledPerm))

The output in python 3.11.4:

(<Perm.R: 4>, <Perm.W: 2>, <Perm.X: 1>)
(<LabeledPerm.R: 4>, <LabeledPerm.W: 2>, <LabeledPerm.X: 1>)

The output in python 3.11.5:

(<Perm.R: 4>, <Perm.W: 2>, <Perm.X: 1>)
()

I suspect this commit to introduce the regression: 59f009e

A workaround for 3.11.5:

class LabeledFlag(IntFlag):
    def __new__(cls, value: int, label: str):
        obj = super().__new__(cls, value)
        obj._value_ = value
        obj.label = label
        return obj

    @classmethod
    def _missing_(cls, value):
        pseudo_member = super(_DnskeyFlagBase, cls)._missing_(value)
        if value in cls._value2member_map_ and pseudo_member.name is None:
            cls._value2member_map_.pop(value)
        return pseudo_member

Linked PRs

@ziima ziima added the type-bug An unexpected behavior, bug, or error label Aug 30, 2023
@AlexWaygood AlexWaygood added stdlib Python modules in the Lib dir 3.11 only security fixes 3.12 bugs and security fixes 3.13 bugs and security fixes labels Aug 30, 2023
@ethanfurman
Copy link
Member

This ended up being a combination of user error, program error, and some not-detailed-enough documentation.

An enum's custom __new__ should not use super(...).__new__(), because the parent's __new__ is only for lookups; either use the data type directly:

def __new__(cls, value, label):
    obj = int.__new__(cls, value)

or, if using an already defined enum type (i.e. LabeledFlag(IntFlag) and not LabeledFlag(int, Flag)):

def __new__(cls, value, label):
    obj = super()._new_member_(cls, value)

The code has been updated to raise a TypeError if super().__new__ is called, and a warning added to the docs.

@ethanfurman
Copy link
Member

@ziima: the correct fix for your code:

obj = int.__new__(cls, value)

which works across all Python versions.

@ziima
Copy link
Contributor Author

ziima commented Aug 31, 2023

Works like a charm, thanks @ethanfurman.

I think I actually tried that, but I may had had other error in there at that moment.

ethanfurman added a commit that referenced this issue Aug 31, 2023
… __new__ (GH-108704)

When overriding the `__new__` method of an enum, the underlying data type should be created directly; i.e. .

    member = object.__new__(cls)
    member = int.__new__(cls, value)
    member = str.__new__(cls, value)

Calling `super().__new__()` finds the lookup version of `Enum.__new__`, and will now raise an exception when detected.
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Aug 31, 2023
…custom __new__ (pythonGH-108704)

When overriding the `__new__` method of an enum, the underlying data type should be created directly; i.e. .

    member = object.__new__(cls)
    member = int.__new__(cls, value)
    member = str.__new__(cls, value)

Calling `super().__new__()` finds the lookup version of `Enum.__new__`, and will now raise an exception when detected.
(cherry picked from commit d48760b)

Co-authored-by: Ethan Furman <[email protected]>
Yhg1s pushed a commit that referenced this issue Aug 31, 2023
… custom __new__ (GH-108704) (#108733)

gh-108682: [Enum] raise TypeError if super().__new__ called in custom __new__ (GH-108704)

When overriding the `__new__` method of an enum, the underlying data type should be created directly; i.e. .

    member = object.__new__(cls)
    member = int.__new__(cls, value)
    member = str.__new__(cls, value)

Calling `super().__new__()` finds the lookup version of `Enum.__new__`, and will now raise an exception when detected.
(cherry picked from commit d48760b)

Co-authored-by: Ethan Furman <[email protected]>
ethanfurman added a commit to ethanfurman/cpython that referenced this issue Aug 31, 2023
…custom __new__ (pythonGH-108704)

When overriding the `__new__` method of an enum, the underlying data type should be created directly; i.e. .

    member = object.__new__(cls)
    member = int.__new__(cls, value)
    member = str.__new__(cls, value)

Calling `super().__new__()` finds the lookup version of `Enum.__new__`, and will now raise an exception when detected.

(cherry picked from commit d48760b)
@cdce8p
Copy link
Contributor

cdce8p commented Sep 6, 2023

Seems to be a breaking change. Not something would have expected in an .rc2 release to be honest.
In particular raising a TypeError for enums without members worked fine before.

Haven't had the time to look at it more closely, but noticed our test suite failing once again after it had been fixed for 3.12.0rc1, especially regarding the enum changes.

ethanfurman added a commit that referenced this issue Sep 8, 2023
… custom __new__ (GH-108704) (GH-108739)

When overriding the `__new__` method of an enum, the underlying data type should be created directly; i.e. .

    member = object.__new__(cls)
    member = int.__new__(cls, value)
    member = str.__new__(cls, value)

Calling `super().__new__()` finds the lookup version of `Enum.__new__`, and will now raise an exception when detected.

(cherry picked from commit d48760b)
@ethanfurman
Copy link
Member

For posterity: the issue is that zigpy has empty enums, but uses a custom _missing_ to create their members on demand. This PR broke that, but I consider that behavior to be anti-enum, and keeping it also means keeping the bug in this issue.

The solution for zigpy is to move the desired behavior into a custom metaclass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.11 only security fixes 3.12 bugs and security fixes 3.13 bugs and security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants