diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2fda0c462..a8ab4a825 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: branches: - master push: - branches: + branches: - master schedule: # Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions @@ -14,10 +14,22 @@ jobs: pytest: name: pytest runs-on: ${{matrix.os}} + continue-on-error: ${{ matrix.experimental }} strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10'] os: [ubuntu-latest, windows-latest, macos-latest] + experimental: [false] + include: + - python-version: 3.11.0-rc.1 + os: ubuntu-latest + experimental: true + - python-version: 3.11.0-rc.1 + os: windows-latest + experimental: true + - python-version: 3.11.0-rc.1 + os: macos-latest + experimental: true fail-fast: False steps: - uses: actions/checkout@v3 diff --git a/docs/source/conf.py b/docs/source/conf.py index c93f969f6..1f96883e7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -478,6 +478,10 @@ def autodoc_process_bases(app, name, obj, option, bases: list): bases.insert(0, ":class:`str`") continue + if "IntEnum" in base: + bases[idx] = ":class:`enum.IntEnum`" + continue + # Drop generics (at least for now) if base.endswith("]"): base = base.split("[", maxsplit=1)[0] diff --git a/setup.py b/setup.py index 7b9e72ffb..48f74596c 100644 --- a/setup.py +++ b/setup.py @@ -89,6 +89,7 @@ def get_setup_kwargs(raw=False): "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], python_requires=">=3.7", ) diff --git a/telegram/_utils/enum.py b/telegram/_utils/enum.py index 1a8c87371..36183d0f5 100644 --- a/telegram/_utils/enum.py +++ b/telegram/_utils/enum.py @@ -23,30 +23,54 @@ Warning: user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ -from enum import Enum +import enum as _enum +import sys from typing import Type, TypeVar, Union _A = TypeVar("_A") _B = TypeVar("_B") -_Enum = TypeVar("_Enum", bound=Enum) +_Enum = TypeVar("_Enum", bound=_enum.Enum) -def get_member(enum: Type[_Enum], value: _A, default: _B) -> Union[_Enum, _A, _B]: - """Tries to call ``enum(value)`` to convert the value into an enumeration member. +def get_member(enum_cls: Type[_Enum], value: _A, default: _B) -> Union[_Enum, _A, _B]: + """Tries to call ``enum_cls(value)`` to convert the value into an enumeration member. If that fails, the ``default`` is returned. """ try: - return enum(value) + return enum_cls(value) except ValueError: return default -class StringEnum(str, Enum): - """Helper class for string enums where the value is not important to be displayed on - stringification. +# Python 3.11 and above has a different output for mixin classes for IntEnum, StrEnum and IntFlag +# see https://docs.python.org/3.11/library/enum.html#notes. We want e.g. str(StrEnumTest.FOO) to +# return "foo" instead of "StrEnumTest.FOO", which is not the case < py3.11 +class StringEnum(str, _enum.Enum): + """Helper class for string enums where ``str(member)`` prints the value, but ``repr(member)`` + gives ``EnumName.MEMBER_NAME``. """ __slots__ = () def __repr__(self) -> str: return f"<{self.__class__.__name__}.{self.name}>" + + def __str__(self) -> str: + return str.__str__(self) + + +# Apply the __repr__ modification and __str__ fix to IntEnum +class IntEnum(_enum.IntEnum): + """Helper class for int enums where ``str(member)`` prints the value, but ``repr(member)`` + gives ``EnumName.MEMBER_NAME``. + """ + + __slots__ = () + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}.{self.name}>" + + if sys.version_info < (3, 11): + + def __str__(self) -> str: + return str(self.value) diff --git a/telegram/constants.py b/telegram/constants.py index f72ca8192..57b378132 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -25,7 +25,8 @@ enums. If they are related to a specific class, then they are also available as those classes. .. versionchanged:: 20.0 - Since v20.0, most of the constants in this module are grouped into enums. + + * Most of the constants in this module are grouped into enums. """ __all__ = [ @@ -61,10 +62,9 @@ __all__ = [ "UpdateType", ] -from enum import IntEnum from typing import List, NamedTuple -from telegram._utils.enum import StringEnum +from telegram._utils.enum import IntEnum, StringEnum class _BotAPIVersion(NamedTuple): diff --git a/tests/bots.py b/tests/bots.py index f02b0696d..69927a32c 100644 --- a/tests/bots.py +++ b/tests/bots.py @@ -58,7 +58,7 @@ def get(name, fallback): if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None: try: return BOTS[JOB_INDEX][name] - except KeyError: + except (KeyError, IndexError): pass # Otherwise go with the fallback diff --git a/tests/test_constants.py b/tests/test_constants.py index 2d346e780..c006bbd04 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import json -from enum import IntEnum import pytest from flaky import flaky from telegram import constants -from telegram._utils.enum import StringEnum +from telegram._utils.enum import IntEnum, StringEnum from telegram.error import BadRequest from tests.conftest import data_file @@ -62,8 +61,24 @@ class TestConstants: assert json.dumps(IntEnumTest.FOO) == json.dumps(1) def test_string_representation(self): + # test __repr__ assert repr(StrEnumTest.FOO) == "" - assert str(StrEnumTest.FOO) == "StrEnumTest.FOO" + + # test __format__ + assert f"{StrEnumTest.FOO} this {StrEnumTest.BAR}" == "foo this bar" + assert f"{StrEnumTest.FOO:*^10}" == "***foo****" + + # test __str__ + assert str(StrEnumTest.FOO) == "foo" + + def test_int_representation(self): + # test __repr__ + assert repr(IntEnumTest.FOO) == "" + # test __format__ + assert f"{IntEnumTest.FOO}/0 is undefined!" == "1/0 is undefined!" + assert f"{IntEnumTest.FOO:*^10}" == "****1*****" + # test __str__ + assert str(IntEnumTest.FOO) == "1" def test_string_inheritance(self): assert isinstance(StrEnumTest.FOO, str)