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

Enum functional syntax allows creation of invalid member names #105906

Closed
flxdot opened this issue Jun 19, 2023 · 6 comments
Closed

Enum functional syntax allows creation of invalid member names #105906

flxdot opened this issue Jun 19, 2023 · 6 comments
Assignees
Labels
stdlib Python modules in the Lib dir type-feature A feature request or enhancement

Comments

@flxdot
Copy link

flxdot commented Jun 19, 2023

Bug report

I discovered this behavior when we tried to dynamically create an Enum for all timezones available from zoneinfo.available_timezones().

According to the docs, the functional syntax work as such:

# functional syntax
Color = Enum('Color', ['RED', 'GREEN', 'BLUE'])

Using this knowledge to create StrEnum, this allows me to create the following Enum:

from enum import Enum, StrEnum

InvalidEnum = StrEnum(
    "InvalidEnum", {"America/Chicago": "America/Chicago", "123": "123"}
)

# Same applies for the much simpler `Enum` as well
InvalidToo = Enum("InvalidToo", ["America/Chicago", "123"])

This works without any errors. As we can see from the following code snippet it also works as expected:

assert InvalidEnum("America/Chicago") == "America/Chicago"
assert InvalidEnum("123") == "123"

But when trying to access the members of the Enum directly, the invalid member names become obvious:

a = InvalidEnum.America/Chicago  # raises an AttributeError
a = InvalidEnum.123  # raises an SyntaxError

I think the expected behavior here would be to raise a ValueError, similar to when trying to create an Enum with an empty string as member name:

StrEnum("EnumWithEmptyString", {"": "EMPTY"})

The reasoning here would be: The class syntax (obviously - due to syntax errors) prevents me from defining this enum. The functional syntax should behave in a similar manor. ValueError instead of SyntaxError, as it can only be evaluated during run time.

class InvalidEnumClass(StrEnum):
    America/Chicago = "America/Chicago"
    123 = "123"

Your environment

  • CPython versions tested on: 3.11
  • Operating system and architecture: macOS 13.3.1 (a) / Apple M1
@flxdot flxdot added the type-bug An unexpected behavior, bug, or error label Jun 19, 2023
@sunmy2019
Copy link
Member

If your code all uses InvalidEnum("America/Chicago"), everything is fine.

So why bother?

For example, you cannot use the normal syntax foo.1, but you can still access it with getattr(foo, "1") setattr(foo, "1", ...).

I think disallowing it will restrict too much on users. This is not usually what Python would do.

I would be slightly against this change.


@ethanfurman

@sunmy2019 sunmy2019 added type-feature A feature request or enhancement stdlib Python modules in the Lib dir and removed type-bug An unexpected behavior, bug, or error labels Jun 19, 2023
@TeamSpen210
Copy link

You can also do this with regular classes, by assigning to locals(), or simply using setattr(). This was actually brought up with the Steering Council in September last year, with a consensus that it should be kept as an official feature. It's also mentioned now in the docs for setattr().

@flxdot
Copy link
Author

flxdot commented Jun 19, 2023

Interesting. Makes sense to me if argued in that manner.
But from a user perspective, this raises some questions for me.

This was actually brought up with the Steering Council in September last year, with a consensus that it should be kept as an official feature.

The behavior is not consistent. Whereas StrEnum("EnumWithEmptyString", {"": "EMPTY"}) raises a ValueError, setattr(InvalidEnum, "", "Empty") does not. So I would argue, if it was decided for the functional syntax to enforce the Identifier restrictions, it should do it consistently.

I think disallowing it will restrict too much on users. This is not usually what Python would do.

Fair point. But this would not restrict the users. It would introduce a breaking API change for some.
As you mentioned one can still make use of setattr() and getattr(). But It would help to prevent inexperienced developers to shoot them selfs in the foot while still offering the experienced devs to do all kinds of nasty things.

I think it comes down to the point of what the purpose of an Enum is. I see Enum as

  • a way to runtime check/enforce variants of something
  • a way to hide magic string constants
  • a tool used in combination with IDEs to suggest possible variants, instead of forcing me to remember or lookup valid variants

Based on these parameters (which are somewhat my personal view of course), I would argue that using the suggested "workarounds" getattr(InvalidEnum, "America/Chicago"), InvalidEnum("America/Chicago") defeats point 2 & 3.

@ethanfurman
Copy link
Member

I've now read the referenced SC discussion. Their decision makes sense, but Enum is a strange beast and I don't mind being more restrictive (as Enum already is in other areas).

Having said that, I need to think about whether I want to validate every member name, or allow an empty string as a name, as consistency in this case certainly seems reasonable.

@ethanfurman ethanfurman self-assigned this Jun 19, 2023
@sobolevn
Copy link
Member

sobolevn commented Jun 20, 2023

Just a quick note that this would be a breaking change, because some people might rely on it:

Python 3.11.3 (main, May 27 2023, 12:23:35) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from enum import Enum, StrEnum
>>> 
>>> InvalidEnum = StrEnum(
...     "InvalidEnum", {"America/Chicago": "America/Chicago", "123": "123"}
... )
>>> 

I would also like to highlight that you can still use []:

>>> InvalidEnum["America/Chicago"]
<InvalidEnum.America/Chicago: 'America/Chicago'>

(in my opinion, there's no bug to fix here)

@ethanfurman
Copy link
Member

I agree. Closing.

@terryjreedy terryjreedy closed this as not planned Won't fix, can't repro, duplicate, stale Jul 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

6 participants