From 422993b8ab05d49dd49ab58c500f880918d894a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A3=D1=97=D0=B8=C3=A3=D0=BC?= <76057348+jainamoswal@users.noreply.github.com> Date: Sun, 14 Jul 2024 00:56:44 +0530 Subject: [PATCH 01/26] Add Parameter `game_pattern` to `CallbackQueryHandler` (#4353) --- AUTHORS.rst | 1 + .../ext/_handlers/callbackqueryhandler.py | 72 ++++++++++++++----- tests/ext/test_callbackqueryhandler.py | 44 ++++++++++++ 3 files changed, 99 insertions(+), 18 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index fe69dba625d..403362aaab0 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -60,6 +60,7 @@ The following wonderful people contributed directly or indirectly to this projec - `Hugo Damer `_ - `ihoru `_ - `Iulian Onofrei `_ +- `Jainam Oswal `_ - `Jasmin Bom `_ - `JASON0916 `_ - `jeffffc `_ diff --git a/telegram/ext/_handlers/callbackqueryhandler.py b/telegram/ext/_handlers/callbackqueryhandler.py index afd64887964..c8fb0e7b3f0 100644 --- a/telegram/ext/_handlers/callbackqueryhandler.py +++ b/telegram/ext/_handlers/callbackqueryhandler.py @@ -51,6 +51,15 @@ class CallbackQueryHandler(BaseHandler[Update, CCT]): .. versionadded:: 13.6 + * If neither :paramref:`pattern` nor :paramref:`game_pattern` is set, `any` + ``CallbackQuery`` will be handled. If only :paramref:`pattern` is set, queries with + :attr:`~telegram.CallbackQuery.game_short_name` will `not` be considered and vice versa. + If both patterns are set, queries with either :attr: + `~telegram.CallbackQuery.game_short_name` or :attr:`~telegram.CallbackQuery.data` + matching the defined pattern will be handled + + .. versionadded:: NEXT.VERSION + Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. @@ -85,6 +94,13 @@ async def callback(update: Update, context: CallbackContext) .. versionchanged:: 13.6 Added support for arbitrary callback data. + game_pattern (:obj:`str` | :func:`re.Pattern ` | optional) + Pattern to test :attr:`telegram.CallbackQuery.game_short_name` against. If a string or + a regex pattern is passed, :func:`re.match` is used on + :attr:`telegram.CallbackQuery.game_short_name` to determine if an update should be + handled by this handler. + + .. versionadded:: NEXT.VERSION block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. @@ -98,13 +114,15 @@ async def callback(update: Update, context: CallbackContext) .. versionchanged:: 13.6 Added support for arbitrary callback data. + game_pattern (:func:`re.Pattern `): Optional. + Regex pattern to test :attr:`telegram.CallbackQuery.game_short_name` block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ - __slots__ = ("pattern",) + __slots__ = ("game_pattern", "pattern") def __init__( self, @@ -112,6 +130,7 @@ def __init__( pattern: Optional[ Union[str, Pattern[str], type, Callable[[object], Optional[bool]]] ] = None, + game_pattern: Optional[Union[str, Pattern[str]]] = None, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) @@ -120,13 +139,15 @@ def __init__( raise TypeError( "The `pattern` must not be a coroutine function! Use an ordinary function instead." ) - if isinstance(pattern, str): pattern = re.compile(pattern) + if isinstance(game_pattern, str): + game_pattern = re.compile(game_pattern) self.pattern: Optional[ Union[str, Pattern[str], type, Callable[[object], Optional[bool]]] ] = pattern + self.game_pattern: Optional[Union[str, Pattern[str]]] = game_pattern def check_update(self, update: object) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handler's :attr:`callback`. @@ -139,22 +160,37 @@ def check_update(self, update: object) -> Optional[Union[bool, object]]: """ # pylint: disable=too-many-return-statements - if isinstance(update, Update) and update.callback_query: - callback_data = update.callback_query.data - if self.pattern: - if callback_data is None: - return False - if isinstance(self.pattern, type): - return isinstance(callback_data, self.pattern) - if callable(self.pattern): - return self.pattern(callback_data) - if not isinstance(callback_data, str): - return False - if match := re.match(self.pattern, callback_data): - return match - else: - return True - return None + if not (isinstance(update, Update) and update.callback_query): + return None + + callback_data = update.callback_query.data + game_short_name = update.callback_query.game_short_name + + if not any([self.pattern, self.game_pattern]): + return True + + # we check for .data or .game_short_name from update to filter based on whats coming + # this gives xor-like behavior + if callback_data: + if not self.pattern: + return False + if isinstance(self.pattern, type): + return isinstance(callback_data, self.pattern) + if callable(self.pattern): + return self.pattern(callback_data) + if not isinstance(callback_data, str): + return False + if match := re.match(self.pattern, callback_data): + return match + + elif game_short_name: + if not self.game_pattern: + return False + if match := re.match(self.game_pattern, game_short_name): + return match + else: + return True + return False def collect_additional_context( self, diff --git a/tests/ext/test_callbackqueryhandler.py b/tests/ext/test_callbackqueryhandler.py index 65b5589c041..f96c074d2d1 100644 --- a/tests/ext/test_callbackqueryhandler.py +++ b/tests/ext/test_callbackqueryhandler.py @@ -228,3 +228,47 @@ async def pattern(): with pytest.raises(TypeError, match="must not be a coroutine function"): CallbackQueryHandler(self.callback, pattern=pattern) + + def test_game_pattern(self, callback_query): + callback_query.callback_query.data = None + + callback_query.callback_query.game_short_name = "test data" + handler = CallbackQueryHandler(self.callback_basic, game_pattern=".*est.*") + assert handler.check_update(callback_query) + + callback_query.callback_query.game_short_name = "nothing here" + assert not handler.check_update(callback_query) + + callback_query.callback_query.game_short_name = "this is a short game name" + assert not handler.check_update(callback_query) + + callback_query.callback_query.data = "something" + handler = CallbackQueryHandler(self.callback_basic, game_pattern="") + assert not handler.check_update(callback_query) + + @pytest.mark.parametrize( + ("data", "pattern", "game_short_name", "game_pattern", "expected_result"), + [ + (None, None, None, None, True), + (None, ".*data", None, None, True), + (None, None, None, ".*game", True), + (None, ".*data", None, ".*game", True), + ("some_data", None, None, None, True), + ("some_data", ".*data", None, None, True), + ("some_data", None, None, ".*game", False), + ("some_data", ".*data", None, ".*game", True), + (None, None, "some_game", None, True), + (None, ".*data", "some_game", None, False), + (None, None, "some_game", ".*game", True), + (None, ".*data", "some_game", ".*game", True), + ], + ) + def test_pattern_and_game_pattern_interaction( + self, callback_query, data, pattern, game_short_name, game_pattern, expected_result + ): + callback_query.callback_query.data = data + callback_query.callback_query.game_short_name = game_short_name + handler = CallbackQueryHandler( + callback=self.callback, pattern=pattern, game_pattern=game_pattern + ) + assert bool(handler.check_update(callback_query)) == expected_result From 006a290b7bff0fe185c0e6c0d205db6d49d323d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Jul 2024 21:05:01 +0200 Subject: [PATCH 02/26] Bump `furo` from 2024.5.6 to 2024.7.18 (#4392) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 08fba15d32f..f876229003f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ sphinx==7.3.7 -furo==2024.5.6 +furo==2024.7.18 furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1 sphinx-paramlinks==0.6.0 sphinxcontrib-mermaid==0.9.2 From c3f17bb18e5f13dad4466227a8bb0dc55a7c7e73 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 21 Jul 2024 21:12:30 +0200 Subject: [PATCH 03/26] Start Adapting to RTD Addons (#4386) --- docs/source/conf.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5858a79e2da..fdc3b27e857 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,3 +1,4 @@ +import os import re import sys from pathlib import Path @@ -251,7 +252,14 @@ # The base URL which points to the root of the HTML documentation. It is used to indicate the # location of document using The Canonical Link Relation. Default: ''. -html_baseurl = "https://docs.python-telegram-bot.org" +# Set canonical URL from the Read the Docs Domain +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") + +# Tell Jinja2 templates the build is running on Read the Docs +html_context = {} +if os.environ.get("READTHEDOCS", "") == "True": + html_context["READTHEDOCS"] = True + # -- Options for LaTeX output --------------------------------------------- From 0913b859d7956e1201d947e2d4ea4d131ba9c7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= <58857054+elpekenin@users.noreply.github.com> Date: Sun, 21 Jul 2024 21:13:30 +0200 Subject: [PATCH 04/26] Add Internal Constants for Encodings (#4378) --- AUTHORS.rst | 1 + telegram/_files/inputfile.py | 3 ++- telegram/_games/game.py | 5 +++-- telegram/_message.py | 19 +++++++++++-------- telegram/_messageentity.py | 3 ++- telegram/_passport/credentials.py | 3 ++- telegram/_utils/entities.py | 5 +++-- telegram/_utils/strings.py | 17 +++++++++++++++++ telegram/request/_baserequest.py | 3 ++- telegram/request/_requestdata.py | 3 ++- tests/_files/test_inputfile.py | 7 ++++--- tests/auxil/ci_bots.py | 8 ++++++-- tests/auxil/networking.py | 3 ++- tests/request/test_request.py | 3 ++- tests/test_enum_types.py | 4 +++- 15 files changed, 62 insertions(+), 25 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 403362aaab0..6dfff7c6733 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -97,6 +97,7 @@ The following wonderful people contributed directly or indirectly to this projec - `Oleg Sushchenko `_ - `Or Bin `_ - `overquota `_ +- `Pablo Martinez `_ - `Paradox `_ - `Patrick Hofmann `_ - `Paul Larsen `_ diff --git a/telegram/_files/inputfile.py b/telegram/_files/inputfile.py index 994135bb5dd..9a07f6d65fa 100644 --- a/telegram/_files/inputfile.py +++ b/telegram/_files/inputfile.py @@ -23,6 +23,7 @@ from uuid import uuid4 from telegram._utils.files import load_file +from telegram._utils.strings import TextEncoding from telegram._utils.types import FieldTuple _DEFAULT_MIME_TYPE = "application/octet-stream" @@ -74,7 +75,7 @@ def __init__( if isinstance(obj, bytes): self.input_file_content: bytes = obj elif isinstance(obj, str): - self.input_file_content = obj.encode("utf-8") + self.input_file_content = obj.encode(TextEncoding.UTF_8) else: reported_filename, self.input_file_content = load_file(obj) filename = filename or reported_filename diff --git a/telegram/_games/game.py b/telegram/_games/game.py index 93b3f0161cc..1a25d1ad538 100644 --- a/telegram/_games/game.py +++ b/telegram/_games/game.py @@ -24,6 +24,7 @@ from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -157,10 +158,10 @@ def parse_text_entity(self, entity: MessageEntity) -> str: if not self.text: raise RuntimeError("This Game has no 'text'.") - entity_text = self.text.encode("utf-16-le") + entity_text = self.text.encode(TextEncoding.UTF_16_LE) entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] - return entity_text.decode("utf-16-le") + return entity_text.decode(TextEncoding.UTF_16_LE) def parse_text_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]: """ diff --git a/telegram/_message.py b/telegram/_message.py index fceb8cb8768..a705dc65934 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -68,6 +68,7 @@ from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.entities import parse_message_entities, parse_message_entity +from telegram._utils.strings import TextEncoding from telegram._utils.types import ( CorrectOptionID, FileInput, @@ -1516,8 +1517,8 @@ def compute_quote_position_and_entities( raise RuntimeError("This message has neither text nor caption.") # Telegram wants the position in UTF-16 code units, so we have to calculate in that space - utf16_text = text.encode("utf-16-le") - utf16_quote = quote.encode("utf-16-le") + utf16_text = text.encode(TextEncoding.UTF_16_LE) + utf16_quote = quote.encode(TextEncoding.UTF_16_LE) effective_index = index or 0 matches = list(re.finditer(re.escape(utf16_quote), utf16_text)) @@ -4479,7 +4480,7 @@ def _parse_html( if message_text is None: return None - utf_16_text = message_text.encode("utf-16-le") + utf_16_text = message_text.encode(TextEncoding.UTF_16_LE) html_text = "" last_offset = 0 @@ -4543,7 +4544,9 @@ def _parse_html( # text is part of the parent entity html_text += ( escape( - utf_16_text[last_offset * 2 : (entity.offset - offset) * 2].decode("utf-16-le") + utf_16_text[last_offset * 2 : (entity.offset - offset) * 2].decode( + TextEncoding.UTF_16_LE + ) ) + insert ) @@ -4551,7 +4554,7 @@ def _parse_html( last_offset = entity.offset - offset + entity.length # see comment above - html_text += escape(utf_16_text[last_offset * 2 :].decode("utf-16-le")) + html_text += escape(utf_16_text[last_offset * 2 :].decode(TextEncoding.UTF_16_LE)) return html_text @@ -4680,7 +4683,7 @@ def _parse_markdown( if message_text is None: return None - utf_16_text = message_text.encode("utf-16-le") + utf_16_text = message_text.encode(TextEncoding.UTF_16_LE) markdown_text = "" last_offset = 0 @@ -4773,7 +4776,7 @@ def _parse_markdown( markdown_text += ( escape_markdown( utf_16_text[last_offset * 2 : (entity.offset - offset) * 2].decode( - "utf-16-le" + TextEncoding.UTF_16_LE ), version=version, ) @@ -4784,7 +4787,7 @@ def _parse_markdown( # see comment above markdown_text += escape_markdown( - utf_16_text[last_offset * 2 :].decode("utf-16-le"), + utf_16_text[last_offset * 2 :].decode(TextEncoding.UTF_16_LE), version=version, ) diff --git a/telegram/_messageentity.py b/telegram/_messageentity.py index 302f3a1c080..6e219537fae 100644 --- a/telegram/_messageentity.py +++ b/telegram/_messageentity.py @@ -26,6 +26,7 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum +from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -203,7 +204,7 @@ def adjust_message_entities_to_utf_16( for i, position in enumerate(positions): last_position = positions[i - 1] if i > 0 else 0 text_slice = text[last_position:position] - accumulated_length += len(text_slice.encode("utf-16-le")) // 2 + accumulated_length += len(text_slice.encode(TextEncoding.UTF_16_LE)) // 2 position_translation[position] = accumulated_length # get the final output entites out = [] diff --git a/telegram/_passport/credentials.py b/telegram/_passport/credentials.py index 514f7fffb6c..fab0b6eb2c8 100644 --- a/telegram/_passport/credentials.py +++ b/telegram/_passport/credentials.py @@ -39,6 +39,7 @@ from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict from telegram.error import PassportDecryptionError @@ -98,7 +99,7 @@ def decrypt(secret, hash, data): @no_type_check def decrypt_json(secret, hash, data): """Decrypts data using secret and hash and then decodes utf-8 string and loads json""" - return json.loads(decrypt(secret, hash, data).decode("utf-8")) + return json.loads(decrypt(secret, hash, data).decode(TextEncoding.UTF_8)) class EncryptedCredentials(TelegramObject): diff --git a/telegram/_utils/entities.py b/telegram/_utils/entities.py index a3994cd0426..34901c3d6f7 100644 --- a/telegram/_utils/entities.py +++ b/telegram/_utils/entities.py @@ -26,6 +26,7 @@ from typing import Dict, Optional, Sequence from telegram._messageentity import MessageEntity +from telegram._utils.strings import TextEncoding def parse_message_entity(text: str, entity: MessageEntity) -> str: @@ -38,10 +39,10 @@ def parse_message_entity(text: str, entity: MessageEntity) -> str: Returns: :obj:`str`: The text of the given entity. """ - entity_text = text.encode("utf-16-le") + entity_text = text.encode(TextEncoding.UTF_16_LE) entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] - return entity_text.decode("utf-16-le") + return entity_text.decode(TextEncoding.UTF_16_LE) def parse_message_entities( diff --git a/telegram/_utils/strings.py b/telegram/_utils/strings.py index dc044e86420..c57e4e48b32 100644 --- a/telegram/_utils/strings.py +++ b/telegram/_utils/strings.py @@ -24,6 +24,23 @@ the changelog. """ +from telegram._utils.enum import StringEnum + +# TODO: Remove this when https://github.com/PyCQA/pylint/issues/6887 is resolved. +# pylint: disable=invalid-enum-extension,invalid-slots + + +class TextEncoding(StringEnum): + """This enum contains encoding schemes for text. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + UTF_8 = "utf-8" + UTF_16_LE = "utf-16-le" + def to_camel_case(snake_str: str) -> str: """Converts a snake_case string to camelCase. diff --git a/telegram/request/_baserequest.py b/telegram/request/_baserequest.py index 93024d6c4d0..d323dfb0620 100644 --- a/telegram/request/_baserequest.py +++ b/telegram/request/_baserequest.py @@ -26,6 +26,7 @@ from telegram._utils.defaultvalue import DEFAULT_NONE as _DEFAULT_NONE from telegram._utils.defaultvalue import DefaultValue from telegram._utils.logging import get_logger +from telegram._utils.strings import TextEncoding from telegram._utils.types import JSONDict, ODVInput from telegram._utils.warnings import warn from telegram._version import __version__ as ptb_ver @@ -403,7 +404,7 @@ def parse_json_payload(payload: bytes) -> JSONDict: Raises: TelegramError: If loading the JSON data failed """ - decoded_s = payload.decode("utf-8", "replace") + decoded_s = payload.decode(TextEncoding.UTF_8, "replace") try: return json.loads(decoded_s) except ValueError as exc: diff --git a/telegram/request/_requestdata.py b/telegram/request/_requestdata.py index 658a445649d..1b5b5446d5e 100644 --- a/telegram/request/_requestdata.py +++ b/telegram/request/_requestdata.py @@ -21,6 +21,7 @@ from typing import Any, Dict, List, Optional, Union, final from urllib.parse import urlencode +from telegram._utils.strings import TextEncoding from telegram._utils.types import UploadFileDict from telegram.request._requestparameter import RequestParameter @@ -109,7 +110,7 @@ def json_payload(self) -> bytes: To use a custom library for JSON encoding, you can directly encode the keys of :attr:`parameters` - note that string valued keys should not be JSON encoded. """ - return json.dumps(self.json_parameters).encode("utf-8") + return json.dumps(self.json_parameters).encode(TextEncoding.UTF_8) @property def multipart_data(self) -> UploadFileDict: diff --git a/tests/_files/test_inputfile.py b/tests/_files/test_inputfile.py index 2a2a3b60734..1f70cb5ccda 100644 --- a/tests/_files/test_inputfile.py +++ b/tests/_files/test_inputfile.py @@ -24,6 +24,7 @@ import pytest from telegram import InputFile +from telegram._utils.strings import TextEncoding from tests.auxil.files import data_file from tests.auxil.slots import mro_slots @@ -150,17 +151,17 @@ async def test_send_bytes(self, bot, chat_id): await (await message.document.get_file()).download_to_memory(out=out) out.seek(0) - assert out.read().decode("utf-8") == "PTB Rocks! ⅞" + assert out.read().decode(TextEncoding.UTF_8) == "PTB Rocks! ⅞" async def test_send_string(self, bot, chat_id): # We test this here and not at the respective test modules because it's not worth # duplicating the test for the different methods message = await bot.send_document( - chat_id, InputFile(data_file("text_file.txt").read_text(encoding="utf-8")) + chat_id, InputFile(data_file("text_file.txt").read_text(encoding=TextEncoding.UTF_8)) ) out = BytesIO() await (await message.document.get_file()).download_to_memory(out=out) out.seek(0) - assert out.read().decode("utf-8") == "PTB Rocks! ⅞" + assert out.read().decode(TextEncoding.UTF_8) == "PTB Rocks! ⅞" diff --git a/tests/auxil/ci_bots.py b/tests/auxil/ci_bots.py index bfad962b811..bdb25a2f0ee 100644 --- a/tests/auxil/ci_bots.py +++ b/tests/auxil/ci_bots.py @@ -22,6 +22,8 @@ import os import random +from telegram._utils.strings import TextEncoding + # Provide some public fallbacks so it's easy for contributors to run tests on their local machine # These bots are only able to talk in our test chats, so they are quite useless for other # purposes than testing. @@ -42,10 +44,12 @@ BOTS = os.getenv("BOTS", None) JOB_INDEX = os.getenv("JOB_INDEX", None) if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None: - BOTS = json.loads(base64.b64decode(BOTS).decode("utf-8")) + BOTS = json.loads(base64.b64decode(BOTS).decode(TextEncoding.UTF_8)) JOB_INDEX = int(JOB_INDEX) -FALLBACKS = json.loads(base64.b64decode(FALLBACKS).decode("utf-8")) # type: list[dict[str, str]] +FALLBACKS = json.loads( + base64.b64decode(FALLBACKS).decode(TextEncoding.UTF_8) +) # type: list[dict[str, str]] class BotInfoProvider: diff --git a/tests/auxil/networking.py b/tests/auxil/networking.py index 2284f31fc50..7c20da7ac94 100644 --- a/tests/auxil/networking.py +++ b/tests/auxil/networking.py @@ -23,6 +23,7 @@ from httpx import AsyncClient, AsyncHTTPTransport, Response from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.strings import TextEncoding from telegram._utils.types import ODVInput from telegram.error import BadRequest, RetryAfter, TimedOut from telegram.request import HTTPXRequest, RequestData @@ -103,7 +104,7 @@ async def send_webhook_message( content_len = None payload = None else: - payload = bytes(payload_str, encoding="utf-8") + payload = bytes(payload_str, encoding=TextEncoding.UTF_8) if content_len == -1: content_len = len(payload) diff --git a/tests/request/test_request.py b/tests/request/test_request.py index 0f664cbdbcf..55100940b18 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -31,6 +31,7 @@ from httpx import AsyncHTTPTransport from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.strings import TextEncoding from telegram.error import ( BadRequest, ChatMigrated, @@ -247,7 +248,7 @@ async def test_error_description(self, monkeypatch, httpx_request: HTTPXRequest, else: match = "Unknown HTTPError" - server_response = json.dumps(response_data).encode("utf-8") + server_response = json.dumps(response_data).encode(TextEncoding.UTF_8) monkeypatch.setattr( httpx_request, diff --git a/tests/test_enum_types.py b/tests/test_enum_types.py index b16002c6642..947d5fd0655 100644 --- a/tests/test_enum_types.py +++ b/tests/test_enum_types.py @@ -19,6 +19,8 @@ import re from pathlib import Path +from telegram._utils.strings import TextEncoding + telegram_root = Path(__file__).parent.parent / "telegram" telegram_ext_root = telegram_root / "ext" exclude_dirs = { @@ -46,7 +48,7 @@ def test_types_are_converted_to_enum(): # We don't check tg.ext. continue - text = path.read_text(encoding="utf-8") + text = path.read_text(encoding=TextEncoding.UTF_8) for match in re.finditer(pattern, text): if any(exclude_pattern.match(match.group(0)) for exclude_pattern in exclude_patterns): continue From 6d70c561593c6df1d3118cc6ea733c152e3287a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Jul 2024 21:23:21 +0200 Subject: [PATCH 05/26] Bump `sphinx` from 7.3.7 to 7.4.7 (#4395) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index f876229003f..87f2c1911f6 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ -sphinx==7.3.7 +sphinx==7.4.7 furo==2024.7.18 furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1 sphinx-paramlinks==0.6.0 From 8a205b10c081496ee6633f62f2a80a0c9e9140d6 Mon Sep 17 00:00:00 2001 From: MOHD YUSUF <145057367+mohdyusuf2312@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:32:53 +0530 Subject: [PATCH 06/26] Add Introductory Paragraphs to Telegram Types Subsections (#4389) Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- AUTHORS.rst | 1 + docs/source/telegram.games-tree.rst | 15 +++++++++++++++ docs/source/telegram.inline-tree.rst | 8 ++++++++ docs/source/telegram.passport-tree.rst | 3 +++ docs/source/telegram.payments-tree.rst | 5 +++++ docs/source/telegram.stickers-tree.rst | 2 ++ telegram/_message.py | 12 ++++++------ telegram/_reply.py | 10 ++++------ 8 files changed, 44 insertions(+), 12 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 6dfff7c6733..e95a2b7a3f9 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -87,6 +87,7 @@ The following wonderful people contributed directly or indirectly to this projec - `Miguel C. R. `_ - `miles `_ - `Mischa Krüger `_ +- `Mohd Yusuf `_ - `naveenvhegde `_ - `neurrone `_ - `NikitaPirate `_ diff --git a/docs/source/telegram.games-tree.rst b/docs/source/telegram.games-tree.rst index 64f399d86a9..010c3c6d1fe 100644 --- a/docs/source/telegram.games-tree.rst +++ b/docs/source/telegram.games-tree.rst @@ -1,6 +1,21 @@ +.. _games-tree: + Games ----- +Your bot can offer users **HTML5 games** to play solo or to compete against each other in groups and one-on-one chats. Create games via `@BotFather `_ using the ``/newgame`` command. Please note that this kind of power requires responsibility: you will need to accept the terms for each game that your bots will be offering. + +* Games are a new type of content on Telegram, represented by the :class:`telegram.Game` and :class:`telegram.InlineQueryResultGame` objects. +* Once you've created a game via `BotFather `_, you can send games to chats as regular messages using the :meth:`~telegram.Bot.sendGame` method, or use :ref:`inline mode ` with :class:`telegram.InlineQueryResultGame`. +* If you send the game message without any buttons, it will automatically have a 'Play ``GameName``' button. When this button is pressed, your bot gets a :class:`telegram.CallbackQuery` with the ``game_short_name`` of the requested game. You provide the correct URL for this particular user and the app opens the game in the in-app browser. +* You can manually add multiple buttons to your game message. Please note that the first button in the first row **must always** launch the game, using the field ``callback_game`` in :class:`telegram.InlineKeyboardButton`. You can add extra buttons according to taste: e.g., for a description of the rules, or to open the game's official community. +* To make your game more attractive, you can upload a GIF animation that demonstrates the game to the users via `BotFather `_ (see `Lumberjack `_ for example). +* A game message will also display high scores for the current chat. Use :meth:`~telegram.Bot.setGameScore` to post high scores to the chat with the game, optionally add the :paramref:`~telegram.Bot.set_game_score.disable_edit_message` parameter if you don't want to automatically update the message with the current scoreboard. +* Use :meth:`~telegram.Bot.getGameHighScores` to get data for in-game high score tables. +* You can also add an extra sharing button for users to share their best score to different chats. +* For examples of what can be done using this new stuff, check the `@gamebot `_ and `@gamee `_ bots. + + .. toctree:: :titlesonly: diff --git a/docs/source/telegram.inline-tree.rst b/docs/source/telegram.inline-tree.rst index 7fa52a94b58..c187219e0f6 100644 --- a/docs/source/telegram.inline-tree.rst +++ b/docs/source/telegram.inline-tree.rst @@ -1,6 +1,14 @@ +.. _inline-tree: + Inline Mode ----------- +The following methods and objects allow your bot to work in `inline mode `_. +Please see Telegrams `Introduction to Inline bots `_ for more details. + +To enable this option, send the ``/setinline`` command to `@BotFather `_ and provide the placeholder text that the user will see in the input field after typing your bot's name. + + .. toctree:: :titlesonly: diff --git a/docs/source/telegram.passport-tree.rst b/docs/source/telegram.passport-tree.rst index fb4e3b4ffde..079ce948924 100644 --- a/docs/source/telegram.passport-tree.rst +++ b/docs/source/telegram.passport-tree.rst @@ -1,6 +1,9 @@ Passport -------- +Passport is a unified authorization method for services that require personal identification. Users can upload their documents once, then instantly share their data with services that require real-world ID (finance, ICOs, etc.). Please see the `manual `_ for details. + + .. toctree:: :titlesonly: diff --git a/docs/source/telegram.payments-tree.rst b/docs/source/telegram.payments-tree.rst index 0db0ba21959..ba4c838cae7 100644 --- a/docs/source/telegram.payments-tree.rst +++ b/docs/source/telegram.payments-tree.rst @@ -1,6 +1,11 @@ +.. _payments-tree: + Payments -------- +Your bot can accept payments from Telegram users. Please see the `introduction to payments `_ for more details on the process and how to set up payments for your bot. + + .. toctree:: :titlesonly: diff --git a/docs/source/telegram.stickers-tree.rst b/docs/source/telegram.stickers-tree.rst index 783b90ec0c7..2ea687183c0 100644 --- a/docs/source/telegram.stickers-tree.rst +++ b/docs/source/telegram.stickers-tree.rst @@ -1,6 +1,8 @@ Stickers -------- +The following methods and objects allow your bot to handle stickers and sticker sets. + .. toctree:: :titlesonly: diff --git a/telegram/_message.py b/telegram/_message.py index a705dc65934..d362e42f59f 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -359,7 +359,7 @@ class Message(MaybeInaccessibleMessage): about the animation. For backward compatibility, when this field is set, the document field will also be set. game (:class:`telegram.Game`, optional): Message is a game, information about the game. - `More about games >> `_. + :ref:`More about games >> `. photo (Sequence[:class:`telegram.PhotoSize`], optional): Message is a photo, available sizes of the photo. This list is empty if the message does not contain a photo. @@ -432,10 +432,10 @@ class Message(MaybeInaccessibleMessage): :class:`telegram.InaccessibleMessage`. invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, information about the invoice. - `More about payments >> `_. + :ref:`More about payments >> `. successful_payment (:class:`telegram.SuccessfulPayment`, optional): Message is a service message about a successful payment, information about the payment. - `More about payments >> `_. + :ref:`More about payments >> `. connected_website (:obj:`str`, optional): The domain name of the website on which the user has logged in. `More about Telegram Login >> `_. @@ -676,7 +676,7 @@ class Message(MaybeInaccessibleMessage): .. seealso:: :wiki:`Working with Files and Media ` game (:class:`telegram.Game`): Optional. Message is a game, information about the game. - `More about games >> `_. + :ref:`More about games >> `. photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Message is a photo, available sizes of the photo. This list is empty if the message does not contain a photo. @@ -758,10 +758,10 @@ class Message(MaybeInaccessibleMessage): :class:`telegram.InaccessibleMessage`. invoice (:class:`telegram.Invoice`): Optional. Message is an invoice for a payment, information about the invoice. - `More about payments >> `_. + :ref:`More about payments >> `. successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Message is a service message about a successful payment, information about the payment. - `More about payments >> `_. + :ref:`More about payments >> `. connected_website (:obj:`str`): Optional. The domain name of the website on which the user has logged in. `More about Telegram Login >> `_. diff --git a/telegram/_reply.py b/telegram/_reply.py index 0c15844c8d5..222e522a6a4 100644 --- a/telegram/_reply.py +++ b/telegram/_reply.py @@ -92,14 +92,13 @@ class ExternalReplyInfo(TelegramObject): about the contact. dice (:class:`telegram.Dice`, optional): Message is a dice with random value. game (:Class:`telegram.Game`. optional): Message is a game, information about the game. - `More about games >> `_. + :ref:`More about games >> `. giveaway (:class:`telegram.Giveaway`, optional): Message is a scheduled giveaway, information about the giveaway. giveaway_winners (:class:`telegram.GiveawayWinners`, optional): A giveaway with public winners was completed. invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, - information about the invoice. `More about payments >> - `_. + information about the invoice. :ref:`More about payments >> `. location (:class:`telegram.Location`, optional): Message is a shared location, information about the location. poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the @@ -142,14 +141,13 @@ class ExternalReplyInfo(TelegramObject): about the contact. dice (:class:`telegram.Dice`): Optional. Message is a dice with random value. game (:Class:`telegram.Game`): Optional. Message is a game, information about the game. - `More about games >> `_. + :ref:`More about games >> `. giveaway (:class:`telegram.Giveaway`): Optional. Message is a scheduled giveaway, information about the giveaway. giveaway_winners (:class:`telegram.GiveawayWinners`): Optional. A giveaway with public winners was completed. invoice (:class:`telegram.Invoice`): Optional. Message is an invoice for a payment, - information about the invoice. `More about payments >> - `_. + information about the invoice. :ref:`More about payments >> `. location (:class:`telegram.Location`): Optional. Message is a shared location, information about the location. poll (:class:`telegram.Poll`): Optional. Message is a native poll, information about the From af76a8485f8e01035755816982c65a6ed4fdb2a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:03:54 +0200 Subject: [PATCH 07/26] Update `cachetools` requirement from ~=5.3.3 to >=5.3.3,<5.5.0 (#4390) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .pre-commit-config.yaml | 8 ++++---- README.rst | 4 ++-- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0d933ea11c..0aa5f1a15bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - httpx~=0.27 - tornado~=6.4 - APScheduler~=3.10.4 - - cachetools~=5.3.3 + - cachetools>=5.3.3,<5.5.0 - aiolimiter~=1.1.0 - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.4.2 @@ -37,7 +37,7 @@ repos: - httpx~=0.27 - tornado~=6.4 - APScheduler~=3.10.4 - - cachetools~=5.3.3 + - cachetools>=5.3.3,<5.5.0 - aiolimiter~=1.1.0 - . # this basically does `pip install -e .` - repo: https://github.com/pre-commit/mirrors-mypy @@ -53,7 +53,7 @@ repos: - httpx~=0.27 - tornado~=6.4 - APScheduler~=3.10.4 - - cachetools~=5.3.3 + - cachetools>=5.3.3,<5.5.0 - aiolimiter~=1.1.0 - . # this basically does `pip install -e .` - id: mypy @@ -65,7 +65,7 @@ repos: additional_dependencies: - tornado~=6.4 - APScheduler~=3.10.4 - - cachetools~=5.3.3 + - cachetools>=5.3.3,<5.5.0 - . # this basically does `pip install -e .` - repo: https://github.com/asottile/pyupgrade rev: v3.16.0 diff --git a/README.rst b/README.rst index 09b01d923a8..7bf24728b3d 100644 --- a/README.rst +++ b/README.rst @@ -157,7 +157,7 @@ PTB can be installed with optional dependencies: * ``pip install "python-telegram-bot[http2]"`` installs `httpx[http2] `_. Use this, if you want to use HTTP/2. * ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1.0 `_. Use this, if you want to use ``telegram.ext.AIORateLimiter``. * ``pip install "python-telegram-bot[webhooks]"`` installs the `tornado~=6.4 `_ library. Use this, if you want to use ``telegram.ext.Updater.start_webhook``/``telegram.ext.Application.run_webhook``. -* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools~=5.3.3 `_ library. Use this, if you want to use `arbitrary callback_data `_. +* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools>=5.3.3,<5.5.0 `_ library. Use this, if you want to use `arbitrary callback_data `_. * ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 `_ library and enforces `pytz>=2018.6 `_, where ``pytz`` is a dependency of ``APScheduler``. Use this, if you want to use the ``telegram.ext.JobQueue``. To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot[socks,webhooks]"``. @@ -232,4 +232,4 @@ License You may copy, distribute and modify the software provided that modifications are described and licensed for free under `LGPL-3 `_. Derivatives works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be. -.. _`GitHub releases page`: https://github.com/python-telegram-bot/python-telegram-bot/releases> \ No newline at end of file +.. _`GitHub releases page`: https://github.com/python-telegram-bot/python-telegram-bot/releases> diff --git a/pyproject.toml b/pyproject.toml index 2484250c3d5..551c10b1dcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ all = [ ] callback-data = [ # Cachetools doesn't have a strict stability policy. Let's be cautious for now. - "cachetools~=5.3.3", + "cachetools>=5.3.3,<5.5.0", ] ext = [ "python-telegram-bot[callback-data,job-queue,rate-limiter,webhooks]", From a967dbe37a44925fedd56202bdc0e8e62b5e14a8 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:41:39 +0200 Subject: [PATCH 08/26] Document Return Types of `RequestData` Members (#4396) --- telegram/request/_requestdata.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/telegram/request/_requestdata.py b/telegram/request/_requestdata.py index 1b5b5446d5e..82d1e3d2cdc 100644 --- a/telegram/request/_requestdata.py +++ b/telegram/request/_requestdata.py @@ -55,6 +55,9 @@ def parameters(self) -> Dict[str, Union[str, int, List[Any], Dict[Any, Any]]]: a single object of type :obj:`int`, :obj:`float`, :obj:`str` or :obj:`bool` or any (possibly nested) composition of lists, tuples and dictionaries, where each entry, key and value is of one of the mentioned types. + + Returns: + Dict[:obj:`str`, Union[:obj:`str`, :obj:`int`, List[any], Dict[any, any]]] """ return { param.name: param.value # type: ignore[misc] @@ -71,6 +74,9 @@ def json_parameters(self) -> Dict[str, str]: By default, this property uses the standard library's :func:`json.dumps`. To use a custom library for JSON encoding, you can directly encode the keys of :attr:`parameters` - note that string valued keys should not be JSON encoded. + + Returns: + Dict[:obj:`str`, :obj:`str`] """ return { param.name: param.json_value @@ -84,6 +90,9 @@ def url_encoded_parameters(self, encode_kwargs: Optional[Dict[str, Any]] = None) Args: encode_kwargs (Dict[:obj:`str`, any], optional): Additional keyword arguments to pass along to :func:`urllib.parse.urlencode`. + + Returns: + :obj:`str` """ if encode_kwargs: return urlencode(self.json_parameters, **encode_kwargs) @@ -97,6 +106,9 @@ def parametrized_url(self, url: str, encode_kwargs: Optional[Dict[str, Any]] = N url (:obj:`str`): The URL the parameters will be attached to. encode_kwargs (Dict[:obj:`str`, any], optional): Additional keyword arguments to pass along to :func:`urllib.parse.urlencode`. + + Returns: + :obj:`str` """ url_parameters = self.url_encoded_parameters(encode_kwargs=encode_kwargs) return f"{url}?{url_parameters}" @@ -109,6 +121,9 @@ def json_payload(self) -> bytes: By default, this property uses the standard library's :func:`json.dumps`. To use a custom library for JSON encoding, you can directly encode the keys of :attr:`parameters` - note that string valued keys should not be JSON encoded. + + Returns: + :obj:`bytes` """ return json.dumps(self.json_parameters).encode(TextEncoding.UTF_8) From 6578c76068e1b48bf63216fddb8fe88d28499daf Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:43:27 +0200 Subject: [PATCH 09/26] API 7.8 (#4408) --- README.rst | 4 ++-- telegram/_bot.py | 20 ++++++++++++++++++-- telegram/_chat.py | 4 ++++ telegram/_message.py | 20 ++++++++++++++++++-- telegram/_user.py | 16 ++++++++++++++++ telegram/constants.py | 2 +- telegram/ext/_extbot.py | 4 ++++ tests/test_bot.py | 2 ++ tests/test_callbackquery.py | 12 ++++++++---- tests/test_message.py | 19 +++++++++++++++---- tests/test_user.py | 5 +++++ 11 files changed, 93 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 7bf24728b3d..b2de996d2ed 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.7-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.8-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with Telegram API support ~~~~~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **7.7** are natively supported by this library. +All types and methods of the Telegram Bot API **7.8** are natively supported by this library. In addition, Bot API functionality not yet natively included can still be used as described `in our wiki `_. Notable Features diff --git a/telegram/_bot.py b/telegram/_bot.py index 4d8a778cc63..6cb51d2aea3 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -6131,6 +6131,7 @@ async def pin_chat_message( chat_id: Union[str, int], message_id: int, disable_notification: ODVInput[bool] = DEFAULT_NONE, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -6151,6 +6152,10 @@ async def pin_chat_message( disable_notification (:obj:`bool`, optional): Pass :obj:`True`, if it is not necessary to send a notification to all chat members about the new pinned message. Notifications are always disabled in channels and private chats. + business_connection_id (:obj:`str`, optional): Unique identifier of the business + connection on behalf of which the message will be pinned. + + .. versionadded:: NEXT.VERSION Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -6163,6 +6168,7 @@ async def pin_chat_message( "chat_id": chat_id, "message_id": message_id, "disable_notification": disable_notification, + "business_connection_id": business_connection_id, } return await self._post( @@ -6179,6 +6185,7 @@ async def unpin_chat_message( self, chat_id: Union[str, int], message_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -6195,8 +6202,13 @@ async def unpin_chat_message( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - message_id (:obj:`int`, optional): Identifier of a message to unpin. If not specified, + message_id (:obj:`int`, optional): Identifier of the message to unpin. Required if + :paramref:`business_connection_id` is specified. If not specified, the most recent pinned message (by sending date) will be unpinned. + business_connection_id (:obj:`str`, optional): Unique identifier of the business + connection on behalf of which the message will be unpinned. + + .. versionadded:: NEXT.VERSION Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -6205,7 +6217,11 @@ async def unpin_chat_message( :class:`telegram.error.TelegramError` """ - data: JSONDict = {"chat_id": chat_id, "message_id": message_id} + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "business_connection_id": business_connection_id, + } return await self._post( "unpinChatMessage", diff --git a/telegram/_chat.py b/telegram/_chat.py index 200e192c95f..02d80c94714 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -905,6 +905,7 @@ async def pin_message( self, message_id: int, disable_notification: ODVInput[bool] = DEFAULT_NONE, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -932,11 +933,13 @@ async def pin_message( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) async def unpin_message( self, message_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -963,6 +966,7 @@ async def unpin_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, message_id=message_id, + business_connection_id=business_connection_id, ) async def unpin_all_messages( diff --git a/telegram/_message.py b/telegram/_message.py index d362e42f59f..7d077a4d9a7 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -4106,11 +4106,18 @@ async def pin( """Shortcut for:: await bot.pin_chat_message( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see :meth:`telegram.Bot.pin_chat_message`. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id` to + :meth:`telegram.Bot.pin_chat_message`. + Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -4118,6 +4125,7 @@ async def pin( return await self.get_bot().pin_chat_message( chat_id=self.chat_id, message_id=self.message_id, + business_connection_id=self.business_connection_id, disable_notification=disable_notification, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4138,11 +4146,18 @@ async def unpin( """Shortcut for:: await bot.unpin_chat_message( - chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs ) For the documentation of the arguments, please see :meth:`telegram.Bot.unpin_chat_message`. + .. versionchanged:: NEXT.VERSION + Now also passes :attr:`business_connection_id` to + :meth:`telegram.Bot.pin_chat_message`. + Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -4150,6 +4165,7 @@ async def unpin( return await self.get_bot().unpin_chat_message( chat_id=self.chat_id, message_id=self.message_id, + business_connection_id=self.business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, diff --git a/telegram/_user.py b/telegram/_user.py index 7ea769f28b6..50dd66870f3 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -97,6 +97,10 @@ class User(TelegramObject): :meth:`telegram.Bot.get_me`. .. versionadded:: 21.1 + has_main_web_app (:obj:`bool`, optional): :obj:`True`, if the bot has the main Web App. + Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this user or bot. @@ -124,6 +128,11 @@ class User(TelegramObject): :meth:`telegram.Bot.get_me`. .. versionadded:: 21.1 + has_main_web_app (:obj:`bool`) Optional. :obj:`True`, if the bot has the main Web App. + Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION + .. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id` coincides with the :attr:`Chat.id` of the private chat with the user. This has been the case so far, but Telegram does not guarantee that this stays this way. @@ -135,6 +144,7 @@ class User(TelegramObject): "can_join_groups", "can_read_all_group_messages", "first_name", + "has_main_web_app", "id", "is_bot", "is_premium", @@ -158,6 +168,7 @@ def __init__( is_premium: Optional[bool] = None, added_to_attachment_menu: Optional[bool] = None, can_connect_to_business: Optional[bool] = None, + has_main_web_app: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -176,6 +187,7 @@ def __init__( self.is_premium: Optional[bool] = is_premium self.added_to_attachment_menu: Optional[bool] = added_to_attachment_menu self.can_connect_to_business: Optional[bool] = can_connect_to_business + self.has_main_web_app: Optional[bool] = has_main_web_app self._id_attrs = (self.id,) @@ -301,6 +313,7 @@ async def pin_message( self, message_id: int, disable_notification: ODVInput[bool] = DEFAULT_NONE, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -329,12 +342,14 @@ async def pin_message( write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, + business_connection_id=business_connection_id, api_kwargs=api_kwargs, ) async def unpin_message( self, message_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -363,6 +378,7 @@ async def unpin_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, message_id=message_id, + business_connection_id=business_connection_id, ) async def unpin_all_messages( diff --git a/telegram/constants.py b/telegram/constants.py index fb4bc9a19a9..1aba1f2a93a 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -151,7 +151,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=7) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=8) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 7d8d10e4902..4d0bef88b05 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -2238,6 +2238,7 @@ async def pin_chat_message( chat_id: Union[str, int], message_id: int, disable_notification: ODVInput[bool] = DEFAULT_NONE, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2254,6 +2255,7 @@ async def pin_chat_message( write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, + business_connection_id=business_connection_id, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) @@ -3739,6 +3741,7 @@ async def unpin_chat_message( self, chat_id: Union[str, int], message_id: Optional[int] = None, + business_connection_id: Optional[str] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3754,6 +3757,7 @@ async def unpin_chat_message( write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, + business_connection_id=business_connection_id, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) diff --git a/tests/test_bot.py b/tests/test_bot.py index 85232a8c708..e3476359275 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2207,6 +2207,8 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): await bot.send_message(2, "text", business_connection_id=42) await bot.stop_poll(chat_id=1, message_id=2, business_connection_id=42) + await bot.pin_chat_message(chat_id=1, message_id=2, business_connection_id=42) + await bot.unpin_chat_message(chat_id=1, business_connection_id=42) async def test_message_effect_id_argument(self, bot, monkeypatch): """We can't test every single method easily, so we just test one. Our linting will catch diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 5e41b5993cf..75c7fc63a33 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -465,11 +465,14 @@ async def make_assertion(*args, **kwargs): assert check_shortcut_signature( CallbackQuery.pin_message, Bot.pin_chat_message, - ["message_id", "chat_id"], + ["message_id", "chat_id", "business_connection_id"], [], ) assert await check_shortcut_call( - callback_query.pin_message, callback_query.get_bot(), "pin_chat_message" + callback_query.pin_message, + callback_query.get_bot(), + "pin_chat_message", + ["business_connection_id"], ) assert await check_defaults_handling(callback_query.pin_message, callback_query.get_bot()) @@ -490,14 +493,15 @@ async def make_assertion(*args, **kwargs): assert check_shortcut_signature( CallbackQuery.unpin_message, Bot.unpin_chat_message, - ["message_id", "chat_id"], + ["message_id", "chat_id", "business_connection_id"], [], ) assert await check_shortcut_call( callback_query.unpin_message, callback_query.get_bot(), "unpin_chat_message", - shortcut_kwargs=["message_id", "chat_id"], + shortcut_kwargs=["message_id"], + skip_params=["business_connection_id"], ) assert await check_defaults_handling( callback_query.unpin_message, callback_query.get_bot() diff --git a/tests/test_message.py b/tests/test_message.py index 9e575a99f45..7b4fc0a45a7 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -2592,9 +2592,17 @@ async def make_assertion(*args, **kwargs): return chat_id and message_id assert check_shortcut_signature( - Message.pin, Bot.pin_chat_message, ["chat_id", "message_id"], [] + Message.pin, + Bot.pin_chat_message, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.pin, + message.get_bot(), + "pin_chat_message", + shortcut_kwargs=["chat_id", "message_id", "business_connection_id"], ) - assert await check_shortcut_call(message.pin, message.get_bot(), "pin_chat_message") assert await check_defaults_handling(message.pin, message.get_bot()) monkeypatch.setattr(message.get_bot(), "pin_chat_message", make_assertion) @@ -2607,13 +2615,16 @@ async def make_assertion(*args, **kwargs): return chat_id and message_id assert check_shortcut_signature( - Message.unpin, Bot.unpin_chat_message, ["chat_id", "message_id"], [] + Message.unpin, + Bot.unpin_chat_message, + ["chat_id", "message_id", "business_connection_id"], + [], ) assert await check_shortcut_call( message.unpin, message.get_bot(), "unpin_chat_message", - shortcut_kwargs=["chat_id", "message_id"], + shortcut_kwargs=["chat_id", "message_id", "business_connection_id"], ) assert await check_defaults_handling(message.unpin, message.get_bot()) diff --git a/tests/test_user.py b/tests/test_user.py index 06936532860..ca5ea20118a 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -43,6 +43,7 @@ def json_dict(): "is_premium": TestUserBase.is_premium, "added_to_attachment_menu": TestUserBase.added_to_attachment_menu, "can_connect_to_business": TestUserBase.can_connect_to_business, + "has_main_web_app": TestUserBase.has_main_web_app, } @@ -61,6 +62,7 @@ def user(bot): is_premium=TestUserBase.is_premium, added_to_attachment_menu=TestUserBase.added_to_attachment_menu, can_connect_to_business=TestUserBase.can_connect_to_business, + has_main_web_app=TestUserBase.has_main_web_app, ) user.set_bot(bot) user._unfreeze() @@ -80,6 +82,7 @@ class TestUserBase: is_premium = True added_to_attachment_menu = False can_connect_to_business = True + has_main_web_app = False class TestUserWithoutRequest(TestUserBase): @@ -104,6 +107,7 @@ def test_de_json(self, json_dict, bot): assert user.is_premium == self.is_premium assert user.added_to_attachment_menu == self.added_to_attachment_menu assert user.can_connect_to_business == self.can_connect_to_business + assert user.has_main_web_app == self.has_main_web_app def test_to_dict(self, user): user_dict = user.to_dict() @@ -121,6 +125,7 @@ def test_to_dict(self, user): assert user_dict["is_premium"] == user.is_premium assert user_dict["added_to_attachment_menu"] == user.added_to_attachment_menu assert user_dict["can_connect_to_business"] == user.can_connect_to_business + assert user_dict["has_main_web_app"] == user.has_main_web_app def test_equality(self): a = User(self.id_, self.first_name, self.is_bot, self.last_name) From b89f5d6126a6b75b9348d7c2644e309345d2e6fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:00:26 +0200 Subject: [PATCH 10/26] Bump `dependabot/fetch-metadata` from 2.1.0 to 2.2.0 (#4411) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/dependabot-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-prs.yml b/.github/workflows/dependabot-prs.yml index 80bace2d95d..58fbd304719 100644 --- a/.github/workflows/dependabot-prs.yml +++ b/.github/workflows/dependabot-prs.yml @@ -16,7 +16,7 @@ jobs: - name: Fetch Dependabot metadata id: dependabot-metadata - uses: dependabot/fetch-metadata@v2.1.0 + uses: dependabot/fetch-metadata@v2.2.0 - uses: actions/checkout@v4 with: From e637d1733ceae3942d32e156632ac8d7a1e95584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:01:31 +0200 Subject: [PATCH 11/26] Bump `pytest` from 8.2.2 to 8.3.2 (#4403) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-unit-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-unit-tests.txt b/requirements-unit-tests.txt index eb69f9d9283..df1e83b4573 100644 --- a/requirements-unit-tests.txt +++ b/requirements-unit-tests.txt @@ -4,7 +4,7 @@ build # For the test suite -pytest==8.2.2 +pytest==8.3.2 # needed because pytest doesn't come with native support for coroutines as tests pytest-asyncio==0.21.2 From 3a49372591b0b1890241a3fdc0e2c4316bef6020 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 2 Aug 2024 22:28:38 +0200 Subject: [PATCH 12/26] Add Parameter `read_file_handle` to `InputFile` (#4388) --- telegram/_bot.py | 47 +++++++------ telegram/_files/inputfile.py | 44 ++++++++++-- telegram/_files/inputmedia.py | 37 +++++----- telegram/_files/inputsticker.py | 3 +- telegram/_utils/files.py | 19 ++++-- telegram/_utils/types.py | 2 +- telegram/request/_requestdata.py | 6 +- telegram/request/_requestparameter.py | 6 +- tests/_files/test_inputfile.py | 98 ++++++++++++++++++++++----- tests/request/test_request.py | 14 ++++ tests/test_official/exceptions.py | 2 +- 11 files changed, 206 insertions(+), 72 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 6cb51d2aea3..5f4927cb863 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -1305,8 +1305,8 @@ async def send_photo( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - photo (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.PhotoSize`): Photo to send. + photo (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \ + | :class:`pathlib.Path` | :class:`telegram.PhotoSize`): Photo to send. |fileinput| Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. @@ -1465,9 +1465,9 @@ async def send_audio( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - audio (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Audio`): Audio file to send. - |fileinput| + audio (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \ + :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Audio`): Audio file to + send. |fileinput| Lastly you can pass an existing :class:`telegram.Audio` object to send. .. versionchanged:: 13.2 @@ -1617,8 +1617,8 @@ async def send_document( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - document (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Document`): File to send. + document (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \ + :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Document`): File to send. |fileinput| Lastly you can pass an existing :class:`telegram.Document` object to send. @@ -1755,8 +1755,8 @@ async def send_sticker( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Sticker`): Sticker to send. + sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \ + :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Sticker`): Sticker to send. |fileinput| Video stickers can only be sent by a ``file_id``. Video and animated stickers can't be sent via an HTTP URL. @@ -1895,8 +1895,8 @@ async def send_video( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - video (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Video`): Video file to send. + video (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \ + | :class:`pathlib.Path` | :class:`telegram.Video`): Video file to send. |fileinput| Lastly you can pass an existing :class:`telegram.Video` object to send. @@ -2059,8 +2059,9 @@ async def send_video_note( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - video_note (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.VideoNote`): Video note to send. + video_note (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \ + :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.VideoNote`): Video note + to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. |uploadinput| @@ -2209,9 +2210,9 @@ async def send_animation( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - animation (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Animation`): Animation to send. - |fileinput| + animation (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \ + :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Animation`): Animation to + send. |fileinput| Lastly you can pass an existing :class:`telegram.Animation` object to send. .. versionchanged:: 13.2 @@ -2371,8 +2372,8 @@ async def send_voice( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - voice (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Voice`): Voice file to send. + voice (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \ + | :class:`pathlib.Path` | :class:`telegram.Voice`): Voice file to send. |fileinput| Lastly you can pass an existing :class:`telegram.Voice` object to send. @@ -6370,8 +6371,9 @@ async def upload_sticker_file( Args: user_id (:obj:`int`): User identifier of sticker file owner. - sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): - A file with the sticker in the ``".WEBP"``, ``".PNG"``, ``".TGS"`` or ``".WEBM"`` + sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \ + :obj:`bytes` | :class:`pathlib.Path`): A file with the sticker in the + ``".WEBP"``, ``".PNG"``, ``".TGS"`` or ``".WEBM"`` format. See `here `_ for technical requirements . |uploadinput| @@ -6695,8 +6697,9 @@ async def set_sticker_set_thumbnail( .. versionadded:: 21.1 - thumbnail (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ - optional): A **.WEBP** or **.PNG** image with the thumbnail, must + thumbnail (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \ + :obj:`bytes` | :class:`pathlib.Path`, optional): A **.WEBP** or **.PNG** image + with the thumbnail, must be up to :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_THUMBNAIL_SIZE` kilobytes in size and have width and height of exactly :tg-const:`telegram.constants.StickerSetLimit.STATIC_THUMB_DIMENSIONS` px, or a diff --git a/telegram/_files/inputfile.py b/telegram/_files/inputfile.py index 9a07f6d65fa..8f9c24a2076 100644 --- a/telegram/_files/inputfile.py +++ b/telegram/_files/inputfile.py @@ -22,7 +22,7 @@ from typing import IO, Optional, Union from uuid import uuid4 -from telegram._utils.files import load_file +from telegram._utils.files import guess_file_name, load_file from telegram._utils.strings import TextEncoding from telegram._utils.types import FieldTuple @@ -53,9 +53,36 @@ class InputFile: attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in the request to Telegram should point to the multipart data via an ``attach://`` URI. Defaults to `False`. + read_file_handle (:obj:`bool`, optional): If :obj:`True` and :paramref:`obj` is a file + handle, the data will be read from the file handle on initialization of this object. + If :obj:`False`, the file handle will be passed on to the + `networking backend `_ which will have to + handle the reading. Defaults to :obj:`True`. + + Tip: + If you upload extremely large files, you may want to set this to :obj:`False` to + avoid reading the complete file into memory. Additionally, this may be supported + better by the networking backend (in particular it is handled better by + the default :class:`~telegram.request.HTTPXRequest`). + + Important: + If you set this to :obj:`False`, you have to ensure that the file handle is still + open when the request is made. In particular, the following snippet can *not* work + as expected. + + .. code-block:: python + + with open('file.txt', 'rb') as file: + input_file = InputFile(file, read_file_handle=False) + + # here the file handle is already closed and the upload will fail + await bot.send_document(chat_id, input_file) + + .. versionadded:: NEXT.VERSION + Attributes: - input_file_content (:obj:`bytes`): The binary content of the file to send. + input_file_content (:obj:`bytes` | :class:`IO`): The binary content of the file to send. attach_name (:obj:`str`): Optional. If present, the parameter this file belongs to in the request to Telegram should point to the multipart data via a an URI of the form ``attach://`` URI. @@ -71,14 +98,18 @@ def __init__( obj: Union[IO[bytes], bytes, str], filename: Optional[str] = None, attach: bool = False, + read_file_handle: bool = True, ): if isinstance(obj, bytes): - self.input_file_content: bytes = obj + self.input_file_content: Union[bytes, IO[bytes]] = obj elif isinstance(obj, str): self.input_file_content = obj.encode(TextEncoding.UTF_8) - else: + elif read_file_handle: reported_filename, self.input_file_content = load_file(obj) filename = filename or reported_filename + else: + self.input_file_content = obj + filename = filename or guess_file_name(obj) self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None @@ -95,8 +126,11 @@ def __init__( def field_tuple(self) -> FieldTuple: """Field tuple representing the contents of the file for upload to the Telegram servers. + .. versionchanged:: NEXT.VERSION + Content may now be a file handle. + Returns: - Tuple[:obj:`str`, :obj:`bytes`, :obj:`str`]: + Tuple[:obj:`str`, :obj:`bytes` | :class:`IO`, :obj:`str`]: """ return self.filename, self.input_file_content, self.mimetype diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 692369130a4..c33a87a2d44 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -50,8 +50,8 @@ class InputMedia(TelegramObject): Args: media_type (:obj:`str`): Type of media that the instance represents. - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Animation` | :class:`telegram.Audio` | \ + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path` | :class:`telegram.Animation` | :class:`telegram.Audio` | \ :class:`telegram.Document` | :class:`telegram.PhotoSize` | \ :class:`telegram.Video`): File to send. |fileinputnopath| @@ -128,8 +128,9 @@ class InputPaidMedia(TelegramObject): Args: type (:obj:`str`): Type of media that the instance represents. - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.PhotoSize` | :class:`telegram.Video`): File to send. |fileinputnopath| + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path` | :class:`telegram.PhotoSize` | :class:`telegram.Video`): File + to send. |fileinputnopath| Lastly you can pass an existing telegram media object of the corresponding type to send. @@ -167,8 +168,8 @@ class InputPaidMediaPhoto(InputPaidMedia): .. versionadded:: 21.4 Args: - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.PhotoSize`): File to send. |fileinputnopath| + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path` | :class:`telegram.PhotoSize`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. Attributes: @@ -207,8 +208,8 @@ class InputPaidMediaVideo(InputPaidMedia): changed by Telegram. Args: - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Video`): File to send. |fileinputnopath| + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path` | :class:`telegram.Video`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Video` object to send. thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ optional): |thumbdocstringnopath| @@ -278,8 +279,8 @@ class InputMediaAnimation(InputMedia): |removed_thumb_note| Args: - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Animation`): File to send. |fileinputnopath| + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path` | :class:`telegram.Animation`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Animation` object to send. .. versionchanged:: 13.2 @@ -401,8 +402,8 @@ class InputMediaPhoto(InputMedia): .. seealso:: :wiki:`Working with Files and Media ` Args: - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.PhotoSize`): File to send. |fileinputnopath| + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path` | :class:`telegram.PhotoSize`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. .. versionchanged:: 13.2 @@ -501,8 +502,8 @@ class InputMediaVideo(InputMedia): |removed_thumb_note| Args: - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Video`): File to send. |fileinputnopath| + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path` | :class:`telegram.Video`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Video` object to send. .. versionchanged:: 13.2 @@ -639,8 +640,8 @@ class InputMediaAudio(InputMedia): |removed_thumb_note| Args: - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Audio`): File to send. |fileinputnopath| + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \ + :class:`pathlib.Path` | :class:`telegram.Audio`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Audio` object to send. .. versionchanged:: 13.2 @@ -743,8 +744,8 @@ class InputMediaDocument(InputMedia): |removed_thumb_note| Args: - media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Document`): File to send. |fileinputnopath| + media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \ + | :class:`pathlib.Path` | :class:`telegram.Document`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Document` object to send. .. versionchanged:: 13.2 diff --git a/telegram/_files/inputsticker.py b/telegram/_files/inputsticker.py index 5539d610d83..89f1db81d0c 100644 --- a/telegram/_files/inputsticker.py +++ b/telegram/_files/inputsticker.py @@ -41,7 +41,8 @@ class InputSticker(TelegramObject): order of the arguments has changed. Args: - sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): The + sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \ + | :class:`pathlib.Path`): The added sticker. |uploadinputnopath| Animated and video stickers can't be uploaded via HTTP URL. emoji_list (Sequence[:obj:`str`]): Sequence of diff --git a/telegram/_utils/files.py b/telegram/_utils/files.py index 387743025ee..121c7b3392e 100644 --- a/telegram/_utils/files.py +++ b/telegram/_utils/files.py @@ -61,14 +61,21 @@ def load_file( except AttributeError: return None, cast(Union[bytes, "InputFile", str, Path], obj) - if hasattr(obj, "name") and not isinstance(obj.name, int): - filename = Path(obj.name).name - else: - filename = None + filename = guess_file_name(obj) return filename, contents +def guess_file_name(obj: FileInput) -> Optional[str]: + """If the input is a file handle, read name and return it. Otherwise, return + the input unchanged. + """ + if hasattr(obj, "name") and not isinstance(obj.name, int): + return Path(obj.name).name + + return None + + def is_local_file(obj: Optional[FilePathInput]) -> bool: """ Checks if a given string is a file on local system. @@ -110,8 +117,8 @@ def parse_file_input( # pylint: disable=too-many-return-statements attribute. Args: - file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | Telegram media object): The - input to parse. + file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | :class:`~telegram.InputFile`\ + | Telegram media object): The input to parse. tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g. :class:`telegram.Animation`. filename (:obj:`str`, optional): The filename. Only relevant in case an diff --git a/telegram/_utils/types.py b/telegram/_utils/types.py index efde2807f2b..8a01fdc2dea 100644 --- a/telegram/_utils/types.py +++ b/telegram/_utils/types.py @@ -82,7 +82,7 @@ .. versionadded:: 20.0 """ -FieldTuple = Tuple[str, bytes, str] +FieldTuple = Tuple[str, Union[bytes, IO[bytes]], str] """Alias for return type of `InputFile.field_tuple`.""" UploadFileDict = Dict[str, FieldTuple] """Dictionary containing file data to be uploaded to the API.""" diff --git a/telegram/request/_requestdata.py b/telegram/request/_requestdata.py index 82d1e3d2cdc..a6b8752ee66 100644 --- a/telegram/request/_requestdata.py +++ b/telegram/request/_requestdata.py @@ -129,7 +129,11 @@ def json_payload(self) -> bytes: @property def multipart_data(self) -> UploadFileDict: - """Gives the files contained in this object as mapping of part name to encoded content.""" + """Gives the files contained in this object as mapping of part name to encoded content. + + .. versionchanged:: NEXT.VERSION + Content may now be a file handle. + """ multipart_data: UploadFileDict = {} for param in self._parameters: m_data = param.multipart_data diff --git a/telegram/request/_requestparameter.py b/telegram/request/_requestparameter.py index ab11cbce793..c3d19bdbd46 100644 --- a/telegram/request/_requestparameter.py +++ b/telegram/request/_requestparameter.py @@ -77,7 +77,11 @@ def json_value(self) -> Optional[str]: @property def multipart_data(self) -> Optional[UploadFileDict]: - """A dict with the file data to upload, if any.""" + """A dict with the file data to upload, if any. + + .. versionchanged:: NEXT.VERSION + Content may now be a file handle. + """ if not self.input_files: return None return { diff --git a/tests/_files/test_inputfile.py b/tests/_files/test_inputfile.py index 1f70cb5ccda..b7235497b92 100644 --- a/tests/_files/test_inputfile.py +++ b/tests/_files/test_inputfile.py @@ -19,7 +19,7 @@ import contextlib import subprocess import sys -from io import BytesIO +from io import BufferedReader, BytesIO import pytest @@ -66,21 +66,45 @@ def test_attach(self, attach): assert input_file.attach_name is None assert input_file.attach_uri is None - def test_mimetypes(self): + @pytest.mark.parametrize("read_file_handle", [True, False]) + def test_mimetypes_file_handle(self, read_file_handle): # Only test a few to make sure logic works okay - assert InputFile(data_file("telegram.jpg").open("rb")).mimetype == "image/jpeg" + assert ( + InputFile( + data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle + ).mimetype + == "image/jpeg" + ) # For some reason python can guess the type on macOS - assert InputFile(data_file("telegram.webp").open("rb")).mimetype in [ + assert InputFile( + data_file("telegram.webp").open("rb"), read_file_handle=read_file_handle + ).mimetype in [ "application/octet-stream", "image/webp", ] - assert InputFile(data_file("telegram.mp3").open("rb")).mimetype == "audio/mpeg" + assert ( + InputFile( + data_file("telegram.mp3").open("rb"), read_file_handle=read_file_handle + ).mimetype + == "audio/mpeg" + ) # For some reason windows drops the trailing i - assert InputFile(data_file("telegram.midi").open("rb")).mimetype in [ + assert InputFile( + data_file("telegram.midi").open("rb"), read_file_handle=read_file_handle + ).mimetype in [ "audio/mid", "audio/midi", ] + # Test string file + assert ( + InputFile( + data_file("text_file.txt").open("rb"), read_file_handle=read_file_handle + ).mimetype + == "text/plain" + ) + + def test_mimetypes_other(self): # Test guess from file assert InputFile(BytesIO(b"blah"), filename="tg.jpg").mimetype == "image/jpeg" assert InputFile(BytesIO(b"blah"), filename="tg.mp3").mimetype == "audio/mpeg" @@ -92,20 +116,49 @@ def test_mimetypes(self): ) assert InputFile(BytesIO(b"blah")).mimetype == "application/octet-stream" - # Test string file - assert InputFile(data_file("text_file.txt").open()).mimetype == "text/plain" - - def test_filenames(self): - assert InputFile(data_file("telegram.jpg").open("rb")).filename == "telegram.jpg" - assert InputFile(data_file("telegram.jpg").open("rb"), filename="blah").filename == "blah" + @pytest.mark.parametrize("read_file_handle", [True, False]) + def test_filenames(self, read_file_handle): + assert ( + InputFile( + data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle + ).filename + == "telegram.jpg" + ) assert ( - InputFile(data_file("telegram.jpg").open("rb"), filename="blah.jpg").filename + InputFile( + data_file("telegram.jpg").open("rb"), + filename="blah", + read_file_handle=read_file_handle, + ).filename + == "blah" + ) + assert ( + InputFile( + data_file("telegram.jpg").open("rb"), + filename="blah.jpg", + read_file_handle=read_file_handle, + ).filename == "blah.jpg" ) - assert InputFile(data_file("telegram").open("rb")).filename == "telegram" - assert InputFile(data_file("telegram").open("rb"), filename="blah").filename == "blah" assert ( - InputFile(data_file("telegram").open("rb"), filename="blah.jpg").filename == "blah.jpg" + InputFile(data_file("telegram").open("rb"), read_file_handle=read_file_handle).filename + == "telegram" + ) + assert ( + InputFile( + data_file("telegram").open("rb"), + filename="blah", + read_file_handle=read_file_handle, + ).filename + == "blah" + ) + assert ( + InputFile( + data_file("telegram").open("rb"), + filename="blah.jpg", + read_file_handle=read_file_handle, + ).filename + == "blah.jpg" ) class MockedFileobject: @@ -140,6 +193,19 @@ def read(self): == "blah.jpg" ) + @pytest.mark.parametrize("read_file_handle", [True, False]) + def test_read_file_handle(self, read_file_handle): + input_file = InputFile( + data_file("telegram.jpg").open("rb"), read_file_handle=read_file_handle + ) + content = input_file.field_tuple[1] + if read_file_handle: + assert isinstance(content, bytes) + assert content == data_file("telegram.jpg").read_bytes() + else: + assert isinstance(content, BufferedReader) + assert content.read() == data_file("telegram.jpg").read_bytes() + class TestInputFileWithRequest: async def test_send_bytes(self, bot, chat_id): diff --git a/tests/request/test_request.py b/tests/request/test_request.py index 55100940b18..9ce5ee286e2 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -30,6 +30,7 @@ import pytest from httpx import AsyncHTTPTransport +from telegram import InputFile from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.strings import TextEncoding from telegram.error import ( @@ -48,6 +49,7 @@ from telegram.request._requestparameter import RequestParameter from telegram.warnings import PTBDeprecationWarning from tests.auxil.envvars import TEST_WITH_OPT_DEPS +from tests.auxil.files import data_file from tests.auxil.networking import NonchalantHttpxRequest from tests.auxil.slots import mro_slots @@ -821,3 +823,15 @@ async def test_do_request_wait_for_pool(self, httpx_request): task_2.exception() except (asyncio.CancelledError, asyncio.InvalidStateError): pass + + async def test_input_file_postponed_read(self, bot, chat_id): + """Here we test that `read_file_handle=False` is correctly handled by HTTPXRequest. + Since manually building the RequestData object has no real benefit, we simply use the Bot + for that. + """ + message = await bot.send_document( + document=InputFile(data_file("telegram.jpg").open("rb"), read_file_handle=False), + chat_id=chat_id, + ) + assert message.document + assert message.document.file_name == "telegram.jpg" diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py index c9e3b4e4650..c6122f312e9 100644 --- a/tests/test_official/exceptions.py +++ b/tests/test_official/exceptions.py @@ -117,7 +117,7 @@ class ParamTypeCheckingExceptions: "PassportElementError": {"source", "type", "message"}, "InputMedia": {"caption", "caption_entities", "media", "media_type", "parse_mode"}, "InputMedia(Animation|Audio|Document|Photo|Video|VideoNote|Voice)": {"filename"}, - "InputFile": {"attach", "filename", "obj"}, + "InputFile": {"attach", "filename", "obj", "read_file_handle"}, "MaybeInaccessibleMessage": {"date", "message_id", "chat"}, # attributes common to all subcls "ChatBoostSource": {"source"}, # attributes common to all subclasses "MessageOrigin": {"type", "date"}, # attributes common to all subclasses From 9c50a38512d4f6097c13f737e8ab49287841e5fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:27:39 +0200 Subject: [PATCH 13/26] Bump `test-summary/action` from 2.3 to 2.4 (#4410) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- .github/workflows/test_official.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_official.yml b/.github/workflows/test_official.yml index 5de11471eaa..e5d87a5fd6a 100644 --- a/.github/workflows/test_official.yml +++ b/.github/workflows/test_official.yml @@ -41,7 +41,7 @@ jobs: - name: Test Summary id: test_summary - uses: test-summary/action@v2.3 + uses: test-summary/action@v2.4 if: always() # always run, even if tests fail with: paths: .test_report_official.xml diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 6eac67758da..ccbd999be09 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -83,7 +83,7 @@ jobs: - name: Test Summary id: test_summary - uses: test-summary/action@v2.3 + uses: test-summary/action@v2.4 if: always() # always run, even if tests fail with: paths: | From 17875869022050e724b0d2c6163196f03e7b79b1 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sat, 3 Aug 2024 16:47:38 -0400 Subject: [PATCH 14/26] Bugfix for "Available In" Admonitions (#4413) --- docs/auxil/admonition_inserter.py | 7 ++++--- telegram/_payment/refundedpayment.py | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/auxil/admonition_inserter.py b/docs/auxil/admonition_inserter.py index 4227a845382..9455025331a 100644 --- a/docs/auxil/admonition_inserter.py +++ b/docs/auxil/admonition_inserter.py @@ -140,7 +140,7 @@ def _create_available_in(self) -> dict[type, str]: r"^\s*(?P[a-z_]+)" # Any number of spaces, named group for attribute r"\s?\(" # Optional whitespace, opening parenthesis r".*" # Any number of characters (that could denote a built-in type) - r":class:`.+`" # Marker of a classref, class name in backticks + r":(class|obj):`.+`" # Marker of a classref, class name in backticks r".*\):" # Any number of characters, closing parenthesis, colon. # The ^ colon above along with parenthesis is important because it makes sure that # the class is mentioned in the attribute description, not in free text. @@ -149,11 +149,11 @@ def _create_available_in(self) -> dict[type, str]: ) # for properties: there is no attr name in docstring. Just check if there's a class name. - prop_docstring_pattern = re.compile(r":class:`.+`.*:") + prop_docstring_pattern = re.compile(r":(class|obj):`.+`.*:") # pattern for iterating over potentially many class names in docstring for one attribute. # Tilde is optional (sometimes it is in the docstring, sometimes not). - single_class_name_pattern = re.compile(r":class:`~?(?P[\w.]*)`") + single_class_name_pattern = re.compile(r":(class|obj):`~?(?P[\w.]*)`") classes_to_inspect = inspect.getmembers(telegram, inspect.isclass) + inspect.getmembers( telegram.ext, inspect.isclass @@ -366,6 +366,7 @@ def _find_insert_pos_for_admonition(lines: list[str]) -> int: # to ".. admonition: Examples": ".. admonition:: Examples", ".. version", + "Args:", # The space after ":param" is important because docstring can contain # ":paramref:" in its plain text in the beginning of a line (e.g. ExtBot): ":param ", diff --git a/telegram/_payment/refundedpayment.py b/telegram/_payment/refundedpayment.py index 19bdfe84649..28d52226205 100644 --- a/telegram/_payment/refundedpayment.py +++ b/telegram/_payment/refundedpayment.py @@ -30,6 +30,8 @@ class RefundedPayment(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`telegram_payment_charge_id` is equal. + .. versionadded:: 21.4 + Args: currency (:obj:`str`): Three-letter ISO 4217 `currency `_ code, or ``XTR`` for From 8f9db63f4f4f27974acbeae9f85d0731016a8918 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:56:46 -0400 Subject: [PATCH 15/26] Bump `ruff` and Add New Rules (#4416) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- docs/auxil/sphinx_hooks.py | 5 +-- docs/auxil/tg_const_role.py | 3 +- pyproject.toml | 15 ++++++-- telegram/__main__.py | 2 +- telegram/_bot.py | 10 ++--- telegram/_telegramobject.py | 2 +- telegram/ext/_aioratelimiter.py | 2 +- telegram/ext/_application.py | 23 ++++++----- telegram/ext/_applicationbuilder.py | 2 +- telegram/ext/_baseupdateprocessor.py | 6 +-- telegram/ext/_callbackcontext.py | 4 +- telegram/ext/_callbackdatacache.py | 2 +- telegram/ext/_contexttypes.py | 2 +- telegram/ext/_extbot.py | 2 +- telegram/ext/_updater.py | 38 +++++++++---------- telegram/request/_baserequest.py | 12 +++--- telegram/request/_httpxrequest.py | 2 +- tests/_files/test_animation.py | 2 +- tests/_files/test_audio.py | 2 +- tests/_files/test_chatphoto.py | 2 +- tests/_files/test_document.py | 2 +- tests/_files/test_location.py | 2 +- tests/_files/test_photo.py | 2 +- tests/_files/test_sticker.py | 14 +++---- tests/_files/test_video.py | 2 +- tests/_files/test_videonote.py | 2 +- tests/_files/test_voice.py | 2 +- tests/_inline/test_inlinequeryhandler.py | 2 +- tests/conftest.py | 10 ++--- tests/ext/_utils/test_trackingdict.py | 4 +- tests/ext/test_application.py | 6 +-- tests/ext/test_applicationbuilder.py | 2 +- tests/ext/test_basepersistence.py | 2 +- tests/ext/test_baseupdateprocessor.py | 2 +- tests/ext/test_businessconnectionhandler.py | 2 +- .../test_businessmessagesdeletedhandler.py | 2 +- tests/ext/test_callbackdatacache.py | 2 +- tests/ext/test_callbackqueryhandler.py | 2 +- tests/ext/test_chatjoinrequesthandler.py | 2 +- tests/ext/test_chatmemberhandler.py | 2 +- tests/ext/test_contexttypes.py | 2 +- tests/ext/test_dictpersistence.py | 20 +++++----- tests/ext/test_filters.py | 2 +- tests/ext/test_jobqueue.py | 2 +- tests/ext/test_messagereactionhandler.py | 4 +- tests/ext/test_picklepersistence.py | 32 ++++++++-------- tests/ext/test_pollanswerhandler.py | 2 +- tests/ext/test_prefixhandler.py | 6 +-- tests/ext/test_updater.py | 6 +-- tests/request/test_request.py | 2 +- tests/test_bot.py | 2 +- tests/test_chatbackground.py | 4 +- tests/test_chatboost.py | 2 +- tests/test_chatmember.py | 2 +- tests/test_forum.py | 2 +- tests/test_messageorigin.py | 2 +- tests/test_pollhandler.py | 2 +- tests/test_reaction.py | 2 +- tests/test_stars.py | 8 ++-- tests/test_user.py | 2 +- 61 files changed, 161 insertions(+), 151 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0aa5f1a15bf..0da0cea1381 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.5.0' + rev: 'v0.5.6' hooks: - id: ruff name: ruff diff --git a/docs/auxil/sphinx_hooks.py b/docs/auxil/sphinx_hooks.py index 2cfbfe14049..6853a7fbe93 100644 --- a/docs/auxil/sphinx_hooks.py +++ b/docs/auxil/sphinx_hooks.py @@ -16,6 +16,7 @@ # 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 collections.abc +import contextlib import inspect import re import typing @@ -153,13 +154,11 @@ def autodoc_process_docstring( if isinstance(obj, telegram.ext.filters.BaseFilter): obj = obj.__class__ - try: + with contextlib.suppress(Exception): source_lines, start_line = inspect.getsourcelines(obj) end_line = start_line + len(source_lines) file = Path(inspect.getsourcefile(obj)).relative_to(FILE_ROOT) LINE_NUMBERS[name] = (file, start_line, end_line) - except Exception: - pass # Since we don't document the `__init__`, we call this manually to have it available for # attributes -- see the note above diff --git a/docs/auxil/tg_const_role.py b/docs/auxil/tg_const_role.py index d4d5961ad7f..e7d1b135b19 100644 --- a/docs/auxil/tg_const_role.py +++ b/docs/auxil/tg_const_role.py @@ -88,7 +88,6 @@ def process_link( refnode.rawsource, CONSTANTS_ROLE, ) - return title, target except Exception as exc: sphinx_logger.exception( "%s:%d: WARNING: Did not convert reference %s due to an exception.", @@ -98,3 +97,5 @@ def process_link( exc_info=exc, ) return title, target + else: + return title, target diff --git a/pyproject.toml b/pyproject.toml index 551c10b1dcc..9285497dfad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,14 +128,21 @@ explicit-preview-rules = true # TODO: Drop this when RUF022 and RUF023 are out ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PERF203"] select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE", "G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022", - "RUF023", "Q", "INP", "W", "YTT", "DTZ", "ARG", "T20", "FURB"] + "RUF023", "Q", "INP", "W", "YTT", "DTZ", "ARG", "T20", "FURB", "DOC", "TRY", + "D100", "D101", "D102", "D103", "D300", "D418", "D419", "S"] # Add "A (flake8-builtins)" after we drop pylint [tool.ruff.lint.per-file-ignores] "tests/*.py" = ["B018"] -"tests/**.py" = ["RUF012", "ASYNC230", "DTZ", "ARG", "T201"] -"docs/**.py" = ["INP001", "ARG"] -"examples/**.py" = ["ARG"] +"tests/**.py" = ["RUF012", "ASYNC230", "DTZ", "ARG", "T201", "ASYNC109", "D", "S", "TRY"] +"telegram/**.py" = ["TRY003"] +"telegram/ext/_applicationbuilder.py" = ["TRY004"] +"telegram/ext/filters.py" = ["D102"] +"docs/**.py" = ["INP001", "ARG", "D", "TRY003", "S"] +"examples/**.py" = ["ARG", "D", "S105", "TRY003"] + +[tool.ruff.lint.pydocstyle] +convention = "google" # PYLINT: [tool.pylint."messages control"] diff --git a/telegram/__main__.py b/telegram/__main__.py index 2491a330ac6..6a508e3574b 100644 --- a/telegram/__main__.py +++ b/telegram/__main__.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. # pylint: disable=missing-module-docstring -# ruff: noqa: T201 +# ruff: noqa: T201, D100, S603, S607 import subprocess import sys from typing import Optional diff --git a/telegram/_bot.py b/telegram/_bot.py index 5f4927cb863..d825a88789e 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -324,10 +324,10 @@ async def __aenter__(self: BT) -> BT: """ try: await self.initialize() - return self - except Exception as exc: + except Exception: await self.shutdown() - raise exc + raise + return self async def __aexit__( self, @@ -4271,7 +4271,7 @@ async def get_updates( self, offset: Optional[int] = None, limit: Optional[int] = None, - timeout: Optional[int] = None, + timeout: Optional[int] = None, # noqa: ASYNC109 allowed_updates: Optional[Sequence[str]] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -4387,7 +4387,7 @@ async def get_updates( self._LOGGER.critical( "Error while parsing updates! Received data was %r", result, exc_info=exc ) - raise exc + raise async def set_webhook( self, diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index 4f7ba92d602..6666b49b2ad 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -414,7 +414,7 @@ def _de_json( obj = cls(**data, api_kwargs=api_kwargs) except TypeError as exc: if "__init__() got an unexpected keyword argument" not in str(exc): - raise exc + raise if cls.__INIT_PARAMS_CHECK is not cls: signature = inspect.signature(cls) diff --git a/telegram/ext/_aioratelimiter.py b/telegram/ext/_aioratelimiter.py index 714bbc63f61..3a5af9b8530 100644 --- a/telegram/ext/_aioratelimiter.py +++ b/telegram/ext/_aioratelimiter.py @@ -250,7 +250,7 @@ async def process_request( _LOGGER.exception( "Rate limit hit after maximum of %d retries", max_retries, exc_info=exc ) - raise exc + raise sleep = exc.retry_after + 0.1 _LOGGER.info("Rate limit hit. Retrying after %f seconds", sleep) diff --git a/telegram/ext/_application.py b/telegram/ext/_application.py index 670793c9909..a1013ce570a 100644 --- a/telegram/ext/_application.py +++ b/telegram/ext/_application.py @@ -384,10 +384,10 @@ async def __aenter__(self: _AppType) -> _AppType: # noqa: PYI019 """ try: await self.initialize() - return self - except Exception as exc: + except Exception: await self.shutdown() - raise exc + raise + return self async def __aexit__( self, @@ -646,9 +646,9 @@ async def start(self) -> None: ) _LOGGER.info("Application started") - except Exception as exc: + except Exception: self._running = False - raise exc + raise async def stop(self) -> None: """Stops the process after processing any pending updates or tasks created by @@ -1227,7 +1227,7 @@ async def __create_task_callback( await self.process_error(update=update, error=exception, coroutine=coroutine) # Raise exception so that it can be set on the task and retrieved by task.exception() - raise exception + raise finally: self._mark_for_persistence_update(update=update) @@ -1445,14 +1445,16 @@ def add_handlers( 1: [CallbackQueryHandler(...), CommandHandler(...)] } + Raises: + :exc:`TypeError`: If the combination of arguments is invalid. """ if isinstance(handlers, dict) and not isinstance(group, DefaultValue): - raise ValueError("The `group` argument can only be used with a sequence of handlers.") + raise TypeError("The `group` argument can only be used with a sequence of handlers.") if isinstance(handlers, dict): for handler_group, grp_handlers in handlers.items(): if not isinstance(grp_handlers, (list, tuple)): - raise ValueError(f"Handlers for group {handler_group} must be a list or tuple") + raise TypeError(f"Handlers for group {handler_group} must be a list or tuple") for handler in grp_handlers: self.add_handler(handler, handler_group) @@ -1462,7 +1464,7 @@ def add_handlers( self.add_handler(handler, DefaultValue.get_value(group)) else: - raise ValueError( + raise TypeError( "The `handlers` argument must be a sequence of handlers or a " "dictionary where the keys are groups and values are sequences of handlers." ) @@ -1644,9 +1646,10 @@ async def _persistence_updater(self) -> None: self.__update_persistence_event.wait(), timeout=self.persistence.update_interval, ) - return except asyncio.TimeoutError: pass + else: + return # putting this *after* the wait_for so we don't immediately update on startup as # that would make little sense diff --git a/telegram/ext/_applicationbuilder.py b/telegram/ext/_applicationbuilder.py index a54d491614b..838c47495c6 100644 --- a/telegram/ext/_applicationbuilder.py +++ b/telegram/ext/_applicationbuilder.py @@ -207,7 +207,7 @@ def __init__(self: "InitApplicationBuilder"): self._job_queue: ODVInput[JobQueue] = DefaultValue(JobQueue()) except RuntimeError as exc: if "PTB must be installed via" not in str(exc): - raise exc + raise self._job_queue = DEFAULT_NONE self._persistence: ODVInput[BasePersistence] = DEFAULT_NONE diff --git a/telegram/ext/_baseupdateprocessor.py b/telegram/ext/_baseupdateprocessor.py index 89d51d96fc2..8e9af01fc3b 100644 --- a/telegram/ext/_baseupdateprocessor.py +++ b/telegram/ext/_baseupdateprocessor.py @@ -81,10 +81,10 @@ async def __aenter__(self: _BUPT) -> _BUPT: # noqa: PYI019 """ try: await self.initialize() - return self - except Exception as exc: + except Exception: await self.shutdown() - raise exc + raise + return self async def __aexit__( self, diff --git a/telegram/ext/_callbackcontext.py b/telegram/ext/_callbackcontext.py index 92113bae9a4..ea708e0e6b8 100644 --- a/telegram/ext/_callbackcontext.py +++ b/telegram/ext/_callbackcontext.py @@ -272,7 +272,9 @@ def drop_callback_data(self, callback_query: CallbackQuery) -> None: ) self.bot.callback_data_cache.drop_data(callback_query) else: - raise RuntimeError("telegram.Bot does not allow for arbitrary callback data.") + raise RuntimeError( # noqa: TRY004 + "telegram.Bot does not allow for arbitrary callback data." + ) @classmethod def from_error( diff --git a/telegram/ext/_callbackdatacache.py b/telegram/ext/_callbackdatacache.py index 150f3f055b0..02aebde5cfd 100644 --- a/telegram/ext/_callbackdatacache.py +++ b/telegram/ext/_callbackdatacache.py @@ -278,9 +278,9 @@ def __get_keyboard_uuid_and_button_data( button_data = keyboard_data.button_data[button] # Update the timestamp for the LRU keyboard_data.update_access_time() - return keyboard, button_data except KeyError: return None, InvalidCallbackData(callback_data) + return keyboard, button_data @staticmethod def extract_uuids(callback_data: str) -> Tuple[str, str]: diff --git a/telegram/ext/_contexttypes.py b/telegram/ext/_contexttypes.py index ea1065112cb..3754ff429a3 100644 --- a/telegram/ext/_contexttypes.py +++ b/telegram/ext/_contexttypes.py @@ -188,7 +188,7 @@ def __init__( # type: ignore[misc] user_data: Type[ADict] = dict, ): if not issubclass(context, CallbackContext): - raise ValueError("context must be a subclass of CallbackContext.") + raise TypeError("context must be a subclass of CallbackContext.") # We make all those only accessible via properties because we don't currently support # changing this at runtime, so overriding the attributes doesn't make sense diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 4d0bef88b05..d85d8822de4 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -637,7 +637,7 @@ async def get_updates( self, offset: Optional[int] = None, limit: Optional[int] = None, - timeout: Optional[int] = None, + timeout: Optional[int] = None, # noqa: ASYNC109 allowed_updates: Optional[Sequence[str]] = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, diff --git a/telegram/ext/_updater.py b/telegram/ext/_updater.py index 04d01a83eae..bdf14e0a920 100644 --- a/telegram/ext/_updater.py +++ b/telegram/ext/_updater.py @@ -145,10 +145,10 @@ async def __aenter__(self: _UpdaterType) -> _UpdaterType: # noqa: PYI019 """ try: await self.initialize() - return self - except Exception as exc: + except Exception: await self.shutdown() - raise exc + raise + return self async def __aexit__( self, @@ -214,7 +214,7 @@ async def shutdown(self) -> None: async def start_polling( self, poll_interval: float = 0.0, - timeout: int = 10, + timeout: int = 10, # noqa: ASYNC109 bootstrap_retries: int = -1, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -339,16 +339,15 @@ def callback(error: telegram.error.TelegramError) _LOGGER.debug("Waiting for polling to start") await polling_ready.wait() _LOGGER.debug("Polling updates from Telegram started") - - return self.update_queue - except Exception as exc: + except Exception: self._running = False - raise exc + raise + return self.update_queue async def _start_polling( self, poll_interval: float, - timeout: int, + timeout: int, # noqa: ASYNC109 read_timeout: ODVInput[float], write_timeout: ODVInput[float], connect_timeout: ODVInput[float], @@ -384,9 +383,9 @@ async def polling_action_cb() -> bool: pool_timeout=pool_timeout, allowed_updates=allowed_updates, ) - except TelegramError as exc: + except TelegramError: # TelegramErrors should be processed by the network retry loop - raise exc + raise except Exception as exc: # Other exceptions should not. Let's log them for now. _LOGGER.critical( @@ -446,13 +445,12 @@ async def _get_updates_cleanup() -> None: pool_timeout=pool_timeout, allowed_updates=allowed_updates, ) - except TelegramError as exc: - _LOGGER.error( + except TelegramError: + _LOGGER.exception( "Error while calling `get_updates` one more time to mark all fetched updates " "as read: %s. Suppressing error to ensure graceful shutdown. When polling for " "updates is restarted, updates may be fetched again. Please adjust timeouts " "via `ApplicationBuilder` or the parameter `get_updates_request` of `Bot`.", - exc_info=exc, ) self.__polling_cleanup_cb = _get_updates_cleanup @@ -623,9 +621,9 @@ async def start_webhook( _LOGGER.debug("Waiting for webhook server to start") await webhook_ready.wait() _LOGGER.debug("Webhook server started") - except Exception as exc: + except Exception: self._running = False - raise exc + raise # Return the update queue so the main thread can insert updates return self.update_queue @@ -761,11 +759,11 @@ async def do_action() -> bool: _LOGGER.debug("Timed out %s: %s", description, toe) # If failure is due to timeout, we should retry asap. cur_interval = 0 - except InvalidToken as pex: - _LOGGER.error("Invalid token; aborting") - raise pex + except InvalidToken: + _LOGGER.exception("Invalid token; aborting") + raise except TelegramError as telegram_exc: - _LOGGER.error("Error while %s: %s", description, telegram_exc) + _LOGGER.exception("Error while %s:", description) on_err_cb(telegram_exc) # increase waiting times on subsequent errors up to 30secs diff --git a/telegram/request/_baserequest.py b/telegram/request/_baserequest.py index d323dfb0620..aa0fe232cf4 100644 --- a/telegram/request/_baserequest.py +++ b/telegram/request/_baserequest.py @@ -115,10 +115,10 @@ async def __aenter__(self: RT) -> RT: """ try: await self.initialize() - return self - except Exception as exc: + except Exception: await self.shutdown() - raise exc + raise + return self async def __aexit__( self, @@ -339,8 +339,8 @@ async def _request_wrapper( connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) - except TelegramError as exc: - raise exc + except TelegramError: + raise except Exception as exc: raise NetworkError(f"Unknown error in HTTP implementation: {exc!r}") from exc @@ -408,7 +408,7 @@ def parse_json_payload(payload: bytes) -> JSONDict: try: return json.loads(decoded_s) except ValueError as exc: - _LOGGER.error('Can not load invalid JSON data: "%s"', decoded_s) + _LOGGER.exception('Can not load invalid JSON data: "%s"', decoded_s) raise TelegramError("Invalid server response") from exc @abc.abstractmethod diff --git a/telegram/request/_httpxrequest.py b/telegram/request/_httpxrequest.py index e9861539234..3dc6cf05fba 100644 --- a/telegram/request/_httpxrequest.py +++ b/telegram/request/_httpxrequest.py @@ -189,7 +189,7 @@ def __init__( self._client = self._build_client() except ImportError as exc: if "httpx[http2]" not in str(exc) and "httpx[socks]" not in str(exc): - raise exc + raise if "httpx[socks]" in str(exc): raise RuntimeError( diff --git a/tests/_files/test_animation.py b/tests/_files/test_animation.py index 74a17fca48a..5e991eabaa8 100644 --- a/tests/_files/test_animation.py +++ b/tests/_files/test_animation.py @@ -37,7 +37,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def animation_file(): with data_file("game.gif").open("rb") as f: yield f diff --git a/tests/_files/test_audio.py b/tests/_files/test_audio.py index 12857ddc6e9..ced1a1d5d75 100644 --- a/tests/_files/test_audio.py +++ b/tests/_files/test_audio.py @@ -37,7 +37,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def audio_file(): with data_file("telegram.mp3").open("rb") as f: yield f diff --git a/tests/_files/test_chatphoto.py b/tests/_files/test_chatphoto.py index ea853fd6b4f..27f3a26ba39 100644 --- a/tests/_files/test_chatphoto.py +++ b/tests/_files/test_chatphoto.py @@ -36,7 +36,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def chatphoto_file(): with data_file("telegram.jpg").open("rb") as f: yield f diff --git a/tests/_files/test_document.py b/tests/_files/test_document.py index 5d078fced20..913e290f963 100644 --- a/tests/_files/test_document.py +++ b/tests/_files/test_document.py @@ -37,7 +37,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def document_file(): with data_file("telegram.png").open("rb") as f: yield f diff --git a/tests/_files/test_location.py b/tests/_files/test_location.py index 5b94df4916b..f9a7e4c6ac6 100644 --- a/tests/_files/test_location.py +++ b/tests/_files/test_location.py @@ -235,7 +235,7 @@ async def test_send_location_default_protect_content(self, chat_id, default_bot, assert protected.has_protected_content assert not unprotected.has_protected_content - @pytest.mark.xfail() + @pytest.mark.xfail async def test_send_live_location(self, bot, chat_id): message = await bot.send_location( chat_id=chat_id, diff --git a/tests/_files/test_photo.py b/tests/_files/test_photo.py index ac53a048c27..1d48bba868b 100644 --- a/tests/_files/test_photo.py +++ b/tests/_files/test_photo.py @@ -38,7 +38,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def photo_file(): with data_file("telegram.jpg").open("rb") as f: yield f diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index bf60272ba04..a994325e1e9 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -49,7 +49,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def sticker_file(): with data_file("telegram.webp").open("rb") as file: yield file @@ -65,7 +65,7 @@ async def sticker(bot, chat_id): return sticker -@pytest.fixture() +@pytest.fixture def animated_sticker_file(): with data_file("telegram_animated_sticker.tgs").open("rb") as f: yield f @@ -77,7 +77,7 @@ async def animated_sticker(bot, chat_id): return (await bot.send_sticker(chat_id, sticker=f, read_timeout=50)).sticker -@pytest.fixture() +@pytest.fixture def video_sticker_file(): with data_file("telegram_video_sticker.webm").open("rb") as f: yield f @@ -524,7 +524,7 @@ async def test_error_send_empty_file_id(self, bot, chat_id): await bot.send_sticker(chat_id, "") -@pytest.fixture() +@pytest.fixture async def sticker_set(bot): ss = await bot.get_sticker_set(f"test_by_{bot.username}") if len(ss.stickers) > 100: @@ -538,7 +538,7 @@ async def sticker_set(bot): return ss -@pytest.fixture() +@pytest.fixture async def animated_sticker_set(bot): ss = await bot.get_sticker_set(f"animated_test_by_{bot.username}") if len(ss.stickers) > 100: @@ -552,7 +552,7 @@ async def animated_sticker_set(bot): return ss -@pytest.fixture() +@pytest.fixture async def video_sticker_set(bot): ss = await bot.get_sticker_set(f"video_test_by_{bot.username}") if len(ss.stickers) > 100: @@ -566,7 +566,7 @@ async def video_sticker_set(bot): return ss -@pytest.fixture() +@pytest.fixture def sticker_set_thumb_file(): with data_file("sticker_set_thumb.png").open("rb") as file: yield file diff --git a/tests/_files/test_video.py b/tests/_files/test_video.py index 7f31f9f6e4d..29a80930fda 100644 --- a/tests/_files/test_video.py +++ b/tests/_files/test_video.py @@ -37,7 +37,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def video_file(): with data_file("telegram.mp4").open("rb") as f: yield f diff --git a/tests/_files/test_videonote.py b/tests/_files/test_videonote.py index 625e85eba87..5f07936ca50 100644 --- a/tests/_files/test_videonote.py +++ b/tests/_files/test_videonote.py @@ -36,7 +36,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def video_note_file(): with data_file("telegram2.mp4").open("rb") as f: yield f diff --git a/tests/_files/test_voice.py b/tests/_files/test_voice.py index 8060221c724..0c92b53902b 100644 --- a/tests/_files/test_voice.py +++ b/tests/_files/test_voice.py @@ -37,7 +37,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def voice_file(): with data_file("telegram.ogg").open("rb") as f: yield f diff --git a/tests/_inline/test_inlinequeryhandler.py b/tests/_inline/test_inlinequeryhandler.py index 556cea46492..58431e59ebb 100644 --- a/tests/_inline/test_inlinequeryhandler.py +++ b/tests/_inline/test_inlinequeryhandler.py @@ -68,7 +68,7 @@ def false_update(request): return Update(update_id=2, **request.param) -@pytest.fixture() +@pytest.fixture def inline_query(bot): update = Update( 0, diff --git a/tests/conftest.py b/tests/conftest.py index a9ef3e68641..dd553f9fe82 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -124,7 +124,7 @@ async def bot(bot_info): yield _bot -@pytest.fixture() +@pytest.fixture def one_time_bot(bot_info): """A function scoped bot since the session bot would shutdown when `async with app` finishes""" return make_bot(bot_info) @@ -206,7 +206,7 @@ def provider_token(bot_info): return bot_info["payment_provider_token"] -@pytest.fixture() +@pytest.fixture async def app(bot_info): # We build a new bot each time so that we use `app` in a context manager without problems application = ( @@ -218,7 +218,7 @@ async def app(bot_info): await application.shutdown() -@pytest.fixture() +@pytest.fixture async def updater(bot_info): # We build a new bot each time so that we use `updater` in a context manager without problems up = Updater(bot=make_bot(bot_info), update_queue=asyncio.Queue()) @@ -228,7 +228,7 @@ async def updater(bot_info): await up.shutdown() -@pytest.fixture() +@pytest.fixture def thumb_file(): with data_file("thumb.jpg").open("rb") as f: yield f @@ -291,6 +291,6 @@ def timezone(tzinfo): return tzinfo -@pytest.fixture() +@pytest.fixture def tmp_file(tmp_path) -> Path: return tmp_path / uuid4().hex diff --git a/tests/ext/_utils/test_trackingdict.py b/tests/ext/_utils/test_trackingdict.py index 5ddf3d371b3..5fa21467e82 100644 --- a/tests/ext/_utils/test_trackingdict.py +++ b/tests/ext/_utils/test_trackingdict.py @@ -23,14 +23,14 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def td() -> TrackingDict: td = TrackingDict() td.update_no_track({1: 1}) return td -@pytest.fixture() +@pytest.fixture def data() -> dict: return {1: 1} diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index a74f3c739bf..c423c5d5fbf 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -639,11 +639,11 @@ async def test_add_handlers(self, app): assert len(app.handlers[-1]) == 1 # Now lets test the errors which can be produced- - with pytest.raises(ValueError, match="The `group` argument"): + with pytest.raises(TypeError, match="The `group` argument"): app.add_handlers({2: [msg_handler_set_count]}, group=0) - with pytest.raises(ValueError, match="Handlers for group 3"): + with pytest.raises(TypeError, match="Handlers for group 3"): app.add_handlers({3: msg_handler_set_count}) - with pytest.raises(ValueError, match="The `handlers` argument must be a sequence"): + with pytest.raises(TypeError, match="The `handlers` argument must be a sequence"): app.add_handlers({msg_handler_set_count}) await app.stop() diff --git a/tests/ext/test_applicationbuilder.py b/tests/ext/test_applicationbuilder.py index 9fcbc140141..189164d1b8c 100644 --- a/tests/ext/test_applicationbuilder.py +++ b/tests/ext/test_applicationbuilder.py @@ -48,7 +48,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def builder(): return ApplicationBuilder() diff --git a/tests/ext/test_basepersistence.py b/tests/ext/test_basepersistence.py index fb61e79bb55..d3c2ef771b9 100644 --- a/tests/ext/test_basepersistence.py +++ b/tests/ext/test_basepersistence.py @@ -261,7 +261,7 @@ def build_conversation_handler(name: str, persistent: bool = True) -> BaseHandle return TrackingConversationHandler(name=name, persistent=persistent) -@pytest.fixture() +@pytest.fixture def papp(request, bot_info) -> Application: papp_input = request.param store_data = {} diff --git a/tests/ext/test_baseupdateprocessor.py b/tests/ext/test_baseupdateprocessor.py index de36ea4aa38..c30e6417cad 100644 --- a/tests/ext/test_baseupdateprocessor.py +++ b/tests/ext/test_baseupdateprocessor.py @@ -28,7 +28,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def mock_processor(): class MockProcessor(SimpleUpdateProcessor): test_flag = False diff --git a/tests/ext/test_businessconnectionhandler.py b/tests/ext/test_businessconnectionhandler.py index c8d741332a4..c64c059dc01 100644 --- a/tests/ext/test_businessconnectionhandler.py +++ b/tests/ext/test_businessconnectionhandler.py @@ -88,7 +88,7 @@ def business_connection(bot): return bc -@pytest.fixture() +@pytest.fixture def business_connection_update(bot, business_connection): return Update(0, business_connection=business_connection) diff --git a/tests/ext/test_businessmessagesdeletedhandler.py b/tests/ext/test_businessmessagesdeletedhandler.py index a15a0a0c2b4..f377c948203 100644 --- a/tests/ext/test_businessmessagesdeletedhandler.py +++ b/tests/ext/test_businessmessagesdeletedhandler.py @@ -85,7 +85,7 @@ def business_messages_deleted(bot): return bmd -@pytest.fixture() +@pytest.fixture def business_messages_deleted_update(bot, business_messages_deleted): return Update(0, deleted_business_messages=business_messages_deleted) diff --git a/tests/ext/test_callbackdatacache.py b/tests/ext/test_callbackdatacache.py index be7fbace0a5..2082e809122 100644 --- a/tests/ext/test_callbackdatacache.py +++ b/tests/ext/test_callbackdatacache.py @@ -31,7 +31,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def callback_data_cache(bot): return CallbackDataCache(bot) diff --git a/tests/ext/test_callbackqueryhandler.py b/tests/ext/test_callbackqueryhandler.py index f96c074d2d1..c1dccd8df6b 100644 --- a/tests/ext/test_callbackqueryhandler.py +++ b/tests/ext/test_callbackqueryhandler.py @@ -65,7 +65,7 @@ def false_update(request): return Update(update_id=2, **request.param) -@pytest.fixture() +@pytest.fixture def callback_query(bot): update = Update(0, callback_query=CallbackQuery(2, User(1, "", False), None, data="test data")) update._unfreeze() diff --git a/tests/ext/test_chatjoinrequesthandler.py b/tests/ext/test_chatjoinrequesthandler.py index 3d2e38f4483..9b769e07e5f 100644 --- a/tests/ext/test_chatjoinrequesthandler.py +++ b/tests/ext/test_chatjoinrequesthandler.py @@ -96,7 +96,7 @@ def chat_join_request(time, bot): return cjr -@pytest.fixture() +@pytest.fixture def chat_join_request_update(bot, chat_join_request): return Update(0, chat_join_request=chat_join_request) diff --git a/tests/ext/test_chatmemberhandler.py b/tests/ext/test_chatmemberhandler.py index ada95288cfc..9f4705b594f 100644 --- a/tests/ext/test_chatmemberhandler.py +++ b/tests/ext/test_chatmemberhandler.py @@ -81,7 +81,7 @@ def chat_member_updated(): ) -@pytest.fixture() +@pytest.fixture def chat_member(bot, chat_member_updated): update = Update(0, my_chat_member=chat_member_updated) update._unfreeze() diff --git a/tests/ext/test_contexttypes.py b/tests/ext/test_contexttypes.py index afe73bb21d4..c1d488076ab 100644 --- a/tests/ext/test_contexttypes.py +++ b/tests/ext/test_contexttypes.py @@ -40,7 +40,7 @@ def test_data_init(self): assert ct.chat_data is float assert ct.user_data is bool - with pytest.raises(ValueError, match="subclass of CallbackContext"): + with pytest.raises(TypeError, match="subclass of CallbackContext"): ContextTypes(context=bool) def test_data_assignment(self): diff --git a/tests/ext/test_dictpersistence.py b/tests/ext/test_dictpersistence.py index ecd004858a4..0b725826c08 100644 --- a/tests/ext/test_dictpersistence.py +++ b/tests/ext/test_dictpersistence.py @@ -31,27 +31,27 @@ def _reset_callback_data_cache(cdc_bot): cdc_bot.callback_data_cache.clear_callback_queries() -@pytest.fixture() +@pytest.fixture def bot_data(): return {"test1": "test2", "test3": {"test4": "test5"}} -@pytest.fixture() +@pytest.fixture def chat_data(): return {-12345: {"test1": "test2", "test3": {"test4": "test5"}}, -67890: {3: "test4"}} -@pytest.fixture() +@pytest.fixture def user_data(): return {12345: {"test1": "test2", "test3": {"test4": "test5"}}, 67890: {3: "test4"}} -@pytest.fixture() +@pytest.fixture def callback_data(): return [("test1", 1000, {"button1": "test0", "button2": "test1"})], {"test1": "test2"} -@pytest.fixture() +@pytest.fixture def conversations(): return { "name1": {(123, 123): 3, (456, 654): 4}, @@ -60,27 +60,27 @@ def conversations(): } -@pytest.fixture() +@pytest.fixture def user_data_json(user_data): return json.dumps(user_data) -@pytest.fixture() +@pytest.fixture def chat_data_json(chat_data): return json.dumps(chat_data) -@pytest.fixture() +@pytest.fixture def bot_data_json(bot_data): return json.dumps(bot_data) -@pytest.fixture() +@pytest.fixture def callback_data_json(callback_data): return json.dumps(callback_data) -@pytest.fixture() +@pytest.fixture def conversations_json(conversations): return """{"name1": {"[123, 123]": 3, "[456, 654]": 4}, "name2": {"[123, 321]": 1, "[890, 890]": 2}, "name3": diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index 9cf47dc47fa..76b8dec916f 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -43,7 +43,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture() +@pytest.fixture def update(): update = Update( 0, diff --git a/tests/ext/test_jobqueue.py b/tests/ext/test_jobqueue.py index 929591d38b9..436dabbcc49 100644 --- a/tests/ext/test_jobqueue.py +++ b/tests/ext/test_jobqueue.py @@ -44,7 +44,7 @@ class CustomContext(CallbackContext): pass -@pytest.fixture() +@pytest.fixture async def job_queue(app): jq = JobQueue() jq.set_application(app) diff --git a/tests/ext/test_messagereactionhandler.py b/tests/ext/test_messagereactionhandler.py index 8f4172c9e34..3a71b20e730 100644 --- a/tests/ext/test_messagereactionhandler.py +++ b/tests/ext/test_messagereactionhandler.py @@ -112,12 +112,12 @@ def message_reaction_count_updated(time, bot): return mr -@pytest.fixture() +@pytest.fixture def message_reaction_update(bot, message_reaction_updated): return Update(0, message_reaction=message_reaction_updated) -@pytest.fixture() +@pytest.fixture def message_reaction_count_update(bot, message_reaction_count_updated): return Update(0, message_reaction_count=message_reaction_count_updated) diff --git a/tests/ext/test_picklepersistence.py b/tests/ext/test_picklepersistence.py index 86a26eeb3ba..ef23715b54d 100644 --- a/tests/ext/test_picklepersistence.py +++ b/tests/ext/test_picklepersistence.py @@ -50,27 +50,27 @@ def _reset_callback_data_cache(cdc_bot): cdc_bot.callback_data_cache.clear_callback_queries() -@pytest.fixture() +@pytest.fixture def bot_data(): return {"test1": "test2", "test3": {"test4": "test5"}} -@pytest.fixture() +@pytest.fixture def chat_data(): return {-12345: {"test1": "test2", "test3": {"test4": "test5"}}, -67890: {3: "test4"}} -@pytest.fixture() +@pytest.fixture def user_data(): return {12345: {"test1": "test2", "test3": {"test4": "test5"}}, 67890: {3: "test4"}} -@pytest.fixture() +@pytest.fixture def callback_data(): return [("test1", 1000, {"button1": "test0", "button2": "test1"})], {"test1": "test2"} -@pytest.fixture() +@pytest.fixture def conversations(): return { "name1": {(123, 123): 3, (456, 654): 4}, @@ -79,7 +79,7 @@ def conversations(): } -@pytest.fixture() +@pytest.fixture def pickle_persistence(): return PicklePersistence( filepath="pickletest", @@ -88,7 +88,7 @@ def pickle_persistence(): ) -@pytest.fixture() +@pytest.fixture def pickle_persistence_only_bot(): return PicklePersistence( filepath="pickletest", @@ -98,7 +98,7 @@ def pickle_persistence_only_bot(): ) -@pytest.fixture() +@pytest.fixture def pickle_persistence_only_chat(): return PicklePersistence( filepath="pickletest", @@ -108,7 +108,7 @@ def pickle_persistence_only_chat(): ) -@pytest.fixture() +@pytest.fixture def pickle_persistence_only_user(): return PicklePersistence( filepath="pickletest", @@ -118,7 +118,7 @@ def pickle_persistence_only_user(): ) -@pytest.fixture() +@pytest.fixture def pickle_persistence_only_callback(): return PicklePersistence( filepath="pickletest", @@ -128,7 +128,7 @@ def pickle_persistence_only_callback(): ) -@pytest.fixture() +@pytest.fixture def bad_pickle_files(): for name in [ "pickletest_user_data", @@ -142,7 +142,7 @@ def bad_pickle_files(): return True -@pytest.fixture() +@pytest.fixture def invalid_pickle_files(): for name in [ "pickletest_user_data", @@ -159,7 +159,7 @@ def invalid_pickle_files(): return True -@pytest.fixture() +@pytest.fixture def good_pickle_files(user_data, chat_data, bot_data, callback_data, conversations): data = { "user_data": user_data, @@ -183,7 +183,7 @@ def good_pickle_files(user_data, chat_data, bot_data, callback_data, conversatio return True -@pytest.fixture() +@pytest.fixture def pickle_files_wo_bot_data(user_data, chat_data, callback_data, conversations): data = { "user_data": user_data, @@ -204,7 +204,7 @@ def pickle_files_wo_bot_data(user_data, chat_data, callback_data, conversations) return True -@pytest.fixture() +@pytest.fixture def pickle_files_wo_callback_data(user_data, chat_data, bot_data, conversations): data = { "user_data": user_data, @@ -225,7 +225,7 @@ def pickle_files_wo_callback_data(user_data, chat_data, bot_data, conversations) return True -@pytest.fixture() +@pytest.fixture def update(bot): user = User(id=321, first_name="test_user", is_bot=False) chat = Chat(id=123, type="group") diff --git a/tests/ext/test_pollanswerhandler.py b/tests/ext/test_pollanswerhandler.py index d36b38d226e..d86b90ff1cc 100644 --- a/tests/ext/test_pollanswerhandler.py +++ b/tests/ext/test_pollanswerhandler.py @@ -67,7 +67,7 @@ def false_update(request): return Update(update_id=2, **request.param) -@pytest.fixture() +@pytest.fixture def poll_answer(bot): return Update(0, poll_answer=PollAnswer(1, [0, 1], User(2, "test user", False), Chat(1, ""))) diff --git a/tests/ext/test_prefixhandler.py b/tests/ext/test_prefixhandler.py index a42ec4e058e..f858e005228 100644 --- a/tests/ext/test_prefixhandler.py +++ b/tests/ext/test_prefixhandler.py @@ -56,15 +56,15 @@ def command(self, request): def commands(self, request): return TestPrefixHandler.COMMANDS[: request.param] - @pytest.fixture() + @pytest.fixture def prefix_message_text(self, prefix, command): return prefix + command - @pytest.fixture() + @pytest.fixture def prefix_message(self, prefix_message_text): return make_message(prefix_message_text) - @pytest.fixture() + @pytest.fixture def prefix_message_update(self, prefix_message): return make_message_update(prefix_message) diff --git a/tests/ext/test_updater.py b/tests/ext/test_updater.py index 1fd8985dea1..f7216100836 100644 --- a/tests/ext/test_updater.py +++ b/tests/ext/test_updater.py @@ -86,7 +86,7 @@ def _reset(self): # This is needed instead of pytest's temp_path because the file path gets too long on macOS # otherwise - @pytest.fixture() + @pytest.fixture def file_path(self) -> str: path = TEST_DATA_PATH / "test.sock" yield str(path) @@ -571,7 +571,7 @@ async def get_updates(*args, **kwargs): else: assert len(caplog.records) > 0 assert any( - "Error while getting Updates: TestMessage" in record.getMessage() + "Error while getting Updates:" in record.getMessage() and record.name == "telegram.ext.Updater" for record in caplog.records ) @@ -593,7 +593,7 @@ async def get_updates(*args, **kwargs): else: assert len(caplog.records) > 0 assert any( - "Error while getting Updates: TestMessage" in record.getMessage() + "Error while getting Updates:" in record.getMessage() and record.name == "telegram.ext.Updater" for record in caplog.records ) diff --git a/tests/request/test_request.py b/tests/request/test_request.py index 9ce5ee286e2..6adcdf7c068 100644 --- a/tests/request/test_request.py +++ b/tests/request/test_request.py @@ -74,7 +74,7 @@ async def make_assertion(*args, **kwargs): return make_assertion -@pytest.fixture() +@pytest.fixture async def httpx_request(): async with NonchalantHttpxRequest() as rq: yield rq diff --git a/tests/test_bot.py b/tests/test_bot.py index e3476359275..cdf00082d59 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -107,7 +107,7 @@ from .auxil.build_messages import make_message -@pytest.fixture() +@pytest.fixture async def message(bot, chat_id): # mostly used in tests for edit_message out = await bot.send_message( chat_id, "Text", disable_web_page_preview=True, disable_notification=True diff --git a/tests/test_chatbackground.py b/tests/test_chatbackground.py index 1f8be1eb451..900fc58709f 100644 --- a/tests/test_chatbackground.py +++ b/tests/test_chatbackground.py @@ -146,7 +146,7 @@ def iter_args( yield inst_at, json_at -@pytest.fixture() +@pytest.fixture def background_type(request): return request.param() @@ -254,7 +254,7 @@ def test_equality(self, background_type): assert hash(f) != hash(c) -@pytest.fixture() +@pytest.fixture def background_fill(request): return request.param() diff --git a/tests/test_chatboost.py b/tests/test_chatboost.py index bc33e1fe21e..f0ef143618a 100644 --- a/tests/test_chatboost.py +++ b/tests/test_chatboost.py @@ -107,7 +107,7 @@ def user_chat_boosts(chat_boost): ) -@pytest.fixture() +@pytest.fixture def chat_boost_source(request): return request.param() diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 28293a6cc80..b4eac51cfb3 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -181,7 +181,7 @@ def iter_args(instance: ChatMember, de_json_inst: ChatMember, include_optional: yield inst_at, json_at -@pytest.fixture() +@pytest.fixture def chat_member_type(request): return request.param() diff --git a/tests/test_forum.py b/tests/test_forum.py index 55419a3854f..1f143616ee9 100644 --- a/tests/test_forum.py +++ b/tests/test_forum.py @@ -56,7 +56,7 @@ async def forum_topic_object(forum_group_id, emoji_id): ) -@pytest.fixture() +@pytest.fixture async def real_topic(bot, emoji_id, forum_group_id): result = await bot.create_forum_topic( chat_id=forum_group_id, diff --git a/tests/test_messageorigin.py b/tests/test_messageorigin.py index 1e46fc53c9a..10d9fa77894 100644 --- a/tests/test_messageorigin.py +++ b/tests/test_messageorigin.py @@ -114,7 +114,7 @@ def iter_args( yield inst_at, json_at -@pytest.fixture() +@pytest.fixture def message_origin_type(request): return request.param() diff --git a/tests/test_pollhandler.py b/tests/test_pollhandler.py index 52b0675e15d..a87c45b28c0 100644 --- a/tests/test_pollhandler.py +++ b/tests/test_pollhandler.py @@ -68,7 +68,7 @@ def false_update(request): return Update(update_id=2, **request.param) -@pytest.fixture() +@pytest.fixture def poll(bot): return Update( 0, diff --git a/tests/test_reaction.py b/tests/test_reaction.py index 30e287ca5e5..6f3d3cb4621 100644 --- a/tests/test_reaction.py +++ b/tests/test_reaction.py @@ -89,7 +89,7 @@ def iter_args(instance: ReactionType, de_json_inst: ReactionType, include_option yield inst_at, json_at -@pytest.fixture() +@pytest.fixture def reaction_type(request): return request.param() diff --git a/tests/test_stars.py b/tests/test_stars.py index fb1339a7217..d3560af7d2f 100644 --- a/tests/test_stars.py +++ b/tests/test_stars.py @@ -49,12 +49,12 @@ def withdrawal_state_succeeded(): ) -@pytest.fixture() +@pytest.fixture def withdrawal_state_failed(): return RevenueWithdrawalStateFailed() -@pytest.fixture() +@pytest.fixture def withdrawal_state_pending(): return RevenueWithdrawalStatePending() @@ -65,7 +65,7 @@ def transaction_partner_user(): ) -@pytest.fixture() +@pytest.fixture def transaction_partner_other(): return TransactionPartnerOther() @@ -86,7 +86,7 @@ def star_transaction(): ) -@pytest.fixture() +@pytest.fixture def star_transactions(): return StarTransactions( transactions=[ diff --git a/tests/test_user.py b/tests/test_user.py index ca5ea20118a..8b0ae5df585 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -47,7 +47,7 @@ def json_dict(): } -@pytest.fixture() +@pytest.fixture def user(bot): user = User( id=TestUserBase.id_, From 374875c7862ff6e57cac2d6ea78580a22b5a182a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:57:26 +0200 Subject: [PATCH 16/26] Bump `sphinx` from 7.4.7 to 8.0.2 and `furo` from 2024.7.18 to 2024.8.6 (#4412) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- .github/CONTRIBUTING.rst | 2 +- .github/workflows/docs.yml | 2 +- docs/requirements-docs.txt | 4 ++-- docs/source/conf.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index 63906fabd6f..b64a368ac43 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -210,7 +210,7 @@ doc strings don't have a separate documentation site they generate, instead, the User facing documentation ------------------------- -We use `sphinx`_ to generate static HTML docs. To build them, first make sure you're running Python 3.9 or above and have the required dependencies installed as explained above. +We use `sphinx`_ to generate static HTML docs. To build them, first make sure you're running Python 3.10 or above and have the required dependencies installed as explained above. Then, run the following from the PTB root directory: .. code-block:: bash diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 73e123e17ea..b6a92ffdba8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - python-version: [3.9] + python-version: ['3.10'] os: [ubuntu-latest] fail-fast: False steps: diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 87f2c1911f6..7c3a4239807 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ -sphinx==7.4.7 -furo==2024.7.18 +sphinx==8.0.2 +furo==2024.8.6 furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1 sphinx-paramlinks==0.6.0 sphinxcontrib-mermaid==0.9.2 diff --git a/docs/source/conf.py b/docs/source/conf.py index fdc3b27e857..abad78812c6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,7 +30,7 @@ release = telegram.__version__ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "6.1.3" +needs_sphinx = "8.0.2" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom From 3017bf00a41afb8f1597aa19d9c464a4c45b32f8 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:58:08 -0400 Subject: [PATCH 17/26] Update Python 3.13 Test Suite to RC1 (#4415) --- .github/workflows/unit_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index ccbd999be09..c891151da11 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-beta.3'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-rc.1'] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: False steps: From fbf07bf1268716e1cc5e370bc13d2fd018a164e6 Mon Sep 17 00:00:00 2001 From: Palaptin <100526200+Palaptin@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:14:01 +0200 Subject: [PATCH 18/26] Improve Fixture Usage in `test_message.py` (#4431) --- tests/test_message.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_message.py b/tests/test_message.py index 7b4fc0a45a7..3cf237a5537 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -90,7 +90,7 @@ from tests.auxil.slots import mro_slots -@pytest.fixture(scope="module") +@pytest.fixture def message(bot): message = PytestMessage( message_id=TestMessageBase.id_, @@ -1193,16 +1193,20 @@ def test_link_with_id(self, message, type_, id_): # The leading - for group ids/ -100 for supergroup ids isn't supposed to be in the link assert message.link == f"https://t.me/c/{3}/{message.message_id}" - def test_link_with_topics(self, message): + @pytest.mark.parametrize("type_", argvalues=[Chat.SUPERGROUP, Chat.CHANNEL]) + def test_link_with_topics(self, message, type_): message.chat.username = None message.chat.id = -1003 + message.chat.type = type_ message.is_topic_message = True message.message_thread_id = 123 assert message.link == f"https://t.me/c/3/{message.message_id}?thread=123" - def test_link_with_reply(self, message): + @pytest.mark.parametrize("type_", argvalues=[Chat.SUPERGROUP, Chat.CHANNEL]) + def test_link_with_reply(self, message, type_): message.chat.username = None message.chat.id = -1003 + message.chat.type = type_ message.reply_to_message = Message(7, self.from_user, self.date, self.chat, text="Reply") message.message_thread_id = 123 assert message.link == f"https://t.me/c/3/{message.message_id}?thread=123" From a05362c79a7dc22f633f60d12e95092a29824040 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 19 Aug 2024 16:40:28 +0200 Subject: [PATCH 19/26] Remove Surplus Logging from `Updater` Network Loop (#4432) --- telegram/ext/_updater.py | 1 - tests/ext/test_updater.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/telegram/ext/_updater.py b/telegram/ext/_updater.py index bdf14e0a920..30635e40ada 100644 --- a/telegram/ext/_updater.py +++ b/telegram/ext/_updater.py @@ -763,7 +763,6 @@ async def do_action() -> bool: _LOGGER.exception("Invalid token; aborting") raise except TelegramError as telegram_exc: - _LOGGER.exception("Error while %s:", description) on_err_cb(telegram_exc) # increase waiting times on subsequent errors up to 30secs diff --git a/tests/ext/test_updater.py b/tests/ext/test_updater.py index f7216100836..84a86c988da 100644 --- a/tests/ext/test_updater.py +++ b/tests/ext/test_updater.py @@ -571,7 +571,7 @@ async def get_updates(*args, **kwargs): else: assert len(caplog.records) > 0 assert any( - "Error while getting Updates:" in record.getMessage() + "Exception happened while polling for updates." in record.getMessage() and record.name == "telegram.ext.Updater" for record in caplog.records ) @@ -593,7 +593,7 @@ async def get_updates(*args, **kwargs): else: assert len(caplog.records) > 0 assert any( - "Error while getting Updates:" in record.getMessage() + "Exception happened while polling for updates." in record.getMessage() and record.name == "telegram.ext.Updater" for record in caplog.records ) From 1e053811332ee5d80d00c70a14415e46594cbebf Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:16:35 +0200 Subject: [PATCH 20/26] Update Test Suite to New Test Channel Setup (#4435) --- tests/test_bot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index cdf00082d59..6615927adb7 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -3093,7 +3093,7 @@ async def test_get_chat_member_count(self, bot, channel_id): async def test_get_chat_member(self, bot, channel_id, chat_id): chat_member = await bot.get_chat_member(channel_id, chat_id) - assert chat_member.status == "administrator" + assert chat_member.status == "creator" assert chat_member.user.first_name == "PTB" assert chat_member.user.last_name == "Test user" @@ -3267,7 +3267,7 @@ async def test_promote_chat_member(self, bot, channel_id, monkeypatch): with pytest.raises(BadRequest, match="Not enough rights"): assert await bot.promote_chat_member( channel_id, - 95205500, + 1325859552, is_anonymous=True, can_change_info=True, can_post_messages=True, @@ -3290,7 +3290,7 @@ async def make_assertion(*args, **_): data = args[1] return ( data.get("chat_id") == channel_id - and data.get("user_id") == 95205500 + and data.get("user_id") == 1325859552 and data.get("is_anonymous") == 1 and data.get("can_change_info") == 2 and data.get("can_post_messages") == 3 @@ -3311,7 +3311,7 @@ async def make_assertion(*args, **_): monkeypatch.setattr(bot, "_post", make_assertion) assert await bot.promote_chat_member( channel_id, - 95205500, + 1325859552, is_anonymous=1, can_change_info=2, can_post_messages=3, From 01f689373ccca0a67c8491350984ddc8778a722e Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Sun, 1 Sep 2024 09:32:42 +0200 Subject: [PATCH 21/26] Bot API 7.9 (#4429) Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- README.rst | 4 +- docs/source/telegram.at-tree.rst | 1 + docs/source/telegram.reactiontypepaid.rst | 6 + telegram/__init__.py | 9 +- telegram/_bot.py | 138 +++++++++++++++++++++- telegram/_chat.py | 77 ++++++++++++ telegram/_chatinvitelink.py | 26 ++++ telegram/_chatmember.py | 15 ++- telegram/_message.py | 34 +++--- telegram/_payment/stars.py | 18 ++- telegram/_reaction.py | 58 +++++++-- telegram/constants.py | 27 ++++- telegram/ext/_extbot.py | 54 +++++++++ tests/auxil/ci_bots.py | 19 +-- tests/conftest.py | 5 + tests/test_bot.py | 35 ++++++ tests/test_chat.py | 48 ++++++++ tests/test_chatinvitelink.py | 10 ++ tests/test_chatmember.py | 6 +- tests/test_reaction.py | 9 +- tests/test_stars.py | 12 ++ 21 files changed, 557 insertions(+), 54 deletions(-) create mode 100644 docs/source/telegram.reactiontypepaid.rst diff --git a/README.rst b/README.rst index b2de996d2ed..f41e1288f7a 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.8-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.9-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with Telegram API support ~~~~~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **7.8** are natively supported by this library. +All types and methods of the Telegram Bot API **7.9** are natively supported by this library. In addition, Bot API functionality not yet natively included can still be used as described `in our wiki `_. Notable Features diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 8d3238a27e4..bb6e4c81472 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -130,6 +130,7 @@ Available Types telegram.reactiontype telegram.reactiontypecustomemoji telegram.reactiontypeemoji + telegram.reactiontypepaid telegram.replykeyboardmarkup telegram.replykeyboardremove telegram.replyparameters diff --git a/docs/source/telegram.reactiontypepaid.rst b/docs/source/telegram.reactiontypepaid.rst new file mode 100644 index 00000000000..f5035a1ba5b --- /dev/null +++ b/docs/source/telegram.reactiontypepaid.rst @@ -0,0 +1,6 @@ +ReactionTypePaid +================ + +.. autoclass:: telegram.ReactionTypePaid + :members: + :show-inheritance: diff --git a/telegram/__init__.py b/telegram/__init__.py index 5b52bf85c40..7b5803a3e9f 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -204,6 +204,7 @@ "ReactionType", "ReactionTypeCustomEmoji", "ReactionTypeEmoji", + "ReactionTypePaid", "RefundedPayment", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", @@ -467,7 +468,13 @@ from ._payment.successfulpayment import SuccessfulPayment from ._poll import InputPollOption, Poll, PollAnswer, PollOption from ._proximityalerttriggered import ProximityAlertTriggered -from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji +from ._reaction import ( + ReactionCount, + ReactionType, + ReactionTypeCustomEmoji, + ReactionTypeEmoji, + ReactionTypePaid, +) from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote from ._replykeyboardmarkup import ReplyKeyboardMarkup from ._replykeyboardremove import ReplyKeyboardRemove diff --git a/telegram/_bot.py b/telegram/_bot.py index d825a88789e..7172344cb4b 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Bot.""" + import asyncio import contextlib import copy @@ -8179,7 +8180,7 @@ async def edit_forum_topic( ) -> bool: """ Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must - be an administrator in the chat for this to work and must have + be an administrator in the chat for this to work and must have the :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, unless it is the creator of the topic. @@ -8447,7 +8448,7 @@ async def edit_general_forum_topic( ) -> bool: """ Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot - must be an administrator in the chat for this to work and must have + must be an administrator in the chat for this to work and must have the :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. .. versionadded:: 20.0 @@ -8946,7 +8947,7 @@ async def set_message_reaction( """ Use this method to change the chosen reactions on a message. Service messages can't be reacted to. Automatically forwarded messages from a channel to its discussion group have - the same available reactions as messages in the channel. + the same available reactions as messages in the channel. Bots can't use paid reactions. .. versionadded:: 20.8 @@ -8959,7 +8960,8 @@ async def set_message_reaction( :class:`telegram.ReactionType` | :obj:`str`, optional): A list of reaction types to set on the message. Currently, as non-premium users, bots can set up to one reaction per message. A custom emoji reaction can be used if it is either - already present on the message or explicitly allowed by chat administrators. + already present on the message or explicitly allowed by chat administrators. Paid + reactions can't be used by bots. Tip: Passed :obj:`str` values will be converted to either @@ -9201,6 +9203,7 @@ async def send_paid_media( protect_content: ODVInput[bool] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -9210,12 +9213,14 @@ async def send_paid_media( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, ) -> Message: - """Use this method to send paid media to channel chats. + """Use this method to send paid media. .. versionadded:: 21.4 Args: - chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| If the chat is a channel, all + Telegram Star proceeds from this media will be credited to the chat's balance. + Otherwise, they will be credited to the bot's balance. star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to the media. media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be @@ -9233,6 +9238,9 @@ async def send_paid_media( :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -9274,8 +9282,122 @@ async def send_paid_media( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) + async def create_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + subscription_period: int, + subscription_price: int, + name: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> ChatInviteLink: + """ + Use this method to create a `subscription invite link `_ for a channel chat. + The bot must have the :attr:`~telegram.ChatPermissions.can_invite_users` administrator + right. The link can be edited using the :meth:`edit_chat_subscription_invite_link` or + revoked using the :meth:`revoke_chat_invite_link`. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + subscription_period (:obj:`int`): The number of seconds the subscription will be + active for before the next payment. Currently, it must always be + :tg-const:`telegram.constants.ChatSubscriptionLimit.SUBSCRIPTION_PERIOD` (30 days). + subscription_price (:obj:`int`): The number of Telegram Stars a user must pay initially + and after each subsequent subscription period to be a member of the chat; + :tg-const:`telegram.constants.ChatSubscriptionLimit.MIN_PRICE`- + :tg-const:`telegram.constants.ChatSubscriptionLimit.MAX_PRICE`. + name (:obj:`str`, optional): Invite link name; + 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + + Returns: + :class:`telegram.ChatInviteLink` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "subscription_period": subscription_period, + "subscription_price": subscription_price, + "name": name, + } + + result = await self._post( + "createChatSubscriptionInviteLink", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return ChatInviteLink.de_json(result, self) # type: ignore[return-value] + + async def edit_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + invite_link: Union[str, "ChatInviteLink"], + name: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> ChatInviteLink: + """ + Use this method to edit a subscription invite link created by the bot. The bot must have + :attr:`telegram.ChatPermissions.can_invite_users` administrator right. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + invite_link (:obj:`str` | :obj:`telegram.ChatInviteLink`): The invite link to edit. + name (:obj:`str`, optional): Invite link name; + 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + + Tip: + Omitting this argument removes the name of the invite link. + + Returns: + :class:`telegram.ChatInviteLink` + + Raises: + :class:`telegram.error.TelegramError` + + """ + link = invite_link.invite_link if isinstance(invite_link, ChatInviteLink) else invite_link + data: JSONDict = { + "chat_id": chat_id, + "invite_link": link, + "name": name, + } + + result = await self._post( + "editChatSubscriptionInviteLink", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return ChatInviteLink.de_json(result, self) # type: ignore[return-value] + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -9532,3 +9654,7 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`get_star_transactions`""" sendPaidMedia = send_paid_media """Alias for :meth:`send_paid_media`""" + createChatSubscriptionInviteLink = create_chat_subscription_invite_link + """Alias for :meth:`create_chat_subscription_invite_link`""" + editChatSubscriptionInviteLink = edit_chat_subscription_invite_link + """Alias for :meth:`edit_chat_subscription_invite_link`""" diff --git a/telegram/_chat.py b/telegram/_chat.py index 02d80c94714..a73a504d8c5 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -2666,6 +2666,81 @@ async def revoke_invite_link( api_kwargs=api_kwargs, ) + async def create_subscription_invite_link( + self, + subscription_period: int, + subscription_price: int, + name: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "ChatInviteLink": + """Shortcut for:: + + await bot.create_chat_subscription_invite_link( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.create_chat_subscription_invite_link`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.ChatInviteLink` + """ + return await self.get_bot().create_chat_subscription_invite_link( + chat_id=self.id, + subscription_period=subscription_period, + subscription_price=subscription_price, + name=name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_subscription_invite_link( + self, + invite_link: Union[str, "ChatInviteLink"], + name: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "ChatInviteLink": + """Shortcut for:: + + await bot.edit_chat_subscription_invite_link( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_chat_subscription_invite_link`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return await self.get_bot().edit_chat_subscription_invite_link( + chat_id=self.id, + invite_link=invite_link, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + name=name, + ) + async def approve_join_request( self, user_id: int, @@ -3274,6 +3349,7 @@ async def send_paid_media( protect_content: ODVInput[bool] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3314,6 +3390,7 @@ async def send_paid_media( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) diff --git a/telegram/_chatinvitelink.py b/telegram/_chatinvitelink.py index 43e7e8ab62d..1e1a0e1cf44 100644 --- a/telegram/_chatinvitelink.py +++ b/telegram/_chatinvitelink.py @@ -69,6 +69,16 @@ class ChatInviteLink(TelegramObject): created using this link. .. versionadded:: 13.8 + subscription_period (:obj:`int`, optional): The number of seconds the subscription will be + active for before the next payment. + + .. versionadded:: NEXT.VERSION + subscription_price (:obj:`int`, optional): The amount of Telegram Stars a user must pay + initially and after each subsequent subscription period to be a member of the chat + using the link. + + .. versionadded:: NEXT.VERSION + Attributes: invite_link (:obj:`str`): The invite link. If the link was created by another chat administrator, then the second part of the link will be replaced with ``'…'``. @@ -96,6 +106,15 @@ class ChatInviteLink(TelegramObject): created using this link. .. versionadded:: 13.8 + subscription_period (:obj:`int`): Optional. The number of seconds the subscription will be + active for before the next payment. + + .. versionadded:: NEXT.VERSION + subscription_price (:obj:`int`): Optional. The amount of Telegram Stars a user must pay + initially and after each subsequent subscription period to be a member of the chat + using the link. + + .. versionadded:: NEXT.VERSION """ @@ -109,6 +128,8 @@ class ChatInviteLink(TelegramObject): "member_limit", "name", "pending_join_request_count", + "subscription_period", + "subscription_price", ) def __init__( @@ -122,6 +143,8 @@ def __init__( member_limit: Optional[int] = None, name: Optional[str] = None, pending_join_request_count: Optional[int] = None, + subscription_period: Optional[int] = None, + subscription_price: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -140,6 +163,9 @@ def __init__( self.pending_join_request_count: Optional[int] = ( int(pending_join_request_count) if pending_join_request_count is not None else None ) + self.subscription_period: Optional[int] = subscription_period + self.subscription_price: Optional[int] = subscription_price + self._id_attrs = ( self.invite_link, self.creates_join_request, diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index 0cc06bf5804..1eabaa14e56 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChatMember.""" + import datetime from typing import TYPE_CHECKING, Dict, Final, Optional, Type @@ -391,24 +392,34 @@ class ChatMemberMember(ChatMember): Args: user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`, optional): Date when the user's subscription will + expire. + + .. versionadded:: NEXT.VERSION Attributes: status (:obj:`str`): The member's status in the chat, always :tg-const:`telegram.ChatMember.MEMBER`. user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`): Optional. Date when the user's subscription will + expire. + + .. versionadded:: NEXT.VERSION """ - __slots__ = () + __slots__ = ("until_date",) def __init__( self, user: User, + until_date: Optional[datetime.datetime] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(status=ChatMember.MEMBER, user=user, api_kwargs=api_kwargs) - self._freeze() + with self._unfrozen(): + self.until_date: Optional[datetime.datetime] = until_date class ChatMemberRestricted(ChatMember): diff --git a/telegram/_message.py b/telegram/_message.py index 7d077a4d9a7..5e416ab7a50 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -280,15 +280,14 @@ class Message(MaybeInaccessibleMessage): Args: message_id (:obj:`int`): Unique message identifier inside this chat. - from_user (:class:`telegram.User`, optional): Sender of the message; empty for messages - sent to channels. For backward compatibility, this will contain a fake sender user in - non-channel chats, if the message was sent on behalf of a chat. - sender_chat (:class:`telegram.Chat`, optional): Sender of the message, sent on behalf of a - chat. For example, the channel itself for channel posts, the supergroup itself for - messages from anonymous group administrators, the linked channel for messages - automatically forwarded to the discussion group. For backward compatibility, - :attr:`from_user` contains a fake sender user in non-channel chats, if the message was - sent on behalf of a chat. + from_user (:class:`telegram.User`, optional): Sender of the message; may be empty for + messages sent to channels. For backward compatibility, if the message was sent on + behalf of a chat, the field contains a fake sender user in non-channel chats. + sender_chat (:class:`telegram.Chat`, optional): Sender of the message when sent on behalf + of a chat. For example, the supergroup itself for messages sent by its anonymous + administrators or a linked channel for messages automatically forwarded to the + channel's discussion group. For backward compatibility, if the message was sent on + behalf of a chat, the field from contains a fake sender user in non-channel chats. date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to :class:`datetime.datetime`. @@ -591,15 +590,14 @@ class Message(MaybeInaccessibleMessage): Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. - from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages - sent to channels. For backward compatibility, this will contain a fake sender user in - non-channel chats, if the message was sent on behalf of a chat. - sender_chat (:class:`telegram.Chat`): Optional. Sender of the message, sent on behalf of a - chat. For example, the channel itself for channel posts, the supergroup itself for - messages from anonymous group administrators, the linked channel for messages - automatically forwarded to the discussion group. For backward compatibility, - :attr:`from_user` contains a fake sender user in non-channel chats, if the message was - sent on behalf of a chat. + from_user (:class:`telegram.User`): Optional. Sender of the message; may be empty for + messages sent to channels. For backward compatibility, if the message was sent on + behalf of a chat, the field contains a fake sender user in non-channel chats. + sender_chat (:class:`telegram.Chat`): Optional. Sender of the message when sent on behalf + of a chat. For example, the supergroup itself for messages sent by its anonymous + administrators or a linked channel for messages automatically forwarded to the + channel's discussion group. For backward compatibility, if the message was sent on + behalf of a chat, the field from contains a fake sender user in non-channel chats. date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to :class:`datetime.datetime`. diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index b176f2315fe..0baebd39d7f 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type from telegram import constants +from telegram._paidmedia import PaidMedia from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum @@ -310,20 +311,33 @@ class TransactionPartnerUser(TransactionPartner): Args: user (:class:`telegram.User`): Information about the user. invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. + paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid + media bought by the user. + + .. versionadded:: NEXT.VERSION Attributes: type (:obj:`str`): The type of the transaction partner, always :tg-const:`telegram.TransactionPartner.USER`. user (:class:`telegram.User`): Information about the user. invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. + paid_media (Tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid + media bought by the user. + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("invoice_payload", "user") + __slots__ = ( + "invoice_payload", + "paid_media", + "user", + ) def __init__( self, user: "User", invoice_payload: Optional[str] = None, + paid_media: Optional[Sequence[PaidMedia]] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: @@ -332,6 +346,7 @@ def __init__( with self._unfrozen(): self.user: User = user self.invoice_payload: Optional[str] = invoice_payload + self.paid_media: Optional[Tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media) self._id_attrs = ( self.type, self.user, @@ -347,6 +362,7 @@ def de_json( return None data["user"] = User.de_json(data.get("user"), bot) + data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot) return super().de_json(data=data, bot=bot) # type: ignore[return-value] diff --git a/telegram/_reaction.py b/telegram/_reaction.py index d1ba718f0d6..55e8968e8eb 100644 --- a/telegram/_reaction.py +++ b/telegram/_reaction.py @@ -17,7 +17,8 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects that represents a Telegram ReactionType.""" -from typing import TYPE_CHECKING, Final, Literal, Optional, Union + +from typing import TYPE_CHECKING, Dict, Final, Literal, Optional, Type, Union from telegram import constants from telegram._telegramobject import TelegramObject @@ -30,16 +31,22 @@ class ReactionType(TelegramObject): """Base class for Telegram ReactionType Objects. - There exist :class:`telegram.ReactionTypeEmoji` and :class:`telegram.ReactionTypeCustomEmoji`. + There exist :class:`telegram.ReactionTypeEmoji`, :class:`telegram.ReactionTypeCustomEmoji` + and :class:`telegram.ReactionTypePaid`. .. versionadded:: 20.8 + .. versionchanged:: NEXT.VERSION + + Added paid reaction. Args: type (:obj:`str`): Type of the reaction. Can be - :attr:`~telegram.ReactionType.EMOJI` or :attr:`~telegram.ReactionType.CUSTOM_EMOJI`. + :attr:`~telegram.ReactionType.EMOJI`, :attr:`~telegram.ReactionType.CUSTOM_EMOJI` or + :attr:`~telegram.ReactionType.PAID`. Attributes: type (:obj:`str`): Type of the reaction. Can be - :attr:`~telegram.ReactionType.EMOJI` or :attr:`~telegram.ReactionType.CUSTOM_EMOJI`. + :attr:`~telegram.ReactionType.EMOJI`, :attr:`~telegram.ReactionType.CUSTOM_EMOJI` or + :attr:`~telegram.ReactionType.PAID`. """ @@ -49,11 +56,16 @@ class ReactionType(TelegramObject): """:const:`telegram.constants.ReactionType.EMOJI`""" CUSTOM_EMOJI: Final[constants.ReactionType] = constants.ReactionType.CUSTOM_EMOJI """:const:`telegram.constants.ReactionType.CUSTOM_EMOJI`""" + PAID: Final[constants.ReactionType] = constants.ReactionType.PAID + """:const:`telegram.constants.ReactionType.PAID` + + .. versionadded:: NEXT.VERSION + """ def __init__( self, type: Union[ # pylint: disable=redefined-builtin - Literal["emoji", "custom_emoji"], constants.ReactionType + Literal["emoji", "custom_emoji", "paid"], constants.ReactionType ], *, api_kwargs: Optional[JSONDict] = None, @@ -71,14 +83,20 @@ def de_json( """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) - if not data: + if data is None: return None - if cls is ReactionType and data.get("type") in [cls.EMOJI, cls.CUSTOM_EMOJI]: - reaction_type = data.pop("type") - if reaction_type == cls.EMOJI: - return ReactionTypeEmoji.de_json(data=data, bot=bot) - return ReactionTypeCustomEmoji.de_json(data=data, bot=bot) + if not data and cls is ReactionType: + return None + + _class_mapping: Dict[str, Type[ReactionType]] = { + cls.EMOJI: ReactionTypeEmoji, + cls.CUSTOM_EMOJI: ReactionTypeCustomEmoji, + cls.PAID: ReactionTypePaid, + } + + if cls is ReactionType and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data, bot) return super().de_json(data=data, bot=bot) @@ -152,6 +170,24 @@ def __init__( self._id_attrs = (self.custom_emoji_id,) +class ReactionTypePaid(ReactionType): + """ + The reaction is paid. + + .. versionadded:: NEXT.VERSION + + Attributes: + type (:obj:`str`): Type of the reaction, + always :tg-const:`telegram.ReactionType.PAID`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=ReactionType.PAID, api_kwargs=api_kwargs) + self._freeze() + + class ReactionCount(TelegramObject): """This class represents a reaction added to a message along with the number of times it was added. diff --git a/telegram/constants.py b/telegram/constants.py index 1aba1f2a93a..2867c6c7c4f 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -54,6 +54,7 @@ "ChatLimit", "ChatMemberStatus", "ChatPhotoSize", + "ChatSubscriptionLimit", "ChatType", "ContactLimit", "CustomEmojiStickerLimit", @@ -151,7 +152,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=8) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=9) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -2903,6 +2904,11 @@ class ReactionType(StringEnum): """:obj:`str`: A :class:`telegram.ReactionType` with a normal emoji.""" CUSTOM_EMOJI = "custom_emoji" """:obj:`str`: A :class:`telegram.ReactionType` with a custom emoji.""" + PAID = "paid" + """:obj:`str`: A :class:`telegram.ReactionType` with a paid reaction. + + .. versionadded:: NEXT.VERSION + """ class ReactionEmoji(StringEnum): @@ -3096,3 +3102,22 @@ class BackgroundFillType(StringEnum): """:obj:`str`: A :class:`telegram.BackgroundFill` with gradient fill.""" FREEFORM_GRADIENT = "freeform_gradient" """:obj:`str`: A :class:`telegram.BackgroundFill` with freeform_gradient fill.""" + + +class ChatSubscriptionLimit(IntEnum): + """This enum contains limitations for + :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_period` and + :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_price`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + SUBSCRIPTION_PERIOD = 2592000 + """:obj:`int`: The number of seconds the subscription will be active.""" + MIN_PRICE = 1 + """:obj:`int`: Amount of stars a user pays, minimum amount the subscription can be set to.""" + MAX_PRICE = 2500 + """:obj:`int`: Amount of stars a user pays, maximum amount the subscription can be set to.""" diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index d85d8822de4..76b17bad02a 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4234,6 +4234,7 @@ async def send_paid_media( protect_content: ODVInput[bool] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -4263,6 +4264,57 @@ async def send_paid_media( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + business_connection_id=business_connection_id, + ) + + async def create_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + subscription_period: int, + subscription_price: int, + name: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> ChatInviteLink: + return await super().create_chat_subscription_invite_link( + chat_id=chat_id, + subscription_period=subscription_period, + subscription_price=subscription_price, + name=name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def edit_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + invite_link: Union[str, "ChatInviteLink"], + name: Optional[str] = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> ChatInviteLink: + return await super().edit_chat_subscription_invite_link( + chat_id=chat_id, + invite_link=invite_link, + name=name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) # updated camelCase aliases @@ -4388,4 +4440,6 @@ async def send_paid_media( replaceStickerInSet = replace_sticker_in_set refundStarPayment = refund_star_payment getStarTransactions = get_star_transactions + createChatSubscriptionInviteLink = create_chat_subscription_invite_link + editChatSubscriptionInviteLink = edit_chat_subscription_invite_link sendPaidMedia = send_paid_media diff --git a/tests/auxil/ci_bots.py b/tests/auxil/ci_bots.py index bdb25a2f0ee..069a65ccec9 100644 --- a/tests/auxil/ci_bots.py +++ b/tests/auxil/ci_bots.py @@ -29,15 +29,16 @@ # purposes than testing. FALLBACKS = ( "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc" - "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2 lkIjogIjY3NTY2N" - "jIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTgzOD" - "AwNDU3NyIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIi wgIm5hbWUiOiAiUFRCIHRlc3RzIG" - "ZhbGxiYWNrIDEiLCAidXNlcm5hbWUiOiAiQHB0Yl9mYWxsYmFja18xX2JvdCJ9LCB7InRva2VuIjogIjU1ODE5NDA2Njp" - "BQUZ3RFBJRmx6R1VsQ2FXSHRUT0VYNFJGclg4dTlETXFmbyIsIC JwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY" - "4NTA2MzpURVNUOllqRXdPRFF3TVRGbU5EY3kiLCAiY2hhdF9pZCI6ICI2NzU2NjYyMjQiLCAic3VwZXJfZ3JvdXBfaWQi" - "OiAiLTEwMDEyMjEyMTY4MzAiLCAiZm9ydW1fZ3 JvdXBfaWQiOiAiLTEwMDE4NTc4NDgzMTQiLCAiY2hhbm5lbF9pZCI6" - "ICJAcHl0aG9udGVsZWdyYW1ib3R0ZXN0cyIsICJuYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgInVzZXJuYW1lI" - "jogIkBwdGJfZmFsbGJhY2tfMl9ib3QifV0=" + "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj" + "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTgzODA" + "wNDU3NyIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgIm5hbWUiOiAiUFRCIHRlc3RzIGZh" + "bGxiYWNrIDEiLCAidXNlcm5hbWUiOiAiQHB0Yl9mYWxsYmFja18xX2JvdCIsICJzdWJzY3JpcHRpb25fY2hhbm5lbF9pZ" + "CI6IC0xMDAyMjI5NjQ5MzAzfSwgeyJ0b2tlbiI6ICI1NTgxOTQwNjY6QUFGd0RQSUZsekdVbENhV0h0VE9FWDRSRnJYOH" + "U5RE1xZm8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZakV3T0RRd01URm1ORGN5Iiw" + "gImNoYXRfaWQiOiAiNjc1NjY2MjI0IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjIxMjE2ODMwIiwgImZvcnVtX2dy" + "b3VwX2lkIjogIi0xMDAxODU3ODQ4MzE0IiwgImNoYW5uZWxfaWQiOiAiQHB5dGhvbnRlbGVncmFtYm90dGVzdHMiLCAib" + "mFtZSI6ICJQVEIgdGVzdHMgZmFsbGJhY2sgMiIsICJ1c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90IiwgInN1Yn" + "NjcmlwdGlvbl9jaGFubmVsX2lkIjogLTEwMDIyMjk2NDkzMDN9XQ==" ) GITHUB_ACTION = os.getenv("GITHUB_ACTION", None) diff --git a/tests/conftest.py b/tests/conftest.py index dd553f9fe82..c721605bdb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -206,6 +206,11 @@ def provider_token(bot_info): return bot_info["payment_provider_token"] +@pytest.fixture(scope="session") +def subscription_channel_id(bot_info): + return bot_info["subscription_channel_id"] + + @pytest.fixture async def app(bot_info): # We build a new bot each time so that we use `app` in a context manager without problems diff --git a/tests/test_bot.py b/tests/test_bot.py index 6615927adb7..705b14c6d4d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2265,6 +2265,21 @@ async def do_request(url, request_data: RequestData, *args, **kwargs): obj = await bot.get_star_transactions(offset=3) assert isinstance(obj, StarTransactions) + async def test_create_chat_subscription_invite_link( + self, + monkeypatch, + bot, + ): + # Since the chat invite link object does not say if the sub args are passed we can + # only check here + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("subscription_period") == 2592000 + assert request_data.parameters.get("subscription_price") == 6 + + monkeypatch.setattr(bot.request, "post", make_assertion) + + await bot.create_chat_subscription_invite_link(1234, 2592000, 6) + class TestBotWithRequest: """ @@ -4261,3 +4276,23 @@ async def test_get_star_transactions(self, bot): transactions = await bot.get_star_transactions(limit=1) assert isinstance(transactions, StarTransactions) assert len(transactions.transactions) == 0 + + async def test_create_edit_chat_subscription_link( + self, bot, subscription_channel_id, channel_id + ): + sub_link = await bot.create_chat_subscription_invite_link( + subscription_channel_id, + name="sub_name", + subscription_period=2592000, + subscription_price=13, + ) + assert sub_link.name == "sub_name" + assert sub_link.subscription_period == 2592000 + assert sub_link.subscription_price == 13 + + edited_link = await bot.edit_chat_subscription_invite_link( + chat_id=subscription_channel_id, invite_link=sub_link, name="sub_name_2" + ) + assert edited_link.name == "sub_name_2" + assert sub_link.subscription_period == 2592000 + assert sub_link.subscription_price == 13 diff --git a/tests/test_chat.py b/tests/test_chat.py index 682bdbe514a..28905934c3c 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -889,6 +889,54 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "revoke_chat_invite_link", make_assertion) assert await chat.revoke_invite_link(invite_link=link) + async def test_create_subscription_invite_link(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["subscription_price"] == 42 + and kwargs["subscription_period"] == 42 + ) + + assert check_shortcut_signature( + Chat.create_subscription_invite_link, + Bot.create_chat_subscription_invite_link, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.create_subscription_invite_link, + chat.get_bot(), + "create_chat_subscription_invite_link", + ) + assert await check_defaults_handling(chat.create_subscription_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "create_chat_subscription_invite_link", make_assertion) + assert await chat.create_subscription_invite_link( + subscription_price=42, subscription_period=42 + ) + + async def test_edit_subscription_invite_link(self, monkeypatch, chat): + link = "ThisIsALink" + + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["invite_link"] == link + + assert check_shortcut_signature( + Chat.edit_subscription_invite_link, + Bot.edit_chat_subscription_invite_link, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.edit_subscription_invite_link, + chat.get_bot(), + "edit_chat_subscription_invite_link", + ) + assert await check_defaults_handling(chat.edit_subscription_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_chat_subscription_invite_link", make_assertion) + assert await chat.edit_subscription_invite_link(invite_link=link) + async def test_instance_method_get_menu_button(self, monkeypatch, chat): async def make_assertion(*_, **kwargs): return kwargs["chat_id"] == chat.id diff --git a/tests/test_chatinvitelink.py b/tests/test_chatinvitelink.py index 1b95177d9d5..2a4007986b3 100644 --- a/tests/test_chatinvitelink.py +++ b/tests/test_chatinvitelink.py @@ -42,6 +42,8 @@ def invite_link(creator): member_limit=TestChatInviteLinkBase.member_limit, name=TestChatInviteLinkBase.name, pending_join_request_count=TestChatInviteLinkBase.pending_join_request_count, + subscription_period=TestChatInviteLinkBase.subscription_period, + subscription_price=TestChatInviteLinkBase.subscription_price, ) @@ -54,6 +56,8 @@ class TestChatInviteLinkBase: member_limit = 42 name = "LinkName" pending_join_request_count = 42 + subscription_period = 43 + subscription_price = 44 class TestChatInviteLinkWithoutRequest(TestChatInviteLinkBase): @@ -91,6 +95,8 @@ def test_de_json_all_args(self, bot, creator): "member_limit": self.member_limit, "name": self.name, "pending_join_request_count": str(self.pending_join_request_count), + "subscription_period": self.subscription_period, + "subscription_price": self.subscription_price, } invite_link = ChatInviteLink.de_json(json_dict, bot) @@ -106,6 +112,8 @@ def test_de_json_all_args(self, bot, creator): assert invite_link.member_limit == self.member_limit assert invite_link.name == self.name assert invite_link.pending_join_request_count == self.pending_join_request_count + assert invite_link.subscription_period == self.subscription_period + assert invite_link.subscription_price == self.subscription_price def test_de_json_localization(self, tz_bot, bot, raw_bot, creator): json_dict = { @@ -146,6 +154,8 @@ def test_to_dict(self, invite_link): assert invite_link_dict["member_limit"] == self.member_limit assert invite_link_dict["name"] == self.name assert invite_link_dict["pending_join_request_count"] == self.pending_join_request_count + assert invite_link_dict["subscription_period"] == self.subscription_period + assert invite_link_dict["subscription_price"] == self.subscription_price def test_equality(self): a = ChatInviteLink("link", User(1, "", False), True, True, True) diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index b4eac51cfb3..4296fdd2723 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -101,7 +101,7 @@ def chat_member_administrator(): def chat_member_member(): - return ChatMemberMember(CMDefaults.user) + return ChatMemberMember(CMDefaults.user, until_date=CMDefaults.until_date) def chat_member_restricted(): @@ -230,7 +230,9 @@ def test_de_json_all_args(self, bot, chat_member_type): def test_de_json_chatmemberbanned_localization(self, chat_member_type, tz_bot, bot, raw_bot): # We only test two classes because the other three don't have datetimes in them. - if isinstance(chat_member_type, (ChatMemberBanned, ChatMemberRestricted)): + if isinstance( + chat_member_type, (ChatMemberBanned, ChatMemberRestricted, ChatMemberMember) + ): json_dict = make_json_dict(chat_member_type, include_optional_args=True) chatmember_raw = ChatMember.de_json(json_dict, raw_bot) chatmember_bot = ChatMember.de_json(json_dict, bot) diff --git a/tests/test_reaction.py b/tests/test_reaction.py index 6f3d3cb4621..67ece80e3b0 100644 --- a/tests/test_reaction.py +++ b/tests/test_reaction.py @@ -28,6 +28,7 @@ ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji, + ReactionTypePaid, ) from telegram.constants import ReactionEmoji from tests.auxil.slots import mro_slots @@ -48,6 +49,10 @@ def reaction_type_emoji(): return ReactionTypeEmoji(RTDefaults.normal_emoji) +def reaction_type_paid(): + return ReactionTypePaid() + + def make_json_dict(instance: ReactionType, include_optional_args: bool = False) -> dict: """Used to make the json dict which we use for testing de_json. Similar to iter_args()""" json_dict = {"type": instance.type} @@ -99,6 +104,7 @@ def reaction_type(request): [ reaction_type_custom_emoji, reaction_type_emoji, + reaction_type_paid, ], indirect=True, ) @@ -112,6 +118,7 @@ def test_slot_behaviour(self, reaction_type): def test_de_json_required_args(self, bot, reaction_type): cls = reaction_type.__class__ assert cls.de_json(None, bot) is None + assert ReactionType.de_json({}, bot) is None json_dict = make_json_dict(reaction_type) const_reaction_type = ReactionType.de_json(json_dict, bot) @@ -155,7 +162,7 @@ def test_to_dict(self, reaction_type): assert reaction_type_dict["type"] == reaction_type.type if reaction_type.type == ReactionType.EMOJI: assert reaction_type_dict["emoji"] == reaction_type.emoji - else: + elif reaction_type.type == ReactionType.CUSTOM_EMOJI: assert reaction_type_dict["custom_emoji_id"] == reaction_type.custom_emoji_id for slot in reaction_type.__slots__: # additional verification for the optional args diff --git a/tests/test_stars.py b/tests/test_stars.py index d3560af7d2f..10ed7e63b81 100644 --- a/tests/test_stars.py +++ b/tests/test_stars.py @@ -24,6 +24,8 @@ from telegram import ( Dice, + PaidMediaPhoto, + PhotoSize, RevenueWithdrawalState, RevenueWithdrawalStateFailed, RevenueWithdrawalStatePending, @@ -62,6 +64,16 @@ def withdrawal_state_pending(): def transaction_partner_user(): return TransactionPartnerUser( user=User(id=1, is_bot=False, first_name="first_name", username="username"), + invoice_payload="payload", + paid_media=[ + PaidMediaPhoto( + photo=[ + PhotoSize( + file_id="file_id", width=1, height=1, file_unique_id="file_unique_id" + ) + ] + ) + ], ) From e0f36867cc402bd267624d82371bd9d861f2969c Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 1 Sep 2024 09:33:12 +0200 Subject: [PATCH 22/26] Add `MessageEntity.shift_entities` and `MessageEntity.concatenate` (#4376) --- telegram/_messageentity.py | 147 ++++++++++++++++++++++++++++++++++-- tests/test_messageentity.py | 48 ++++++++++++ 2 files changed, 190 insertions(+), 5 deletions(-) diff --git a/telegram/_messageentity.py b/telegram/_messageentity.py index 6e219537fae..cdf103b2d66 100644 --- a/telegram/_messageentity.py +++ b/telegram/_messageentity.py @@ -20,7 +20,7 @@ import copy import itertools -from typing import TYPE_CHECKING, Dict, Final, List, Optional, Sequence +from typing import TYPE_CHECKING, Dict, Final, List, Optional, Sequence, Tuple, Union from telegram import constants from telegram._telegramobject import TelegramObject @@ -32,6 +32,8 @@ if TYPE_CHECKING: from telegram import Bot +_SEM = Sequence["MessageEntity"] + class MessageEntity(TelegramObject): """ @@ -146,9 +148,7 @@ def de_json( return super().de_json(data=data, bot=bot) @staticmethod - def adjust_message_entities_to_utf_16( - text: str, entities: Sequence["MessageEntity"] - ) -> Sequence["MessageEntity"]: + def adjust_message_entities_to_utf_16(text: str, entities: _SEM) -> _SEM: """Utility functionality for converting the offset and length of entities from Unicode (:obj:`str`) to UTF-16 (``utf-16-le`` encoded :obj:`bytes`). @@ -206,7 +206,7 @@ def adjust_message_entities_to_utf_16( text_slice = text[last_position:position] accumulated_length += len(text_slice.encode(TextEncoding.UTF_16_LE)) // 2 position_translation[position] = accumulated_length - # get the final output entites + # get the final output entities out = [] for entity in entities: translated_positions = position_translation[entity.offset] @@ -220,6 +220,143 @@ def adjust_message_entities_to_utf_16( out.append(new_entity) return out + @staticmethod + def shift_entities(by: Union[str, int], entities: _SEM) -> _SEM: + """Utility functionality for shifting the offset of entities by a given amount. + + Examples: + Shifting by an integer amount: + + .. code-block:: python + + text = "Hello, world!" + entities = [ + MessageEntity(offset=0, length=5, type=MessageEntity.BOLD), + MessageEntity(offset=7, length=5, type=MessageEntity.ITALIC), + ] + shifted_entities = MessageEntity.shift_entities(1, entities) + await bot.send_message( + chat_id=123, + text="!" + text, + entities=shifted_entities, + ) + + Shifting using a string: + + .. code-block:: python + + text = "Hello, world!" + prefix = "𝄢" + entities = [ + MessageEntity(offset=0, length=5, type=MessageEntity.BOLD), + MessageEntity(offset=7, length=5, type=MessageEntity.ITALIC), + ] + shifted_entities = MessageEntity.shift_entities(prefix, entities) + await bot.send_message( + chat_id=123, + text=prefix + text, + entities=shifted_entities, + ) + + Tip: + The :paramref:`entities` are *not* modified in place. The function returns a sequence + of new objects. + + .. versionadded:: NEXT.VERSION + + Args: + by (:obj:`str` | :obj:`int`): Either the amount to shift the offset by or + a string whose length will be used as the amount to shift the offset by. In this + case, UTF-16 encoding will be used to calculate the length. + entities (Sequence[:class:`telegram.MessageEntity`]): Sequence of entities + + Returns: + Sequence[:class:`telegram.MessageEntity`]: Sequence of entities with the offset shifted + """ + effective_shift = by if isinstance(by, int) else len(by.encode("utf-16-le")) // 2 + + out = [] + for entity in entities: + new_entity = copy.copy(entity) + with new_entity._unfrozen(): + new_entity.offset += effective_shift + out.append(new_entity) + return out + + @classmethod + def concatenate( + cls, + *args: Union[Tuple[str, _SEM], Tuple[str, _SEM, bool]], + ) -> Tuple[str, _SEM]: + """Utility functionality for concatenating two text along with their formatting entities. + + Tip: + This function is useful for prefixing an already formatted text with a new text and its + formatting entities. In particular, it automatically correctly handles UTF-16 encoding. + + Examples: + This example shows a callback function that can be used to add a prefix and suffix to + the message in a :class:`~telegram.ext.CallbackQueryHandler`: + + .. code-block:: python + + async def prefix_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + prefix = "𠌕 bold 𝄢 italic underlined: 𝛙𝌢𑁍 | " + prefix_entities = [ + MessageEntity(offset=2, length=4, type=MessageEntity.BOLD), + MessageEntity(offset=9, length=6, type=MessageEntity.ITALIC), + MessageEntity(offset=28, length=3, type=MessageEntity.UNDERLINE), + ] + suffix = " | 𠌕 bold 𝄢 italic underlined: 𝛙𝌢𑁍" + suffix_entities = [ + MessageEntity(offset=5, length=4, type=MessageEntity.BOLD), + MessageEntity(offset=12, length=6, type=MessageEntity.ITALIC), + MessageEntity(offset=31, length=3, type=MessageEntity.UNDERLINE), + ] + + message = update.effective_message + first = (prefix, prefix_entities, True) + second = (message.text, message.entities) + third = (suffix, suffix_entities, True) + + new_text, new_entities = MessageEntity.concatenate(first, second, third) + await update.callback_query.edit_message_text( + text=new_text, + entities=new_entities, + ) + + Hint: + The entities are *not* modified in place. The function returns a + new sequence of objects. + + .. versionadded:: NEXT.VERSION + + Args: + *args (Tuple[:obj:`str`, Sequence[:class:`telegram.MessageEntity`]] | \ + Tuple[:obj:`str`, Sequence[:class:`telegram.MessageEntity`], :obj:`bool`]): + Arbitrary number of tuples containing the text and its entities to concatenate. + If the last element of the tuple is a :obj:`bool`, it is used to determine whether + to adjust the entities to UTF-16 via + :meth:`adjust_message_entities_to_utf_16`. UTF-16 adjustment is disabled by + default. + + Returns: + Tuple[:obj:`str`, Sequence[:class:`telegram.MessageEntity`]]: The concatenated text + and its entities + """ + output_text = "" + output_entities: List[MessageEntity] = [] + for arg in args: + text, entities = arg[0], arg[1] + + if len(arg) > 2 and arg[2] is True: + entities = cls.adjust_message_entities_to_utf_16(text, entities) + + output_entities.extend(cls.shift_entities(output_text, entities)) + output_text += text + + return output_text, output_entities + ALL_TYPES: Final[List[str]] = list(constants.MessageEntityType) """List[:obj:`str`]: A list of all available message entity types.""" BLOCKQUOTE: Final[str] = constants.MessageEntityType.BLOCKQUOTE diff --git a/tests/test_messageentity.py b/tests/test_messageentity.py index 8bab9fec7b9..2fc21ea493c 100644 --- a/tests/test_messageentity.py +++ b/tests/test_messageentity.py @@ -103,6 +103,54 @@ def test_fix_utf16(self): assert out_entity.offset == offset assert out_entity.length == length + @pytest.mark.parametrize("by", [6, "prefix", "𝛙𝌢𑁍"]) + def test_shift_entities(self, by): + kwargs = { + "url": "url", + "user": 42, + "language": "python", + "custom_emoji_id": "custom_emoji_id", + } + entities = [ + MessageEntity(MessageEntity.BOLD, 2, 3, **kwargs), + MessageEntity(MessageEntity.BOLD, 5, 6, **kwargs), + ] + shifted = MessageEntity.shift_entities(by, entities) + assert shifted[0].offset == 8 + assert shifted[1].offset == 11 + + assert shifted[0] is not entities[0] + assert shifted[1] is not entities[1] + + for entity in shifted: + for key, value in kwargs.items(): + assert getattr(entity, key) == value + + def test_concatenate(self): + kwargs = { + "url": "url", + "user": 42, + "language": "python", + "custom_emoji_id": "custom_emoji_id", + } + first_entity = MessageEntity(MessageEntity.BOLD, 0, 6, **kwargs) + second_entity = MessageEntity(MessageEntity.ITALIC, 0, 4, **kwargs) + third_entity = MessageEntity(MessageEntity.UNDERLINE, 3, 6, **kwargs) + + first = ("prefix 𝛙𝌢𑁍 | ", [first_entity], True) + second = ("text 𝛙𝌢𑁍", [second_entity], False) + third = (" | suffix 𝛙𝌢𑁍", [third_entity]) + + new_text, new_entities = MessageEntity.concatenate(first, second, third) + + assert new_text == "prefix 𝛙𝌢𑁍 | text 𝛙𝌢𑁍 | suffix 𝛙𝌢𑁍" + assert [entity.offset for entity in new_entities] == [0, 16, 30] + for old, new in zip([first_entity, second_entity, third_entity], new_entities): + assert new is not old + assert new.type == old.type + for key, value in kwargs.items(): + assert getattr(new, key) == value + def test_equality(self): a = MessageEntity(MessageEntity.BOLD, 2, 3) b = MessageEntity(MessageEntity.BOLD, 2, 3) From b17b0d248dab6512ae1d39571f3a9c6cdc274631 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 1 Sep 2024 09:34:20 +0200 Subject: [PATCH 23/26] Improve PyPI Automation (#4375) --- .github/workflows/release_pypi.yml | 107 +++---------------- .github/workflows/release_test_pypi.yml | 132 ++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 91 deletions(-) create mode 100644 .github/workflows/release_test_pypi.yml diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml index bcd1794c468..8ebfd48887e 100644 --- a/.github/workflows/release_pypi.yml +++ b/.github/workflows/release_pypi.yml @@ -1,17 +1,15 @@ name: Publish to PyPI on: - # Run on any tag - push: - tags: - - '**' - # manually trigger the workflow - for testing only + # manually trigger the workflow workflow_dispatch: jobs: build: name: Build Distribution runs-on: ubuntu-latest + outputs: + TAG: ${{ steps.get_tag.outputs.TAG }} steps: - uses: actions/checkout@v4 @@ -29,11 +27,15 @@ jobs: with: name: python-package-distributions path: dist/ + - name: Get Tag Name + id: get_tag + run: | + pip install . + TAG=$(python -c "from telegram import __version__; print(f'v{__version__}')") + echo "TAG=$TAG" >> $GITHUB_OUTPUT publish-to-pypi: name: Publish to PyPI - # only publish to PyPI on tag pushes - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') needs: - build runs-on: ubuntu-latest @@ -52,42 +54,11 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - publish-to-test-pypi: - name: Publish to Test PyPI - needs: - - build - runs-on: ubuntu-latest - environment: - name: release_test_pypi - url: https://test.pypi.org/p/python-telegram-bot - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Publish to Test PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ - compute-signatures: name: Compute SHA1 Sums and Sign with Sigstore runs-on: ubuntu-latest needs: - publish-to-pypi - - publish-to-test-pypi - # run if either of the publishing jobs ran successfully - # see also: - # https://github.com/actions/runner/issues/491#issuecomment-850884422 - if: | - always() && ( - (needs.publish-to-pypi.result == 'success') || - (needs.publish-to-test-pypi.result == 'success') - ) permissions: id-token: write # IMPORTANT: mandatory for sigstore @@ -106,7 +77,7 @@ jobs: sha1sum $file > $file.sha1 done - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v2.1.1 + uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: >- ./dist/*.tar.gz @@ -120,56 +91,9 @@ jobs: github-release: name: Upload to GitHub Release needs: - - publish-to-pypi + - build - compute-signatures - if: | - always() && ( - (needs.publish-to-pypi.result == 'success') && - (needs.compute-signatures.result == 'success') - ) - - runs-on: ubuntu-latest - - permissions: - contents: write # IMPORTANT: mandatory for making GitHub Releases - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions-and-signatures - path: dist/ - - name: Create GitHub Release - env: - GITHUB_TOKEN: ${{ github.token }} - # Create a GitHub Release for this tag. The description can be changed later, as for now - # we don't define it through this workflow. - run: >- - gh release create - '${{ github.ref_name }}' - --repo '${{ github.repository }}' - --generate-notes - - name: Upload artifact signatures to GitHub Release - env: - GITHUB_TOKEN: ${{ github.token }} - # Upload to GitHub Release using the `gh` CLI. - # `dist/` contains the built packages, and the - # sigstore-produced signatures and certificates. - run: >- - gh release upload - '${{ github.ref_name }}' dist/** - --repo '${{ github.repository }}' - github-test-release: - name: Upload to GitHub Release Draft - needs: - - publish-to-test-pypi - - compute-signatures - if: | - always() && ( - (needs.publish-to-test-pypi.result == 'success') && - (needs.compute-signatures.result == 'success') - ) runs-on: ubuntu-latest permissions: @@ -184,21 +108,22 @@ jobs: - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} - # Create a GitHub Release *draft*. The description can be changed later, as for now + TAG: ${{ needs.build.outputs.TAG }} + # Create a tag and a GitHub Release. The description can be changed later, as for now # we don't define it through this workflow. run: >- gh release create - '${{ github.ref_name }}' + '${{ env.TAG }}' --repo '${{ github.repository }}' --generate-notes - --draft - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} + TAG: ${{ needs.build.outputs.TAG }} # Upload to GitHub Release using the `gh` CLI. # `dist/` contains the built packages, and the # sigstore-produced signatures and certificates. run: >- gh release upload - '${{ github.ref_name }}' dist/** + '${{ env.TAG }}' dist/** --repo '${{ github.repository }}' diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml new file mode 100644 index 00000000000..6009a98d7e0 --- /dev/null +++ b/.github/workflows/release_test_pypi.yml @@ -0,0 +1,132 @@ +name: Publish to Test PyPI + +on: + # manually trigger the workflow + workflow_dispatch: + +jobs: + build: + name: Build Distribution + runs-on: ubuntu-latest + outputs: + TAG: ${{ steps.get_tag.outputs.TAG }} + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install pypa/build + run: >- + python3 -m pip install build --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Get Tag Name + id: get_tag + run: | + pip install . + TAG=$(python -c "from telegram import __version__; print(f'v{__version__}')") + echo "TAG=$TAG" >> $GITHUB_OUTPUT + + publish-to-test-pypi: + name: Publish to Test PyPI + needs: + - build + runs-on: ubuntu-latest + environment: + name: release_test_pypi + url: https://test.pypi.org/p/python-telegram-bot + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + compute-signatures: + name: Compute SHA1 Sums and Sign with Sigstore + runs-on: ubuntu-latest + needs: + - publish-to-test-pypi + + permissions: + id-token: write # IMPORTANT: mandatory for sigstore + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Compute SHA1 Sums + run: | + # Compute SHA1 sum of the distribution packages and save it to a file with the same name, + # but with .sha1 extension + for file in dist/*; do + sha1sum $file > $file.sha1 + done + - name: Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v3.0.0 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Store the distribution packages and signatures + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions-and-signatures + path: dist/ + + github-test-release: + name: Upload to GitHub Release Draft + needs: + - build + - compute-signatures + + runs-on: ubuntu-latest + + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions-and-signatures + path: dist/ + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + TAG: ${{ needs.build.outputs.TAG }} + # Create a GitHub Release *draft*. The description can be changed later, as for now + # we don't define it through this workflow. + run: >- + gh release create + '${{ env.TAG }}' + --repo '${{ github.repository }}' + --generate-notes + --draft + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + TAG: ${{ needs.build.outputs.TAG }} + # Upload to GitHub Release using the `gh` CLI. + # `dist/` contains the built packages, and the + # sigstore-produced signatures and certificates. + run: >- + gh release upload + '${{ env.TAG }}' dist/** + --repo '${{ github.repository }}' From 60b439ff42767b583eda4689105e48ce9427b182 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 10:35:11 +0200 Subject: [PATCH 24/26] Update `cachetools` requirement from <5.5.0,>=5.3.3 to >=5.3.3,<5.6.0 (#4437) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> --- README.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f41e1288f7a..d96b4d47ace 100644 --- a/README.rst +++ b/README.rst @@ -157,7 +157,7 @@ PTB can be installed with optional dependencies: * ``pip install "python-telegram-bot[http2]"`` installs `httpx[http2] `_. Use this, if you want to use HTTP/2. * ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1.0 `_. Use this, if you want to use ``telegram.ext.AIORateLimiter``. * ``pip install "python-telegram-bot[webhooks]"`` installs the `tornado~=6.4 `_ library. Use this, if you want to use ``telegram.ext.Updater.start_webhook``/``telegram.ext.Application.run_webhook``. -* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools>=5.3.3,<5.5.0 `_ library. Use this, if you want to use `arbitrary callback_data `_. +* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools>=5.3.3,<5.6.0 `_ library. Use this, if you want to use `arbitrary callback_data `_. * ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 `_ library and enforces `pytz>=2018.6 `_, where ``pytz`` is a dependency of ``APScheduler``. Use this, if you want to use the ``telegram.ext.JobQueue``. To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot[socks,webhooks]"``. diff --git a/pyproject.toml b/pyproject.toml index 9285497dfad..80edfde44f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ all = [ ] callback-data = [ # Cachetools doesn't have a strict stability policy. Let's be cautious for now. - "cachetools>=5.3.3,<5.5.0", + "cachetools>=5.3.3,<5.6.0", ] ext = [ "python-telegram-bot[callback-data,job-queue,rate-limiter,webhooks]", From 970d2ab085a062f46cc1bd1aab70771eb4345ba5 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 1 Sep 2024 14:12:41 +0200 Subject: [PATCH 25/26] Documentation Improvements (#4400, #4448) Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> Co-authored-by: Palaptin <100526200+Palaptin@users.noreply.github.com> --- .github/workflows/docs-linkcheck.yml | 2 +- README.rst | 2 +- docs/source/telegram.games-tree.rst | 2 +- docs/substitutions/global.rst | 4 ++- telegram/_bot.py | 33 ++++++----------- telegram/_chatbackground.py | 20 +++++------ telegram/_chatfullinfo.py | 12 +++---- telegram/_files/inputsticker.py | 4 +-- telegram/_giveaway.py | 6 ++-- telegram/_inline/inlinekeyboardbutton.py | 36 +++++++++---------- telegram/_inline/inlinequeryresultgif.py | 16 +++------ telegram/_inline/inlinequeryresultmpeg4gif.py | 15 +++----- telegram/_inline/inlinequeryresultphoto.py | 16 +++------ telegram/_inline/inlinequeryresultvideo.py | 21 +++-------- .../_inline/inputinvoicemessagecontent.py | 4 +-- telegram/_message.py | 20 +++++------ telegram/_passport/credentials.py | 2 +- .../_passport/encryptedpassportelement.py | 2 +- telegram/_passport/passportfile.py | 2 +- telegram/_payment/stars.py | 20 +++++++++-- telegram/_reply.py | 18 +++++----- telegram/_telegramobject.py | 2 +- telegram/ext/filters.py | 2 +- 23 files changed, 118 insertions(+), 143 deletions(-) diff --git a/.github/workflows/docs-linkcheck.yml b/.github/workflows/docs-linkcheck.yml index 82fafe3e53f..f34fcc17d22 100644 --- a/.github/workflows/docs-linkcheck.yml +++ b/.github/workflows/docs-linkcheck.yml @@ -10,7 +10,7 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - python-version: [3.9] + python-version: [3.10] os: [ubuntu-latest] fail-fast: False steps: diff --git a/README.rst b/README.rst index d96b4d47ace..605942c0691 100644 --- a/README.rst +++ b/README.rst @@ -119,7 +119,7 @@ Verifying Releases To enable you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team, we have taken the following measures. -Starting with v21.4, all releases are signed via `sigstore `_. +Starting with v21.4, all releases are signed via `sigstore `_. The corresponding signature files are uploaded to the `GitHub releases page`_. To verify the signature, please install the `sigstore Python client `_ and follow the instructions for `verifying signatures from GitHub Actions `_. As input for the ``--repository`` parameter, please use the value ``python-telegram-bot/python-telegram-bot``. diff --git a/docs/source/telegram.games-tree.rst b/docs/source/telegram.games-tree.rst index 010c3c6d1fe..97b961a9e85 100644 --- a/docs/source/telegram.games-tree.rst +++ b/docs/source/telegram.games-tree.rst @@ -10,7 +10,7 @@ Your bot can offer users **HTML5 games** to play solo or to compete against each * If you send the game message without any buttons, it will automatically have a 'Play ``GameName``' button. When this button is pressed, your bot gets a :class:`telegram.CallbackQuery` with the ``game_short_name`` of the requested game. You provide the correct URL for this particular user and the app opens the game in the in-app browser. * You can manually add multiple buttons to your game message. Please note that the first button in the first row **must always** launch the game, using the field ``callback_game`` in :class:`telegram.InlineKeyboardButton`. You can add extra buttons according to taste: e.g., for a description of the rules, or to open the game's official community. * To make your game more attractive, you can upload a GIF animation that demonstrates the game to the users via `BotFather `_ (see `Lumberjack `_ for example). -* A game message will also display high scores for the current chat. Use :meth:`~telegram.Bot.setGameScore` to post high scores to the chat with the game, optionally add the :paramref:`~telegram.Bot.set_game_score.disable_edit_message` parameter if you don't want to automatically update the message with the current scoreboard. +* A game message will also display high scores for the current chat. Use :meth:`~telegram.Bot.setGameScore` to post high scores to the chat with the game, add the :paramref:`~telegram.Bot.set_game_score.disable_edit_message` parameter to disable automatic update of the message with the current scoreboard. * Use :meth:`~telegram.Bot.getGameHighScores` to get data for in-game high score tables. * You can also add an extra sharing button for users to share their best score to different chats. * For examples of what can be done using this new stuff, check the `@gamebot `_ and `@gamee `_ bots. diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index c4e9e493bb3..88a604cd139 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -60,10 +60,12 @@ .. |removed_thumb_note| replace:: Removed the deprecated argument and attribute ``thumb``. -.. |removed_thumb_url_note| replace:: Removed the deprecated argument and attribute ``thumb_url``. +.. |removed_thumb_url_note| replace:: Removed the deprecated argument and attribute ``thumb_url`` which made thumbnail_url mandatory. .. |removed_thumb_wildcard_note| replace:: Removed the deprecated arguments and attributes ``thumb_*``. +.. |thumbnail_url_mandatory| replace:: Removal of the deprecated argument ``thumb_url`` made ``thumbnail_url`` mandatory. + .. |async_context_manager| replace:: Asynchronous context manager which .. |reply_parameters| replace:: Description of the message to reply to. diff --git a/telegram/_bot.py b/telegram/_bot.py index 7172344cb4b..1e21206f41e 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -4428,18 +4428,6 @@ async def set_webhook( If you're having any trouble setting up webhooks, please check out this `guide to Webhooks`_. - Note: - 1. You will not be able to receive updates using :meth:`get_updates` for long as an - outgoing webhook is set up. - 2. To use a self-signed certificate, you need to upload your public key certificate - using certificate parameter. Please upload as InputFile, sending a String will not - work. - 3. Ports currently supported for Webhooks: - :attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`. - - If you're having any trouble setting up webhooks, please check out this `guide to - Webhooks`_. - .. seealso:: :meth:`telegram.ext.Application.run_webhook`, :meth:`telegram.ext.Updater.start_webhook` @@ -5020,7 +5008,7 @@ async def send_invoice( payload (:obj:`str`): Bot-defined invoice payload. :tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`- :tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be - displayed to the user, use for your internal processes. + displayed to the user, use it for your internal processes. provider_token (:obj:`str`): Payments provider token, obtained via `@BotFather `_. Pass an empty string for payments in |tg_stars|. @@ -5786,10 +5774,10 @@ async def edit_chat_invite_link( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - invite_link (:obj:`str` | :obj:`telegram.ChatInviteLink`): The invite link to edit. + invite_link (:obj:`str` | :class:`telegram.ChatInviteLink`): The invite link to edit. .. versionchanged:: 20.0 - Now also accepts :obj:`telegram.ChatInviteLink` instances. + Now also accepts :class:`telegram.ChatInviteLink` instances. expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will expire. For timezone naive :obj:`datetime.datetime` objects, the default timezone of the @@ -5858,10 +5846,10 @@ async def revoke_chat_invite_link( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| - invite_link (:obj:`str` | :obj:`telegram.ChatInviteLink`): The invite link to revoke. + invite_link (:obj:`str` | :class:`telegram.ChatInviteLink`): The invite link to revoke. .. versionchanged:: 20.0 - Now also accepts :obj:`telegram.ChatInviteLink` instances. + Now also accepts :class:`telegram.ChatInviteLink` instances. Returns: :class:`telegram.ChatInviteLink` @@ -7403,8 +7391,9 @@ async def set_my_default_administrator_rights( .. versionadded:: 20.0 Args: - rights (:obj:`telegram.ChatAdministratorRights`, optional): A - :obj:`telegram.ChatAdministratorRights` object describing new default administrator + rights (:class:`telegram.ChatAdministratorRights`, optional): A + :class:`telegram.ChatAdministratorRights` object describing new default + administrator rights. If not specified, the default administrator rights will be cleared. for_channels (:obj:`bool`, optional): Pass :obj:`True` to change the default administrator rights of the bot in channels. Otherwise, the default administrator @@ -7414,7 +7403,7 @@ async def set_my_default_administrator_rights( :obj:`bool`: Returns :obj:`True` on success. Raises: - :obj:`telegram.error.TelegramError` + :exc:`telegram.error.TelegramError` """ data: JSONDict = {"rights": rights, "for_channels": for_channels} @@ -7980,7 +7969,7 @@ async def create_invoice_link( payload (:obj:`str`): Bot-defined invoice payload. :tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`- :tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be - displayed to the user, use for your internal processes. + displayed to the user, use it for your internal processes. provider_token (:obj:`str`): Payments provider token, obtained via `@BotFather `_. Pass an empty string for payments in |tg_stars|. @@ -9078,7 +9067,7 @@ async def replace_sticker_in_set( user_id (:obj:`int`): User identifier of the sticker set owner. name (:obj:`str`): Sticker set name. old_sticker (:obj:`str`): File identifier of the replaced sticker. - sticker (:obj:`telegram.InputSticker`): An object with information about the added + sticker (:class:`telegram.InputSticker`): An object with information about the added sticker. If exactly the same sticker had already been added to the set, then the set remains unchanged. diff --git a/telegram/_chatbackground.py b/telegram/_chatbackground.py index f9c77619f4c..b33fd4d91ae 100644 --- a/telegram/_chatbackground.py +++ b/telegram/_chatbackground.py @@ -307,7 +307,7 @@ class BackgroundTypeFill(BackgroundType): .. versionadded:: 21.2 Args: - fill (:obj:`telegram.BackgroundFill`): The background fill. + fill (:class:`telegram.BackgroundFill`): The background fill. dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a percentage; 0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`. @@ -315,7 +315,7 @@ class BackgroundTypeFill(BackgroundType): Attributes: type (:obj:`str`): Type of the background. Always :attr:`~telegram.BackgroundType.FILL`. - fill (:obj:`telegram.BackgroundFill`): The background fill. + fill (:class:`telegram.BackgroundFill`): The background fill. dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a percentage; 0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`. @@ -349,7 +349,7 @@ class BackgroundTypeWallpaper(BackgroundType): .. versionadded:: 21.2 Args: - document (:obj:`telegram.Document`): Document with the wallpaper + document (:class:`telegram.Document`): Document with the wallpaper dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a percentage; 0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`. @@ -361,7 +361,7 @@ class BackgroundTypeWallpaper(BackgroundType): Attributes: type (:obj:`str`): Type of the background. Always :attr:`~telegram.BackgroundType.WALLPAPER`. - document (:obj:`telegram.Document`): Document with the wallpaper + document (:class:`telegram.Document`): Document with the wallpaper dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a percentage; 0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`. @@ -407,8 +407,8 @@ class BackgroundTypePattern(BackgroundType): .. versionadded:: 21.2 Args: - document (:obj:`telegram.Document`): Document with the pattern. - fill (:obj:`telegram.BackgroundFill`): The background fill that is combined with + document (:class:`telegram.Document`): Document with the pattern. + fill (:class:`telegram.BackgroundFill`): The background fill that is combined with the pattern. intensity (:obj:`int`): Intensity of the pattern when it is shown above the filled background; @@ -422,8 +422,8 @@ class BackgroundTypePattern(BackgroundType): Attributes: type (:obj:`str`): Type of the background. Always :attr:`~telegram.BackgroundType.PATTERN`. - document (:obj:`telegram.Document`): Document with the pattern. - fill (:obj:`telegram.BackgroundFill`): The background fill that is combined with + document (:class:`telegram.Document`): Document with the pattern. + fill (:class:`telegram.BackgroundFill`): The background fill that is combined with the pattern. intensity (:obj:`int`): Intensity of the pattern when it is shown above the filled background; @@ -511,10 +511,10 @@ class ChatBackground(TelegramObject): .. versionadded:: 21.2 Args: - type (:obj:`telegram.BackgroundType`): Type of the background. + type (:class:`telegram.BackgroundType`): Type of the background. Attributes: - type (:obj:`telegram.BackgroundType`): Type of the background. + type (:class:`telegram.BackgroundType`): Type of the background. """ __slots__ = ("type",) diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py index 04898659c3c..de26101f33c 100644 --- a/telegram/_chatfullinfo.py +++ b/telegram/_chatfullinfo.py @@ -78,7 +78,7 @@ class ChatFullInfo(_ChatBase): #collectible-usernames>`_; for private chats, supergroups and channels. .. versionadded:: 20.0 - birthdate (:obj:`telegram.Birthdate`, optional): For private chats, + birthdate (:class:`telegram.Birthdate`, optional): For private chats, the date of birth of the user. .. versionadded:: 21.1 @@ -94,8 +94,8 @@ class ChatFullInfo(_ChatBase): chats with business accounts, the opening hours of the business. .. versionadded:: 21.1 - personal_chat (:obj:`telegram.Chat`, optional): For private chats, the personal channel of - the user. + personal_chat (:class:`telegram.Chat`, optional): For private chats, the personal channel + of the user. .. versionadded:: 21.1 available_reactions (Sequence[:class:`telegram.ReactionType`], optional): List of available @@ -232,7 +232,7 @@ class ChatFullInfo(_ChatBase): obtained via :meth:`~telegram.Bot.get_chat`. .. versionadded:: 20.0 - birthdate (:obj:`telegram.Birthdate`): Optional. For private chats, + birthdate (:class:`telegram.Birthdate`): Optional. For private chats, the date of birth of the user. .. versionadded:: 21.1 @@ -248,8 +248,8 @@ class ChatFullInfo(_ChatBase): chats with business accounts, the opening hours of the business. .. versionadded:: 21.1 - personal_chat (:obj:`telegram.Chat`): Optional. For private chats, the personal channel of - the user. + personal_chat (:class:`telegram.Chat`): Optional. For private chats, the personal channel + of the user. .. versionadded:: 21.1 available_reactions (Tuple[:class:`telegram.ReactionType`]): Optional. List of available diff --git a/telegram/_files/inputsticker.py b/telegram/_files/inputsticker.py index 89f1db81d0c..8fc8b8461c6 100644 --- a/telegram/_files/inputsticker.py +++ b/telegram/_files/inputsticker.py @@ -49,7 +49,7 @@ class InputSticker(TelegramObject): :tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI` - :tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with the sticker. - mask_position (:obj:`telegram.MaskPosition`, optional): Position where the mask should be + mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should be placed on faces. For ":tg-const:`telegram.constants.StickerType.MASK`" stickers only. keywords (Sequence[:obj:`str`], optional): Sequence of 0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords @@ -71,7 +71,7 @@ class InputSticker(TelegramObject): :tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI` - :tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with the sticker. - mask_position (:obj:`telegram.MaskPosition`): Optional. Position where the mask should be + mask_position (:class:`telegram.MaskPosition`): Optional. Position where the mask should be placed on faces. For ":tg-const:`telegram.constants.StickerType.MASK`" stickers only. keywords (Tuple[:obj:`str`]): Optional. Tuple of 0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords diff --git a/telegram/_giveaway.py b/telegram/_giveaway.py index 0008dc9dd4c..b287433fe0b 100644 --- a/telegram/_giveaway.py +++ b/telegram/_giveaway.py @@ -126,7 +126,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["Giveaway"]: - """See :obj:`telegram.TelegramObject.de_json`.""" + """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if data is None: @@ -262,7 +262,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["GiveawayWinners"]: - """See :obj:`telegram.TelegramObject.de_json`.""" + """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if data is None: @@ -330,7 +330,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["GiveawayCompleted"]: - """See :obj:`telegram.TelegramObject.de_json`.""" + """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if data is None: diff --git a/telegram/_inline/inlinekeyboardbutton.py b/telegram/_inline/inlinekeyboardbutton.py index 6ceca1a311b..cff4df66a21 100644 --- a/telegram/_inline/inlinekeyboardbutton.py +++ b/telegram/_inline/inlinekeyboardbutton.py @@ -99,7 +99,7 @@ class InlineKeyboardButton(TelegramObject): .. seealso:: :wiki:`Arbitrary callback_data ` - web_app (:obj:`telegram.WebAppInfo`, optional): Description of the `Web App + web_app (:class:`telegram.WebAppInfo`, optional): Description of the `Web App `_ that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method :meth:`~telegram.Bot.answer_web_app_query`. Available only in @@ -107,16 +107,14 @@ class InlineKeyboardButton(TelegramObject): a Telegram Business account. .. versionadded:: 20.0 - switch_inline_query (:obj:`str`, optional): If set, pressing the button will insert the - bot's username and the specified inline query in the current chat's input field. May be - empty, in which case only the bot's username will be inserted. - - This offers a quick way for the user to open your bot in inline mode in the same chat - - good for selecting something from multiple options. Not supported in channels and for - messages sent on behalf of a Telegram Business account. + switch_inline_query (:obj:`str`, optional): If set, pressing the button will prompt the + user to select one of their chats, open that chat and insert the bot's username and the + specified inline query in the input field. May be empty, in which case just the bot's + username will be inserted. Not supported for messages sent on behalf of a Telegram + Business account. Tip: - This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`, + This is similar to the parameter :paramref:`switch_inline_query_chosen_chat`, but gives no control over which chats can be selected. switch_inline_query_current_chat (:obj:`str`, optional): If set, pressing the button will insert the bot's username and the specified inline query in the current chat's input @@ -137,7 +135,7 @@ class InlineKeyboardButton(TelegramObject): Note: This type of button **must** always be the first button in the first row and can only be used in invoice messages. - switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`, optional): + switch_inline_query_chosen_chat (:class:`telegram.SwitchInlineQueryChosenChat`, optional): If set, pressing the button will prompt the user to select one of their chats of the specified type, open that chat and insert the bot's username and the specified inline query in the input field. Not supported for messages sent on behalf of a Telegram @@ -170,7 +168,7 @@ class InlineKeyboardButton(TelegramObject): to the bot when the button is pressed, UTF-8 :tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`- :tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes. - web_app (:obj:`telegram.WebAppInfo`): Optional. Description of the `Web App + web_app (:class:`telegram.WebAppInfo`): Optional. Description of the `Web App `_ that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method :meth:`~telegram.Bot.answer_web_app_query`. Available only in @@ -178,16 +176,14 @@ class InlineKeyboardButton(TelegramObject): a Telegram Business account. .. versionadded:: 20.0 - switch_inline_query (:obj:`str`): Optional. If set, pressing the button will insert the - bot's username and the specified inline query in the current chat's input field. May be - empty, in which case only the bot's username will be inserted. - - This offers a quick way for the user to open your bot in inline mode in the same chat - - good for selecting something from multiple options. Not supported in channels and for - messages sent on behalf of a Telegram Business account. + switch_inline_query (:obj:`str`): Optional. If set, pressing the button will prompt the + user to select one of their chats, open that chat and insert the bot's username and the + specified inline query in the input field. May be empty, in which case just the bot's + username will be inserted. Not supported for messages sent on behalf of a Telegram + Business account. Tip: - This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`, + This is similar to the parameter :paramref:`switch_inline_query_chosen_chat`, but gives no control over which chats can be selected. switch_inline_query_current_chat (:obj:`str`): Optional. If set, pressing the button will insert the bot's username and the specified inline query in the current chat's input @@ -208,7 +204,7 @@ class InlineKeyboardButton(TelegramObject): Note: This type of button **must** always be the first button in the first row and can only be used in invoice messages. - switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`): Optional. + switch_inline_query_chosen_chat (:class:`telegram.SwitchInlineQueryChosenChat`): Optional. If set, pressing the button will prompt the user to select one of their chats of the specified type, open that chat and insert the bot's username and the specified inline query in the input field. Not supported for messages sent on behalf of a Telegram diff --git a/telegram/_inline/inlinequeryresultgif.py b/telegram/_inline/inlinequeryresultgif.py index 13e1f253b99..e5694e4f856 100644 --- a/telegram/_inline/inlinequeryresultgif.py +++ b/telegram/_inline/inlinequeryresultgif.py @@ -50,16 +50,14 @@ class InlineQueryResultGif(InlineQueryResult): gif_width (:obj:`int`, optional): Width of the GIF. gif_height (:obj:`int`, optional): Height of the GIF. gif_duration (:obj:`int`, optional): Duration of the GIF in seconds. - thumbnail_url (:obj:`str`, optional): URL of the static (JPEG or GIF) or animated (MPEG4) + thumbnail_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. - Warning: - The Bot API does **not** define this as an optional argument. It is formally - optional for backwards compatibility with the deprecated :paramref:`thumb_url`. - If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`, - :class:`ValueError` will be raised. - .. versionadded:: 20.2 + + ..versionchanged:: 20.5 + |thumbnail_url_mandatory| + thumbnail_mime_type (:obj:`str`, optional): MIME type of the thumbnail, must be one of ``'image/jpeg'``, ``'image/gif'``, or ``'video/mp4'``. Defaults to ``'image/jpeg'``. @@ -82,10 +80,6 @@ class InlineQueryResultGif(InlineQueryResult): .. versionadded:: 21.3 - Raises: - :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is - supplied or if both are supplied and are not equal. - Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.GIF`. id (:obj:`str`): Unique identifier for this result, diff --git a/telegram/_inline/inlinequeryresultmpeg4gif.py b/telegram/_inline/inlinequeryresultmpeg4gif.py index 1fff848418d..9e27ab949df 100644 --- a/telegram/_inline/inlinequeryresultmpeg4gif.py +++ b/telegram/_inline/inlinequeryresultmpeg4gif.py @@ -51,16 +51,14 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): mpeg4_width (:obj:`int`, optional): Video width. mpeg4_height (:obj:`int`, optional): Video height. mpeg4_duration (:obj:`int`, optional): Video duration in seconds. - thumbnail_url (:obj:`str`, optional): URL of the static (JPEG or GIF) or animated (MPEG4) + thumbnail_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. - Warning: - The Bot API does **not** define this as an optional argument. It is formally - optional for backwards compatibility with the deprecated :paramref:`thumb_url`. - If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`, - :class:`ValueError` will be raised. - .. versionadded:: 20.2 + + ..versionchanged:: 20.5 + |thumbnail_url_mandatory| + thumbnail_mime_type (:obj:`str`, optional): MIME type of the thumbnail, must be one of ``'image/jpeg'``, ``'image/gif'``, or ``'video/mp4'``. Defaults to ``'image/jpeg'``. @@ -83,9 +81,6 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| .. versionadded:: 21.3 - Raises: - :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is - supplied or if both are supplied and are not equal. Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.MPEG4GIF`. diff --git a/telegram/_inline/inlinequeryresultphoto.py b/telegram/_inline/inlinequeryresultphoto.py index 637e952d4d0..b74adf218e3 100644 --- a/telegram/_inline/inlinequeryresultphoto.py +++ b/telegram/_inline/inlinequeryresultphoto.py @@ -48,15 +48,13 @@ class InlineQueryResultPhoto(InlineQueryResult): :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. photo_url (:obj:`str`): A valid URL of the photo. Photo must be in JPEG format. Photo size must not exceed 5MB. - thumbnail_url (:obj:`str`, optional): URL of the thumbnail for the photo. - - Warning: - The Bot API does **not** define this as an optional argument. It is formally - optional for backwards compatibility with the deprecated :paramref:`thumb_url`. - If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`, - :class:`ValueError` will be raised. + thumbnail_url (:obj:`str`): URL of the thumbnail for the photo. .. versionadded:: 20.2 + + ..versionchanged:: 20.5 + |thumbnail_url_mandatory| + photo_width (:obj:`int`, optional): Width of the photo. photo_height (:obj:`int`, optional): Height of the photo. title (:obj:`str`, optional): Title for the result. @@ -78,10 +76,6 @@ class InlineQueryResultPhoto(InlineQueryResult): .. versionadded:: 21.3 - Raises: - :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is - supplied or if both are supplied and are not equal. - Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.PHOTO`. id (:obj:`str`): Unique identifier for this result, diff --git a/telegram/_inline/inlinequeryresultvideo.py b/telegram/_inline/inlinequeryresultvideo.py index 90bf4c86d3d..bb01c1ac1bd 100644 --- a/telegram/_inline/inlinequeryresultvideo.py +++ b/telegram/_inline/inlinequeryresultvideo.py @@ -55,20 +55,12 @@ class InlineQueryResultVideo(InlineQueryResult): mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4". thumbnail_url (:obj:`str`, optional): URL of the thumbnail (JPEG only) for the video. - Warning: - The Bot API does **not** define this as an optional argument. It is formally - optional for backwards compatibility with the deprecated :paramref:`thumb_url`. - If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`, - :class:`ValueError` will be raised. - .. versionadded:: 20.2 - title (:obj:`str`, optional): Title for the result. - Warning: - The Bot API does **not** define this as an optional argument. It is formally - optional to ensure backwards compatibility of :paramref:`thumbnail_url` with the - deprecated :paramref:`thumb_url`, which required that :paramref:`thumbnail_url` - become optional. :class:`TypeError` will be raised if no ``title`` is passed. + ..versionchanged:: 20.5 + |thumbnail_url_mandatory| + + title (:obj:`str`): Title for the result. caption (:obj:`str`, optional): Caption of the video to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -92,11 +84,6 @@ class InlineQueryResultVideo(InlineQueryResult): .. versionadded:: 21.3 - Raises: - :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is - supplied or if both are supplied and are not equal. - :class:`TypeError`: If no :paramref:`title` is passed. - Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.VIDEO`. id (:obj:`str`): Unique identifier for this result, diff --git a/telegram/_inline/inputinvoicemessagecontent.py b/telegram/_inline/inputinvoicemessagecontent.py index e13642da562..101e0184b57 100644 --- a/telegram/_inline/inputinvoicemessagecontent.py +++ b/telegram/_inline/inputinvoicemessagecontent.py @@ -47,7 +47,7 @@ class InputInvoiceMessageContent(InputMessageContent): payload (:obj:`str`): Bot-defined invoice payload. :tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`- :tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed - to the user, use for your internal processes. + to the user, use it for your internal processes. provider_token (:obj:`str`): Payment provider token, obtained via `@Botfather `_. Pass an empty string for payments in |tg_stars|. @@ -115,7 +115,7 @@ class InputInvoiceMessageContent(InputMessageContent): payload (:obj:`str`): Bot-defined invoice payload. :tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`- :tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed - to the user, use for your internal processes. + to the user, use it for your internal processes. provider_token (:obj:`str`): Payment provider token, obtained via `@Botfather `_. Pass an empty string for payments in `Telegram Stars `_. diff --git a/telegram/_message.py b/telegram/_message.py index 5e416ab7a50..48e34583ed6 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -569,22 +569,22 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: 21.1 - sender_business_bot (:obj:`telegram.User`, optional): The bot that actually sent the + sender_business_bot (:class:`telegram.User`, optional): The bot that actually sent the message on behalf of the business account. Available only for outgoing messages sent on behalf of the connected business account. .. versionadded:: 21.1 - chat_background_set (:obj:`telegram.ChatBackground`, optional): Service message: chat + chat_background_set (:class:`telegram.ChatBackground`, optional): Service message: chat background set. .. versionadded:: 21.2 - paid_media (:obj:`telegram.PaidMediaInfo`, optional): Message contains paid media; + paid_media (:class:`telegram.PaidMediaInfo`, optional): Message contains paid media; information about the paid media. .. versionadded:: 21.4 - refunded_payment (:obj:`telegram.RefundedPayment`, optional): Message is a service message - about a refunded payment, information about the payment. + refunded_payment (:class:`telegram.RefundedPayment`, optional): Message is a service + message about a refunded payment, information about the payment. .. versionadded:: 21.4 @@ -895,22 +895,22 @@ class Message(MaybeInaccessibleMessage): .. versionadded:: 21.1 - sender_business_bot (:obj:`telegram.User`): Optional. The bot that actually sent the + sender_business_bot (:class:`telegram.User`): Optional. The bot that actually sent the message on behalf of the business account. Available only for outgoing messages sent on behalf of the connected business account. .. versionadded:: 21.1 - chat_background_set (:obj:`telegram.ChatBackground`): Optional. Service message: chat + chat_background_set (:class:`telegram.ChatBackground`): Optional. Service message: chat background set .. versionadded:: 21.2 - paid_media (:obj:`telegram.PaidMediaInfo`): Optional. Message contains paid media; + paid_media (:class:`telegram.PaidMediaInfo`): Optional. Message contains paid media; information about the paid media. .. versionadded:: 21.4 - refunded_payment (:obj:`telegram.RefundedPayment`): Optional. Message is a service message - about a refunded payment, information about the payment. + refunded_payment (:class:`telegram.RefundedPayment`): Optional. Message is a service + message about a refunded payment, information about the payment. .. versionadded:: 21.4 diff --git a/telegram/_passport/credentials.py b/telegram/_passport/credentials.py index fab0b6eb2c8..7345991a5ac 100644 --- a/telegram/_passport/credentials.py +++ b/telegram/_passport/credentials.py @@ -112,7 +112,7 @@ class EncryptedCredentials(TelegramObject): Note: This object is decrypted only when originating from - :obj:`telegram.PassportData.decrypted_credentials`. + :attr:`telegram.PassportData.decrypted_credentials`. Args: data (:class:`telegram.Credentials` | :obj:`str`): Decrypted data with unique user's diff --git a/telegram/_passport/encryptedpassportelement.py b/telegram/_passport/encryptedpassportelement.py index b05003f2cbd..9f16d81e0f2 100644 --- a/telegram/_passport/encryptedpassportelement.py +++ b/telegram/_passport/encryptedpassportelement.py @@ -42,7 +42,7 @@ class EncryptedPassportElement(TelegramObject): Note: This object is decrypted only when originating from - :obj:`telegram.PassportData.decrypted_data`. + :attr:`telegram.PassportData.decrypted_data`. Args: type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license", diff --git a/telegram/_passport/passportfile.py b/telegram/_passport/passportfile.py index 61b70486279..84a1ce201ed 100644 --- a/telegram/_passport/passportfile.py +++ b/telegram/_passport/passportfile.py @@ -203,7 +203,7 @@ async def get_file( """ Wrapper over :meth:`telegram.Bot.get_file`. Will automatically assign the correct credentials to the returned :class:`telegram.File` if originating from - :obj:`telegram.PassportData.decrypted_data`. + :attr:`telegram.PassportData.decrypted_data`. For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index 0baebd39d7f..ed83aabc2a5 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -75,6 +75,17 @@ def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None) -> None: def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["RevenueWithdrawalState"]: + """Converts JSON data to the appropriate :class:`RevenueWithdrawalState` object, i.e. takes + care of selecting the correct subclass. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with this object. + + Returns: + The Telegram object. + + """ data = cls._parse_data(data) if not data: @@ -151,6 +162,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["RevenueWithdrawalStateSucceeded"]: + """See :meth:`telegram.RevenueWithdrawalState.de_json`.""" data = cls._parse_data(data) if not data: @@ -261,13 +273,13 @@ class TransactionPartnerFragment(TransactionPartner): .. versionadded:: 21.4 Args: - withdrawal_state (:obj:`telegram.RevenueWithdrawalState`, optional): State of the + withdrawal_state (:class:`telegram.RevenueWithdrawalState`, optional): State of the transaction if the transaction is outgoing. Attributes: type (:obj:`str`): The type of the transaction partner, always :tg-const:`telegram.TransactionPartner.FRAGMENT`. - withdrawal_state (:obj:`telegram.RevenueWithdrawalState`): Optional. State of the + withdrawal_state (:class:`telegram.RevenueWithdrawalState`): Optional. State of the transaction if the transaction is outgoing. """ @@ -288,6 +300,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["TransactionPartnerFragment"]: + """See :meth:`telegram.TransactionPartner.de_json`.""" data = cls._parse_data(data) if not data: @@ -356,6 +369,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["TransactionPartnerUser"]: + """See :meth:`telegram.TransactionPartner.de_json`.""" data = cls._parse_data(data) if not data: @@ -468,6 +482,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["StarTransaction"]: + """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: @@ -514,6 +529,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["StarTransactions"]: + """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if data is None: diff --git a/telegram/_reply.py b/telegram/_reply.py index 222e522a6a4..65e42665718 100644 --- a/telegram/_reply.py +++ b/telegram/_reply.py @@ -250,7 +250,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["ExternalReplyInfo"]: - """See :obj:`telegram.TelegramObject.de_json`.""" + """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if data is None: @@ -299,7 +299,8 @@ class TextQuote(TelegramObject): message. position (:obj:`int`): Approximate quote position in the original message in UTF-16 code units as specified by the sender. - entities (Sequence[:obj:`telegram.MessageEntity`], optional): Special entities that appear + entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities that + appear in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and custom_emoji entities are kept in quotes. is_manual (:obj:`bool`, optional): :obj:`True`, if the quote was chosen manually by the @@ -310,7 +311,7 @@ class TextQuote(TelegramObject): message. position (:obj:`int`): Approximate quote position in the original message in UTF-16 code units as specified by the sender. - entities (Tuple[:obj:`telegram.MessageEntity`]): Optional. Special entities that appear + entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special entities that appear in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and custom_emoji entities are kept in quotes. is_manual (:obj:`bool`): Optional. :obj:`True`, if the quote was chosen manually by the @@ -351,7 +352,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["TextQuote"]: - """See :obj:`telegram.TelegramObject.de_json`.""" + """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if data is None: @@ -387,7 +388,8 @@ class ReplyParameters(TelegramObject): quote_parse_mode (:obj:`str`, optional): Mode for parsing entities in the quote. See :wiki:`formatting options ` for more details. - quote_entities (Sequence[:obj:`telegram.MessageEntity`], optional): A JSON-serialized list + quote_entities (Sequence[:class:`telegram.MessageEntity`], optional): A JSON-serialized + list of special entities that appear in the quote. It can be specified instead of :paramref:`quote_parse_mode`. quote_position (:obj:`int`, optional): Position of the quote in the original message in @@ -409,8 +411,8 @@ class ReplyParameters(TelegramObject): quote_parse_mode (:obj:`str`): Optional. Mode for parsing entities in the quote. See :wiki:`formatting options ` for more details. - quote_entities (Tuple[:obj:`telegram.MessageEntity`]): Optional. A JSON-serialized list of - special entities that appear in the quote. It can be specified instead of + quote_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. A JSON-serialized list + of special entities that appear in the quote. It can be specified instead of :paramref:`quote_parse_mode`. quote_position (:obj:`int`): Optional. Position of the quote in the original message in UTF-16 code units. @@ -458,7 +460,7 @@ def __init__( def de_json( cls, data: Optional[JSONDict], bot: Optional["Bot"] = None ) -> Optional["ReplyParameters"]: - """See :obj:`telegram.TelegramObject.de_json`.""" + """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if data is None: diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index 6666b49b2ad..50407553226 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -354,7 +354,7 @@ def __deepcopy__(self: Tele_co, memodict: Dict[int, object]) -> Tele_co: memodict (:obj:`dict`): A dictionary that maps objects to their copies. Returns: - :obj:`telegram.TelegramObject`: The copied object. + :class:`telegram.TelegramObject`: The copied object. """ bot = self._bot # Save bot so we can set it after copying self.set_bot(None) # set to None so it is not deepcopied diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index de105e28b6a..fe5b8a79d60 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -302,7 +302,7 @@ def check_update(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: class MessageFilter(BaseFilter): """Base class for all Message Filters. In contrast to :class:`UpdateFilter`, the object passed - to :meth:`filter` is :obj:`telegram.Update.effective_message`. + to :meth:`filter` is :attr:`telegram.Update.effective_message`. Please see :class:`BaseFilter` for details on how to create custom filters. From 8c692d1008f35c4c4b0f125807592e5f1cf1189d Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 1 Sep 2024 15:25:34 +0200 Subject: [PATCH 26/26] Bump Version to v21.5 (#4449) --- CHANGES.rst | 52 +++++++++++++++++++ telegram/_bot.py | 10 ++-- telegram/_chat.py | 4 +- telegram/_chatinvitelink.py | 8 +-- telegram/_chatmember.py | 4 +- telegram/_files/inputfile.py | 4 +- telegram/_message.py | 4 +- telegram/_messageentity.py | 4 +- telegram/_payment/stars.py | 4 +- telegram/_reaction.py | 6 +-- telegram/_user.py | 4 +- telegram/_utils/strings.py | 2 +- telegram/_version.py | 2 +- telegram/constants.py | 4 +- .../ext/_handlers/callbackqueryhandler.py | 4 +- telegram/request/_requestdata.py | 2 +- telegram/request/_requestparameter.py | 2 +- 17 files changed, 86 insertions(+), 34 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f56a61b9b0c..8e5f302dd03 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,58 @@ Changelog ========= +Version 21.5 +============ + +*Released 2024-09-01* + +This is the technical changelog for version 21.5. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support for Bot API 7.9 (:pr:`4429`) +- Full Support for Bot API 7.8 (:pr:`4408`) + +New Features +------------ + +- Add ``MessageEntity.shift_entities`` and ``MessageEntity.concatenate`` (:pr:`4376` closes :issue:`4372`) +- Add Parameter ``game_pattern`` to ``CallbackQueryHandler`` (:pr:`4353` by `jainamoswal `_ closes :issue:`4269`) +- Add Parameter ``read_file_handle`` to ``InputFile`` (:pr:`4388` closes :issue:`4339`) + +Documentation Improvements +-------------------------- + +- Bugfix for "Available In" Admonitions (:pr:`4413`) +- Documentation Improvements (:pr:`4400` closes :issue:`4446`, :pr:`4448` by `Palaptin `_) +- Document Return Types of ``RequestData`` Members (:pr:`4396`) +- Add Introductory Paragraphs to Telegram Types Subsections (:pr:`4389` by `mohdyusuf2312 `_ closes :issue:`4380`) +- Start Adapting to RTD Addons (:pr:`4386`) + +Minor and Internal Changes +--------------------------- + +- Remove Surplus Logging from ``Updater`` Network Loop (:pr:`4432` by `MartinHjelmare `_) +- Add Internal Constants for Encodings (:pr:`4378` by `elpekenin `_) +- Improve PyPI Automation (:pr:`4375` closes :issue:`4373`) +- Update Test Suite to New Test Channel Setup (:pr:`4435`) +- Improve Fixture Usage in ``test_message.py`` (:pr:`4431` by `Palaptin `_) +- Update Python 3.13 Test Suite to RC1 (:pr:`4415`) +- Bump ``ruff`` and Add New Rules (:pr:`4416`) + +Dependency Updates +------------------ + +- Update ``cachetools`` requirement from <5.5.0,>=5.3.3 to >=5.3.3,<5.6.0 (:pr:`4437`) +- Bump ``sphinx`` from 7.4.7 to 8.0.2 and ``furo`` from 2024.7.18 to 2024.8.6 (:pr:`4412`) +- Bump ``test-summary/action`` from 2.3 to 2.4 (:pr:`4410`) +- Bump ``pytest`` from 8.2.2 to 8.3.2 (:pr:`4403`) +- Bump ``dependabot/fetch-metadata`` from 2.1.0 to 2.2.0 (:pr:`4411`) +- Update ``cachetools`` requirement from ~=5.3.3 to >=5.3.3,<5.5.0 (:pr:`4390`) +- Bump ``sphinx`` from 7.3.7 to 7.4.7 (:pr:`4395`) +- Bump ``furo`` from 2024.5.6 to 2024.7.18 (:pr:`4392`) + Version 21.4 ============ diff --git a/telegram/_bot.py b/telegram/_bot.py index 1e21206f41e..b79df08ff17 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -6145,7 +6145,7 @@ async def pin_chat_message( business_connection_id (:obj:`str`, optional): Unique identifier of the business connection on behalf of which the message will be pinned. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -6198,7 +6198,7 @@ async def unpin_chat_message( business_connection_id (:obj:`str`, optional): Unique identifier of the business connection on behalf of which the message will be unpinned. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -9229,7 +9229,7 @@ async def send_paid_media( keyboard, instructions to remove reply keyboard or to force a reply from the user. business_connection_id (:obj:`str`, optional): |business_id_str| - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -9294,7 +9294,7 @@ async def create_chat_subscription_invite_link( right. The link can be edited using the :meth:`edit_chat_subscription_invite_link` or revoked using the :meth:`revoke_chat_invite_link`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| @@ -9350,7 +9350,7 @@ async def edit_chat_subscription_invite_link( Use this method to edit a subscription invite link created by the bot. The bot must have :attr:`telegram.ChatPermissions.can_invite_users` administrator right. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| diff --git a/telegram/_chat.py b/telegram/_chat.py index a73a504d8c5..6eb78978596 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -2687,7 +2687,7 @@ async def create_subscription_invite_link( For the documentation of the arguments, please see :meth:`telegram.Bot.create_chat_subscription_invite_link`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Returns: :class:`telegram.ChatInviteLink` @@ -2724,7 +2724,7 @@ async def edit_subscription_invite_link( For the documentation of the arguments, please see :meth:`telegram.Bot.edit_chat_subscription_invite_link`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Returns: :class:`telegram.ChatInviteLink` diff --git a/telegram/_chatinvitelink.py b/telegram/_chatinvitelink.py index 1e1a0e1cf44..b26de4e332b 100644 --- a/telegram/_chatinvitelink.py +++ b/telegram/_chatinvitelink.py @@ -72,12 +72,12 @@ class ChatInviteLink(TelegramObject): subscription_period (:obj:`int`, optional): The number of seconds the subscription will be active for before the next payment. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 subscription_price (:obj:`int`, optional): The amount of Telegram Stars a user must pay initially and after each subsequent subscription period to be a member of the chat using the link. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Attributes: invite_link (:obj:`str`): The invite link. If the link was created by another chat @@ -109,12 +109,12 @@ class ChatInviteLink(TelegramObject): subscription_period (:obj:`int`): Optional. The number of seconds the subscription will be active for before the next payment. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 subscription_price (:obj:`int`): Optional. The amount of Telegram Stars a user must pay initially and after each subsequent subscription period to be a member of the chat using the link. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 """ diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index 1eabaa14e56..da84516b165 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -395,7 +395,7 @@ class ChatMemberMember(ChatMember): until_date (:class:`datetime.datetime`, optional): Date when the user's subscription will expire. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Attributes: status (:obj:`str`): The member's status in the chat, @@ -404,7 +404,7 @@ class ChatMemberMember(ChatMember): until_date (:class:`datetime.datetime`): Optional. Date when the user's subscription will expire. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 """ diff --git a/telegram/_files/inputfile.py b/telegram/_files/inputfile.py index 8f9c24a2076..e7c9cc6c64b 100644 --- a/telegram/_files/inputfile.py +++ b/telegram/_files/inputfile.py @@ -78,7 +78,7 @@ class InputFile: # here the file handle is already closed and the upload will fail await bot.send_document(chat_id, input_file) - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Attributes: @@ -126,7 +126,7 @@ def __init__( def field_tuple(self) -> FieldTuple: """Field tuple representing the contents of the file for upload to the Telegram servers. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.5 Content may now be a file handle. Returns: diff --git a/telegram/_message.py b/telegram/_message.py index 48e34583ed6..11bee572493 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -4112,7 +4112,7 @@ async def pin( For the documentation of the arguments, please see :meth:`telegram.Bot.pin_chat_message`. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.5 Now also passes :attr:`business_connection_id` to :meth:`telegram.Bot.pin_chat_message`. @@ -4152,7 +4152,7 @@ async def unpin( For the documentation of the arguments, please see :meth:`telegram.Bot.unpin_chat_message`. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.5 Now also passes :attr:`business_connection_id` to :meth:`telegram.Bot.pin_chat_message`. diff --git a/telegram/_messageentity.py b/telegram/_messageentity.py index cdf103b2d66..ae675e8e9fd 100644 --- a/telegram/_messageentity.py +++ b/telegram/_messageentity.py @@ -262,7 +262,7 @@ def shift_entities(by: Union[str, int], entities: _SEM) -> _SEM: The :paramref:`entities` are *not* modified in place. The function returns a sequence of new objects. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Args: by (:obj:`str` | :obj:`int`): Either the amount to shift the offset by or @@ -329,7 +329,7 @@ async def prefix_message(update: Update, context: ContextTypes.DEFAULT_TYPE): The entities are *not* modified in place. The function returns a new sequence of objects. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Args: *args (Tuple[:obj:`str`, Sequence[:class:`telegram.MessageEntity`]] | \ diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index ed83aabc2a5..94f621d00e8 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -327,7 +327,7 @@ class TransactionPartnerUser(TransactionPartner): paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid media bought by the user. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Attributes: type (:obj:`str`): The type of the transaction partner, @@ -337,7 +337,7 @@ class TransactionPartnerUser(TransactionPartner): paid_media (Tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid media bought by the user. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 """ __slots__ = ( diff --git a/telegram/_reaction.py b/telegram/_reaction.py index 55e8968e8eb..90de7823d79 100644 --- a/telegram/_reaction.py +++ b/telegram/_reaction.py @@ -35,7 +35,7 @@ class ReactionType(TelegramObject): and :class:`telegram.ReactionTypePaid`. .. versionadded:: 20.8 - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.5 Added paid reaction. @@ -59,7 +59,7 @@ class ReactionType(TelegramObject): PAID: Final[constants.ReactionType] = constants.ReactionType.PAID """:const:`telegram.constants.ReactionType.PAID` - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 """ def __init__( @@ -174,7 +174,7 @@ class ReactionTypePaid(ReactionType): """ The reaction is paid. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Attributes: type (:obj:`str`): Type of the reaction, diff --git a/telegram/_user.py b/telegram/_user.py index 50dd66870f3..075c4f12861 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -100,7 +100,7 @@ class User(TelegramObject): has_main_web_app (:obj:`bool`, optional): :obj:`True`, if the bot has the main Web App. Returned only in :meth:`telegram.Bot.get_me`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Attributes: id (:obj:`int`): Unique identifier for this user or bot. @@ -131,7 +131,7 @@ class User(TelegramObject): has_main_web_app (:obj:`bool`) Optional. :obj:`True`, if the bot has the main Web App. Returned only in :meth:`telegram.Bot.get_me`. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 .. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id` coincides with the :attr:`Chat.id` of the private chat with the user. This has been the diff --git a/telegram/_utils/strings.py b/telegram/_utils/strings.py index c57e4e48b32..9c386247eca 100644 --- a/telegram/_utils/strings.py +++ b/telegram/_utils/strings.py @@ -33,7 +33,7 @@ class TextEncoding(StringEnum): """This enum contains encoding schemes for text. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 """ __slots__ = () diff --git a/telegram/_version.py b/telegram/_version.py index ec3f5618c21..20043c8309b 100644 --- a/telegram/_version.py +++ b/telegram/_version.py @@ -51,6 +51,6 @@ def __str__(self) -> str: __version_info__: Final[Version] = Version( - major=21, minor=4, micro=0, releaselevel="final", serial=0 + major=21, minor=5, micro=0, releaselevel="final", serial=0 ) __version__: Final[str] = str(__version_info__) diff --git a/telegram/constants.py b/telegram/constants.py index 2867c6c7c4f..52d69aacaef 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -2907,7 +2907,7 @@ class ReactionType(StringEnum): PAID = "paid" """:obj:`str`: A :class:`telegram.ReactionType` with a paid reaction. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 """ @@ -3110,7 +3110,7 @@ class ChatSubscriptionLimit(IntEnum): :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_price`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 """ __slots__ = () diff --git a/telegram/ext/_handlers/callbackqueryhandler.py b/telegram/ext/_handlers/callbackqueryhandler.py index c8fb0e7b3f0..a149739a2fb 100644 --- a/telegram/ext/_handlers/callbackqueryhandler.py +++ b/telegram/ext/_handlers/callbackqueryhandler.py @@ -58,7 +58,7 @@ class CallbackQueryHandler(BaseHandler[Update, CCT]): `~telegram.CallbackQuery.game_short_name` or :attr:`~telegram.CallbackQuery.data` matching the defined pattern will be handled - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom @@ -100,7 +100,7 @@ async def callback(update: Update, context: CallbackContext) :attr:`telegram.CallbackQuery.game_short_name` to determine if an update should be handled by this handler. - .. versionadded:: NEXT.VERSION + .. versionadded:: 21.5 block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. diff --git a/telegram/request/_requestdata.py b/telegram/request/_requestdata.py index a6b8752ee66..71b2654e5b6 100644 --- a/telegram/request/_requestdata.py +++ b/telegram/request/_requestdata.py @@ -131,7 +131,7 @@ def json_payload(self) -> bytes: def multipart_data(self) -> UploadFileDict: """Gives the files contained in this object as mapping of part name to encoded content. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.5 Content may now be a file handle. """ multipart_data: UploadFileDict = {} diff --git a/telegram/request/_requestparameter.py b/telegram/request/_requestparameter.py index c3d19bdbd46..88ed231c066 100644 --- a/telegram/request/_requestparameter.py +++ b/telegram/request/_requestparameter.py @@ -79,7 +79,7 @@ def json_value(self) -> Optional[str]: def multipart_data(self) -> Optional[UploadFileDict]: """A dict with the file data to upload, if any. - .. versionchanged:: NEXT.VERSION + .. versionchanged:: 21.5 Content may now be a file handle. """ if not self.input_files: