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

Python 3.11 changes behavior how enums are formatted as string #447

Closed
OrangeTux opened this issue Jun 30, 2023 · 6 comments
Closed

Python 3.11 changes behavior how enums are formatted as string #447

OrangeTux opened this issue Jun 30, 2023 · 6 comments
Labels

Comments

@OrangeTux
Copy link
Collaborator

OrangeTux commented Jun 30, 2023

@proelke made me aware about a changes to Python 3.11 that introduce subtle changes to all enums that subclass from both str and enum.Enum. Practically all enums defined in this library are subclassed from these 2 classes.

Consider the following code:

import enum

class ResetType(str, enum.Enum):
    hard = "hard"
    soft = "soft"

# Passes in both Python 3.11 and earlier.
assert ResetType.hard == "hard"

# Passes in Python 3.10 and below, but fails in Python 3.11.
assert f'{ResetType.hard}' == "hard"
$  python3.11 /enum_test.py
Traceback (most recent call last):
  File "/tmp/enum_test.py", line 15, in <module>
    assert f'{ResetType.hard}' == "hard"
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError

The underlying cause is that Python 3.11 changes the behavior of enum.Enum.__format__() slightly. f-strings (and a few other string formatting methods) rely on dunder method __format___().

So far, I considered two solutions.

Solution 1: StrEnum

Python 3.11 introduces enum.StrEnum. One could implement a solution as described at https://tomwojcik.com/posts/2023-01-02/python-311-str-enum-breaking-change

try:
    # breaking change introduced in python 3.11
    from enum import StrEnum
except ImportError:  # pragma: no cover
    from enum import Enum  # pragma: no cover

    class StrEnum(str, Enum):  # pragma: no cover
        pass  # pragma: no cover

class ResetType(StrEnum):
    hard = "hard"
    soft = "soft"

# Passes in both Python 3.11 and earlier.
assert ResetType.hard == "hard"

# Passes now in Python 3.10 and below and Python 3.11!
assert f'{ResetType.hard}' == "hard"

Manually implement __format__()

The other solution is to implement __format__() manually.

import enum

class _Enum(str, enum.Enum):
    def __format__(self, spec) -> str:
        return str.__format__(str(self.name), spec)

class ResetType(_Enum):
    hard = "hard"
    soft = "soft"

# Passes in both Python 3.11 and earlier.
assert ResetType.hard == "hard"

# Passes now in Python 3.10 and below and Python 3.11!
assert f"{ResetType.hard}" == "hard"

See also

@OrangeTux OrangeTux added the bug Something isn't working label Jun 30, 2023
@larrykluger
Copy link

A regression error introduced in a .01 update! Good grief.

@Jared-Newell-Mobility
Copy link
Contributor

@OrangeTux - I would suggest we go with Solution 1 - in looks cleaner and more logical moving forward with new Python versions; that at some stage, long into the future, the ImportError handling could be removed without refactoring the rest of the code?

@Jared-Newell-Mobility
Copy link
Contributor

Jared-Newell-Mobility commented Nov 8, 2023

@OrangeTux - Another option may be this, I think this could be non-breaking too (introduced in 3.6)?

import enum

def test_this():

    class ResetType(str, enum.auto):
        hard = "hard"
        soft = "soft"

    print(ResetType.hard == "hard")
    print(f'{type(ResetType.hard)}')

    # Passes in Python 3.10 and Python 3.11.
    assert f'{ResetType.hard}' == "hard"


===================================================================================================== test session starts ======================================================================================================
platform linux -- Python 3.10.12, pytest-7.4.3, pluggy-1.3.0
rootdir: /home/jared/PycharmProjects/test_project/tests
collected 1 item

test_enum.py . [100%]

====================================================================================================== 1 passed in 0.00s =======================================================================================================

===================================================================================================== test session starts ======================================================================================================
platform linux -- Python 3.11.5, pytest-7.4.3, pluggy-1.3.0
rootdir: /home/jared/PycharmProjects/test_project
collected 1 item

tests/test_enum.py . [100%]

====================================================================================================== 1 passed in 0.00s =======================================================================================================

@Jared-Newell-Mobility
Copy link
Contributor

Related ticket has this same behaviour, see comments in #433

@OrangeTux
Copy link
Collaborator Author

