From 9ce0f498823c142599b6f722be83d212f17c3283 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:25:02 -0400 Subject: [PATCH] Add Support for Python 3.13 Beta (#4253) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/type_completeness.yml | 4 +- .github/workflows/unit_tests.yml | 2 +- pyproject.toml | 3 + telegram/_chatfullinfo.py | 8 +-- telegram/_giveaway.py | 2 +- telegram/_inline/inputtextmessagecontent.py | 2 +- telegram/_update.py | 4 +- telegram/ext/_application.py | 69 +++++++++++---------- tests/auxil/pytest_classes.py | 6 +- tests/conftest.py | 6 +- tests/ext/test_application.py | 62 +++++++++++++++--- tests/request/test_request.py | 11 ++-- tests/test_bot.py | 4 +- tests/test_message.py | 6 +- 14 files changed, 123 insertions(+), 66 deletions(-) diff --git a/.github/workflows/type_completeness.yml b/.github/workflows/type_completeness.yml index 74087e3e8..4a98c0b30 100644 --- a/.github/workflows/type_completeness.yml +++ b/.github/workflows/type_completeness.yml @@ -18,12 +18,12 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 cache: 'pip' cache-dependency-path: '**/requirements*.txt' - name: Install Pyright run: | - python -W ignore -m pip install pyright~=1.1.316 + python -W ignore -m pip install pyright~=1.1.367 - name: Get PR Completeness # Must run before base completeness, as base completeness will checkout the base branch # And we can't go back to the PR branch after that in case the PR is coming from a fork diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 8e1c7bb06..214eca12b 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-beta.2'] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: False steps: diff --git a/pyproject.toml b/pyproject.toml index 4f40bd910..13ae98395 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ "httpx ~= 0.27", @@ -82,6 +83,8 @@ job-queue = [ ] passport = [ "cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1", + # cffi is a dependency of cryptography and added support for python 3.13 in 1.17.0rc1 + "cffi >= 1.17.0rc1; python_version > '3.12'" ] rate-limiter = [ "aiolimiter~=1.1.0", diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index 213baed7e..3458f6fa6 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -490,10 +490,10 @@ class ChatFullInfo(_ChatBase): self.unrestrict_boost_count: Optional[int] = unrestrict_boost_count self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name self.birthdate: Optional[Birthdate] = birthdate - self.personal_chat: Optional["Chat"] = personal_chat - self.business_intro: Optional["BusinessIntro"] = business_intro - self.business_location: Optional["BusinessLocation"] = business_location - self.business_opening_hours: Optional["BusinessOpeningHours"] = business_opening_hours + self.personal_chat: Optional[Chat] = personal_chat + self.business_intro: Optional[BusinessIntro] = business_intro + self.business_location: Optional[BusinessLocation] = business_location + self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatFullInfo"]: diff --git a/telegram/_giveaway.py b/telegram/_giveaway.py index 325189803..ed6d4a288 100644 --- a/telegram/_giveaway.py +++ b/telegram/_giveaway.py @@ -313,7 +313,7 @@ class GiveawayCompleted(TelegramObject): self.winner_count: int = winner_count self.unclaimed_prize_count: Optional[int] = unclaimed_prize_count - self.giveaway_message: Optional["Message"] = giveaway_message + self.giveaway_message: Optional[Message] = giveaway_message self._id_attrs = ( self.winner_count, diff --git a/telegram/_inline/inputtextmessagecontent.py b/telegram/_inline/inputtextmessagecontent.py index 0e127ce70..475f9c5bb 100644 --- a/telegram/_inline/inputtextmessagecontent.py +++ b/telegram/_inline/inputtextmessagecontent.py @@ -108,7 +108,7 @@ class InputTextMessageContent(InputMessageContent): # Optionals self.parse_mode: ODVInput[str] = parse_mode self.entities: Tuple[MessageEntity, ...] = parse_sequence_arg(entities) - self.link_preview_options: ODVInput["LinkPreviewOptions"] = parse_lpo_and_dwpp( + self.link_preview_options: ODVInput[LinkPreviewOptions] = parse_lpo_and_dwpp( disable_web_page_preview, link_preview_options ) diff --git a/telegram/_update.py b/telegram/_update.py index 784dea52a..68ff52649 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -446,7 +446,7 @@ class Update(TelegramObject): ) self._effective_user: Optional[User] = None - self._effective_sender: Optional[Union["User", "Chat"]] = None + self._effective_sender: Optional[Union[User, Chat]] = None self._effective_chat: Optional[Chat] = None self._effective_message: Optional[Message] = None @@ -568,7 +568,7 @@ class Update(TelegramObject): if self._effective_sender: return self._effective_sender - sender: Optional[Union["User", "Chat"]] = None + sender: Optional[Union[User, Chat]] = None if message := ( self.message diff --git a/telegram/ext/_application.py b/telegram/ext/_application.py index f5a9d6df4..4f623ed36 100644 --- a/telegram/ext/_application.py +++ b/telegram/ext/_application.py @@ -251,39 +251,44 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica """ __slots__ = ( - "__create_task_tasks", - "__update_fetcher_task", - "__update_persistence_event", - "__update_persistence_lock", - "__update_persistence_task", + ( # noqa: RUF005 + "__create_task_tasks", + "__update_fetcher_task", + "__update_persistence_event", + "__update_persistence_lock", + "__update_persistence_task", + "__stop_running_marker", + "_chat_data", + "_chat_ids_to_be_deleted_in_persistence", + "_chat_ids_to_be_updated_in_persistence", + "_conversation_handler_conversations", + "_initialized", + "_job_queue", + "_running", + "_update_processor", + "_user_data", + "_user_ids_to_be_deleted_in_persistence", + "_user_ids_to_be_updated_in_persistence", + "bot", + "bot_data", + "chat_data", + "context_types", + "error_handlers", + "handlers", + "persistence", + "post_init", + "post_shutdown", + "post_stop", + "update_queue", + "updater", + "user_data", + ) # Allowing '__weakref__' creation here since we need it for the JobQueue - # Uncomment if necessary - currently the __weakref__ slot is already created - # in the AsyncContextManager base class - # "__weakref__", - "_chat_data", - "_chat_ids_to_be_deleted_in_persistence", - "_chat_ids_to_be_updated_in_persistence", - "_conversation_handler_conversations", - "_initialized", - "_job_queue", - "_running", - "_update_processor", - "_user_data", - "_user_ids_to_be_deleted_in_persistence", - "_user_ids_to_be_updated_in_persistence", - "bot", - "bot_data", - "chat_data", - "context_types", - "error_handlers", - "handlers", - "persistence", - "post_init", - "post_shutdown", - "post_stop", - "update_queue", - "updater", - "user_data", + # Currently the __weakref__ slot is already created + # in the AsyncContextManager base class for pythons < 3.13 + + ("__weakref__",) + if sys.version_info >= (3, 13) + else () ) def __init__( diff --git a/tests/auxil/pytest_classes.py b/tests/auxil/pytest_classes.py index 5586a8ea0..1b976b02e 100644 --- a/tests/auxil/pytest_classes.py +++ b/tests/auxil/pytest_classes.py @@ -21,7 +21,7 @@ modify behavior of the respective parent classes in order to make them easier to pytest framework. A common change is to allow monkeypatching of the class members by not enforcing slots in the subclasses.""" from telegram import Bot, Message, User -from telegram.ext import Application, ExtBot +from telegram.ext import Application, ExtBot, Updater from tests.auxil.ci_bots import BOT_INFO_PROVIDER from tests.auxil.constants import PRIVATE_KEY from tests.auxil.envvars import TEST_WITH_OPT_DEPS @@ -89,6 +89,10 @@ class PytestMessage(Message): pass +class PytestUpdater(Updater): + pass + + def make_bot(bot_info=None, **kwargs): """ Tests are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot diff --git a/tests/conftest.py b/tests/conftest.py index 213bcff4a..a9ef3e686 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ import asyncio import datetime import logging import sys +from pathlib import Path from typing import Dict, List from uuid import uuid4 @@ -291,6 +292,5 @@ def timezone(tzinfo): @pytest.fixture() -def tmp_file(tmp_path): - with tmp_path / uuid4().hex as file: - yield file +def tmp_file(tmp_path) -> Path: + return tmp_path / uuid4().hex diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index 714abf853..acfce013a 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -61,7 +61,7 @@ from tests.auxil.asyncio_helpers import call_after from tests.auxil.build_messages import make_message_update from tests.auxil.files import PROJECT_ROOT_PATH from tests.auxil.networking import send_webhook_message -from tests.auxil.pytest_classes import make_bot +from tests.auxil.pytest_classes import PytestApplication, PytestUpdater, make_bot from tests.auxil.slots import mro_slots @@ -1581,7 +1581,13 @@ class TestApplication: async def post_init(app: Application) -> None: events.append("post_init") - app = Application.builder().bot(one_time_bot).post_init(post_init).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_init(post_init) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr( @@ -1624,7 +1630,13 @@ class TestApplication: async def post_shutdown(app: Application) -> None: events.append("post_shutdown") - app = Application.builder().bot(one_time_bot).post_shutdown(post_shutdown).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_shutdown(post_shutdown) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr( @@ -1650,7 +1662,7 @@ class TestApplication: platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) - def test_run_polling_post_stop(self, bot, monkeypatch): + def test_run_polling_post_stop(self, one_time_bot, monkeypatch): events = [] async def get_updates(*args, **kwargs): @@ -1671,7 +1683,13 @@ class TestApplication: async def post_stop(app: Application) -> None: events.append("post_stop") - app = Application.builder().token(bot.token).post_stop(post_stop).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_stop(post_stop) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr(app, "stop", call_after(app.stop, lambda _: events.append("stop"))) @@ -1863,7 +1881,13 @@ class TestApplication: async def post_init(app: Application) -> None: events.append("post_init") - app = Application.builder().bot(one_time_bot).post_init(post_init).build() + app = ( + Application.builder() + .post_init(post_init) + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) @@ -1923,7 +1947,13 @@ class TestApplication: async def post_shutdown(app: Application) -> None: events.append("post_shutdown") - app = Application.builder().bot(one_time_bot).post_shutdown(post_shutdown).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_shutdown(post_shutdown) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) @@ -1960,7 +1990,7 @@ class TestApplication: platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) - def test_run_webhook_post_stop(self, bot, monkeypatch): + def test_run_webhook_post_stop(self, one_time_bot, monkeypatch): events = [] async def delete_webhook(*args, **kwargs): @@ -1987,7 +2017,13 @@ class TestApplication: async def post_stop(app: Application) -> None: events.append("post_stop") - app = Application.builder().token(bot.token).post_stop(post_stop).build() + app = ( + Application.builder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_stop(post_stop) + .build() + ) app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) @@ -2480,7 +2516,13 @@ class TestApplication: app.create_task(task(app)) - app = ApplicationBuilder().bot(one_time_bot).post_init(post_init).build() + app = ( + ApplicationBuilder() + .application_class(PytestApplication) + .updater(PytestUpdater(one_time_bot, asyncio.Queue())) + .post_init(post_init) + .build() + ) monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) diff --git a/tests/request/test_request.py b/tests/request/test_request.py index 47e7d2125..ecfb65ece 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -47,6 +47,7 @@ from telegram.request._httpxrequest import HTTPXRequest from telegram.request._requestparameter import RequestParameter from telegram.warnings import PTBDeprecationWarning from tests.auxil.envvars import TEST_WITH_OPT_DEPS +from tests.auxil.networking import NonchalantHttpxRequest from tests.auxil.slots import mro_slots # We only need mixed_rqs fixture, but it uses the others, so pytest needs us to import them as well @@ -72,7 +73,7 @@ def mocker_factory( @pytest.fixture() async def httpx_request(): - async with HTTPXRequest() as rq: + async with NonchalantHttpxRequest() as rq: yield rq @@ -137,7 +138,7 @@ class TestRequestWithoutRequest: async def shutdown(): self.test_flag.append("stop") - httpx_request = HTTPXRequest() + httpx_request = NonchalantHttpxRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx_request, "shutdown", shutdown) @@ -154,7 +155,7 @@ class TestRequestWithoutRequest: async def shutdown(): self.test_flag = "stop" - httpx_request = HTTPXRequest() + httpx_request = NonchalantHttpxRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx_request, "shutdown", shutdown) @@ -545,7 +546,7 @@ class TestHTTPXRequestWithoutRequest: async def aclose(*args): self.test_flag.append("stop") - httpx_request = HTTPXRequest() + httpx_request = NonchalantHttpxRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx.AsyncClient, "aclose", aclose) @@ -562,7 +563,7 @@ class TestHTTPXRequestWithoutRequest: async def aclose(*args): self.test_flag = "stop" - httpx_request = HTTPXRequest() + httpx_request = NonchalantHttpxRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx.AsyncClient, "aclose", aclose) diff --git a/tests/test_bot.py b/tests/test_bot.py index 8fa536281..d22ea96db 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -97,7 +97,7 @@ from tests.auxil.bot_method_checks import check_defaults_handling from tests.auxil.ci_bots import FALLBACKS from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS from tests.auxil.files import data_file -from tests.auxil.networking import expect_bad_request +from tests.auxil.networking import NonchalantHttpxRequest, expect_bad_request from tests.auxil.pytest_classes import PytestBot, PytestExtBot, make_bot from tests.auxil.slots import mro_slots @@ -253,7 +253,7 @@ class TestBotWithoutRequest: async def stop(*args, **kwargs): self.test_flag.append("stop") - temp_bot = PytestBot(token=bot.token) + temp_bot = PytestBot(token=bot.token, request=NonchalantHttpxRequest()) orig_stop = temp_bot.request.shutdown try: diff --git a/tests/test_message.py b/tests/test_message.py index c51e3a92a..075d7089d 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -2624,7 +2624,9 @@ class TestMessageWithoutRequest(TestMessageBase): async def test_default_do_quote( self, bot, message, default_quote, chat_type, expected, monkeypatch ): - message.set_bot(PytestExtBot(token=bot.token, defaults=Defaults(do_quote=default_quote))) + original_bot = message.get_bot() + temp_bot = PytestExtBot(token=bot.token, defaults=Defaults(do_quote=default_quote)) + message.set_bot(temp_bot) async def make_assertion(*_, **kwargs): reply_parameters = kwargs.get("reply_parameters") or ReplyParameters(message_id=False) @@ -2637,7 +2639,7 @@ class TestMessageWithoutRequest(TestMessageBase): message.chat.type = chat_type assert await message.reply_text("test") finally: - message.get_bot()._defaults = None + message.set_bot(original_bot) async def test_edit_forum_topic(self, monkeypatch, message): async def make_assertion(*_, **kwargs):