That's a clever solution. I didn't knew of this enum.auto and it's behavior. But I'm afraid that doesn't work for 2 reason:

  • my demo code contains a bug. The members string should be capitalized, so "Hard" instead of "hard"
  • a lot of enums in this project consists of multiple terms, e.g. FirmwareStatus.download_failed which should match the string "DownloadFailed". enum.auto does't work in this case.

@OrangeTux
Copy link
Collaborator Author

I suggest that we pick Solution #1.

Jared-Newell-Mobility added a commit that referenced this issue Jan 17, 2024
- [#544](#544) Pass
`Call.unique_id` to the `on` and `after` routing handlers.
- [#559](#559) Update
project dependencies as of 22-12-2023
- [#447](#447) Make
formatting of enums in py3.11 consistent with earlier Python versions
- [#421](#421) Type of
v16.datatypes.SampledValue.context is incorrect
RoaringDev1203 added a commit to RoaringDev1203/Cratus-OCPP that referenced this issue Aug 27, 2024
- [#544](mobilityhouse/ocpp#544) Pass
`Call.unique_id` to the `on` and `after` routing handlers.
- [#559](mobilityhouse/ocpp#559) Update
project dependencies as of 22-12-2023
- [#447](mobilityhouse/ocpp#447) Make
formatting of enums in py3.11 consistent with earlier Python versions
- [#421](mobilityhouse/ocpp#421) Type of
v16.datatypes.SampledValue.context is incorrect
csharplus added a commit to mage-ai/mage-ai that referenced this issue Sep 9, 2024
…ith Python 3.11+ (#5397)

# Description

Fix the following unit test failure when running with Python 3.11+. More
background information about this issue is discussed in
mobilityhouse/ocpp#447.

```
FAIL: test_create_endpoint_dbt_yaml_with_parent_pipelines (tests.api.endpoints.test_blocks.DBTBlockAPIEndpointTest.test_create_endpoint_dbt_yaml_with_parent_pipelines)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/Cellar/python@3.12/3.12.5/Frameworks/Python.framework/Versions/3.12/lib/python3.12/unittest/async_case.py", line 90, in _callTestMethod
    if self._callMaybeAsync(method) is not None:
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/python@3.12/3.12.5/Frameworks/Python.framework/Versions/3.12/lib/python3.12/unittest/async_case.py", line 112, in _callMaybeAsync
    return self._asyncioRunner.run(
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/python@3.12/3.12.5/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/python@3.12/3.12.5/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/Users/hw/workspace/dev/mage-ai/mage_ai/tests/api/endpoints/mixins.py", line 247, in _test_create_endpoint
    await self.build_test_create_endpoint(
  File "/Users/hw/workspace/dev/mage-ai/mage_ai/tests/api/endpoints/mixins.py", line 813, in build_test_create_endpoint
    validation = assert_after_create_count(self)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/hw/workspace/dev/mage-ai/mage_ai/tests/api/endpoints/test_blocks.py", line 154, in __assert_after_create_yaml
    self.assertEqual({
AssertionError: {'dbt[56 chars]h': 'dbts/dbt_yaml.yaml', 'project_path': 'dbt/test_project'}} != {'dbt[56 chars]h': 'BlockType.DBTs/dbt_yaml.yaml', 'project_p[21 chars]ct'}}
  {'dbt_project_name': 'dbt/test_project',
-  'file_source': {'path': 'dbts/dbt_yaml.yaml',
?                           ^^^

+  'file_source': {'path': 'BlockType.DBTs/dbt_yaml.yaml',
?                           ^^^^^^^^^^^^^

                   'project_path': 'dbt/test_project'}}

----------------------------------------------------------------------
Ran 27 tests in 2.061s

FAILED (failures=1)
```

# How Has This Been Tested?
<!-- Please describe the tests that you ran to verify your changes.
Provide instructions so we can reproduce.
-->

- [X] Tested with a local development environment.

# Checklist
- [ ] The PR is tagged with proper labels (bug, enhancement, feature,
documentation)
- [X] I have performed a self-review of my own code
- [ ] I have added unit tests that prove my fix is effective or that my
feature works
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation

cc:
<!-- Optionally mention someone to let them know about this pull request
-->
@wangxiaoyou1993
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

3 participants