From b11a0c7778a477dcaad3971f3d85bcdaa00be3f3 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:00:36 +0100 Subject: [PATCH] Make `TelegramObject` Immutable (#3249) --- docs/source/telegram.telegramobject.rst | 2 +- docs/substitutions/global.rst | 8 +- setup.cfg | 1 + telegram/_bot.py | 291 ++++++++++-------- telegram/_botcommand.py | 2 + telegram/_botcommandscope.py | 35 ++- telegram/_callbackquery.py | 6 +- telegram/_chat.py | 24 +- telegram/_chatadministratorrights.py | 2 + telegram/_chatinvitelink.py | 2 + telegram/_chatjoinrequest.py | 2 + telegram/_chatlocation.py | 2 + telegram/_chatmember.py | 64 ++-- telegram/_chatmemberupdated.py | 2 + telegram/_chatpermissions.py | 2 + telegram/_choseninlineresult.py | 2 + telegram/_dice.py | 2 + telegram/_files/animation.py | 15 +- telegram/_files/audio.py | 15 +- telegram/_files/chatphoto.py | 2 + telegram/_files/contact.py | 2 + telegram/_files/document.py | 7 +- telegram/_files/file.py | 2 + telegram/_files/inputmedia.py | 140 ++++++--- telegram/_files/location.py | 2 + telegram/_files/photosize.py | 7 +- telegram/_files/sticker.py | 49 +-- telegram/_files/venue.py | 2 + telegram/_files/video.py | 15 +- telegram/_files/videonote.py | 7 +- telegram/_files/voice.py | 9 +- telegram/_forcereply.py | 2 + telegram/_forumtopic.py | 4 + telegram/_games/game.py | 45 ++- telegram/_games/gamehighscore.py | 2 + telegram/_inline/inlinekeyboardbutton.py | 7 +- telegram/_inline/inlinekeyboardmarkup.py | 32 +- telegram/_inline/inlinequery.py | 2 + telegram/_inline/inlinequeryresult.py | 2 + telegram/_inline/inlinequeryresultarticle.py | 21 +- telegram/_inline/inlinequeryresultaudio.py | 39 ++- .../_inline/inlinequeryresultcachedaudio.py | 36 ++- .../inlinequeryresultcacheddocument.py | 38 ++- .../_inline/inlinequeryresultcachedgif.py | 36 ++- .../inlinequeryresultcachedmpeg4gif.py | 36 ++- .../_inline/inlinequeryresultcachedphoto.py | 38 ++- .../_inline/inlinequeryresultcachedsticker.py | 9 +- .../_inline/inlinequeryresultcachedvideo.py | 34 +- .../_inline/inlinequeryresultcachedvoice.py | 35 ++- telegram/_inline/inlinequeryresultcontact.py | 21 +- telegram/_inline/inlinequeryresultdocument.py | 46 +-- telegram/_inline/inlinequeryresultgame.py | 7 +- telegram/_inline/inlinequeryresultgif.py | 46 +-- telegram/_inline/inlinequeryresultlocation.py | 33 +- telegram/_inline/inlinequeryresultmpeg4gif.py | 49 +-- telegram/_inline/inlinequeryresultphoto.py | 44 ++- telegram/_inline/inlinequeryresultvenue.py | 29 +- telegram/_inline/inlinequeryresultvideo.py | 65 ++-- telegram/_inline/inlinequeryresultvoice.py | 38 ++- .../_inline/inputcontactmessagecontent.py | 2 + .../_inline/inputinvoicemessagecontent.py | 52 ++-- .../_inline/inputlocationmessagecontent.py | 2 + telegram/_inline/inputtextmessagecontent.py | 23 +- telegram/_inline/inputvenuemessagecontent.py | 2 + telegram/_keyboardbutton.py | 2 + telegram/_keyboardbuttonpolltype.py | 2 + telegram/_loginurl.py | 2 + telegram/_menubutton.py | 11 +- telegram/_message.py | 107 +++++-- telegram/_messageautodeletetimerchanged.py | 2 + telegram/_messageentity.py | 2 + telegram/_messageid.py | 2 + telegram/_passport/credentials.py | 45 ++- telegram/_passport/data.py | 6 + .../_passport/encryptedpassportelement.py | 43 ++- telegram/_passport/passportdata.py | 45 ++- telegram/_passport/passportelementerrors.py | 55 ++-- telegram/_passport/passportfile.py | 31 +- telegram/_payment/invoice.py | 2 + telegram/_payment/labeledprice.py | 2 + telegram/_payment/orderinfo.py | 2 + telegram/_payment/precheckoutquery.py | 2 + telegram/_payment/shippingaddress.py | 2 + telegram/_payment/shippingoption.py | 20 +- telegram/_payment/shippingquery.py | 2 + telegram/_payment/successfulpayment.py | 2 + telegram/_poll.py | 61 ++-- telegram/_proximityalerttriggered.py | 2 + telegram/_replykeyboardmarkup.py | 34 +- telegram/_replykeyboardremove.py | 2 + telegram/_sentwebappmessage.py | 2 + telegram/_telegramobject.py | 159 ++++++++-- telegram/_update.py | 2 + telegram/_user.py | 7 +- telegram/_userprofilephotos.py | 29 +- telegram/_utils/argumentparsing.py | 40 +++ telegram/_utils/markup.py | 8 +- telegram/_videochat.py | 27 +- telegram/_webappdata.py | 2 + telegram/_webappinfo.py | 2 + telegram/_webhookinfo.py | 29 +- telegram/ext/_aioratelimiter.py | 10 +- telegram/ext/_baseratelimiter.py | 6 +- telegram/ext/_callbackdatacache.py | 3 +- telegram/ext/_extbot.py | 74 +++-- telegram/ext/_picklepersistence.py | 3 + telegram/request/_baserequest.py | 6 +- tests/auxil/bot_method_checks.py | 2 +- tests/conftest.py | 10 +- tests/test_animation.py | 2 +- tests/test_application.py | 8 +- tests/test_audio.py | 2 +- tests/test_bot.py | 45 +-- tests/test_callbackdatacache.py | 1 + tests/test_callbackquery.py | 1 + tests/test_callbackqueryhandler.py | 5 +- tests/test_chat.py | 14 +- tests/test_chatjoinrequesthandler.py | 1 + tests/test_chatmemberhandler.py | 4 +- tests/test_chatmemberupdated.py | 5 +- tests/test_choseninlineresult.py | 4 +- tests/test_choseninlineresulthandler.py | 5 +- tests/test_commandhandler.py | 2 +- tests/test_conversationhandler.py | 52 ++++ tests/test_document.py | 2 +- tests/test_encryptedpassportelement.py | 7 +- tests/test_file.py | 1 + tests/test_filters.py | 17 +- tests/test_game.py | 10 +- tests/test_helpers.py | 2 +- tests/test_inlinekeyboardmarkup.py | 10 +- tests/test_inlinequeryhandler.py | 6 +- tests/test_inlinequeryresultaudio.py | 6 +- tests/test_inlinequeryresultcachedaudio.py | 6 +- tests/test_inlinequeryresultcacheddocument.py | 6 +- tests/test_inlinequeryresultcachedgif.py | 6 +- tests/test_inlinequeryresultcachedmpeg4gif.py | 8 +- tests/test_inlinequeryresultcachedphoto.py | 6 +- tests/test_inlinequeryresultcachedvideo.py | 7 +- tests/test_inlinequeryresultcachedvoice.py | 6 +- tests/test_inlinequeryresultdocument.py | 6 +- tests/test_inlinequeryresultgif.py | 6 +- tests/test_inlinequeryresultmpeg4gif.py | 6 +- tests/test_inlinequeryresultphoto.py | 6 +- tests/test_inlinequeryresultvideo.py | 8 +- tests/test_inlinequeryresultvoice.py | 11 +- tests/test_inputinvoicemessagecontent.py | 55 +++- tests/test_inputmedia.py | 54 +++- tests/test_inputtextmessagecontent.py | 6 +- tests/test_message.py | 8 +- tests/test_messagehandler.py | 2 + tests/test_passport.py | 1 + tests/test_photo.py | 2 +- tests/test_poll.py | 16 +- tests/test_replykeyboardmarkup.py | 11 +- tests/test_shippingoption.py | 2 +- tests/test_sticker.py | 4 +- tests/test_telegramobject.py | 141 ++++++++- tests/test_user.py | 1 + tests/test_userprofilephotos.py | 2 +- tests/test_video.py | 2 +- tests/test_videochat.py | 8 +- tests/test_voice.py | 2 +- tests/test_webhookinfo.py | 8 +- 164 files changed, 2155 insertions(+), 979 deletions(-) create mode 100644 telegram/_utils/argumentparsing.py diff --git a/docs/source/telegram.telegramobject.rst b/docs/source/telegram.telegramobject.rst index c9ce365a461..ca05cdd812e 100644 --- a/docs/source/telegram.telegramobject.rst +++ b/docs/source/telegram.telegramobject.rst @@ -4,4 +4,4 @@ telegram.TelegramObject .. autoclass:: telegram.TelegramObject :members: :show-inheritance: - :special-members: __repr__, __getitem__, __eq__, __hash__, __setstate__, __getstate__, __deepcopy__ + :special-members: __repr__, __getitem__, __eq__, __hash__, __setstate__, __getstate__, __deepcopy__, __setattr__, __delattr__ diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index c021020a799..233c2ca27a0 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -40,4 +40,10 @@ .. |disable_notification| replace:: Sends the message silently. Users will receive a notification with no sound. -.. |reply_to_msg_id| replace:: If the message is a reply, ID of the original message. \ No newline at end of file +.. |reply_to_msg_id| replace:: If the message is a reply, ID of the original message. + +.. |sequenceclassargs| replace:: Accepts any :class:`collections.abc.Sequence` as input instead of just a list. The input is converted to a tuple. + +.. |tupleclassattrs| replace:: This attribute is now an immutable tuple. + +.. |alwaystuple| replace:: This attribute is now always a tuple, that may be empty. diff --git a/setup.cfg b/setup.cfg index cac0ea41be4..a0e0eb8561b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ disable = duplicate-code,too-many-arguments,too-many-public-methods,too-few-publ missing-class-docstring,too-many-locals,too-many-lines,too-many-branches, too-many-statements enable=useless-suppression ; Warns about unused pylint ignores +exclude-protected=_unfrozen [tool:pytest] testpaths = tests diff --git a/telegram/_bot.py b/telegram/_bot.py index 0092a11a0e8..a6042b1f3b8 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -88,6 +88,7 @@ from telegram._update import Update from telegram._user import User from telegram._userprofilephotos import UserProfilePhotos +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.files import is_local_file, parse_file_input from telegram._utils.types import DVInput, FileInput, JSONDict, ODVInput, ReplyMarkup @@ -253,6 +254,8 @@ def __init__( private_key, password=private_key_password, backend=default_backend() ) + self._freeze() + @property def token(self) -> str: """:obj:`str`: Bot's unique authentication token. @@ -357,13 +360,16 @@ def _insert_defaults(self, data: Dict[str, object]) -> None: # skipcq: PYL-R020 if isinstance(val, InputMedia): # Copy object as not to edit it in-place val = copy.copy(val) - val.parse_mode = DefaultValue.get_value(val.parse_mode) + with val._unfrozen(): + val.parse_mode = DefaultValue.get_value(val.parse_mode) data[key] = val elif key == "media" and isinstance(val, list): # Copy objects as not to edit them in-place copy_list = [copy.copy(media) for media in val] for media in copy_list: - media.parse_mode = DefaultValue.get_value(media.parse_mode) + with media._unfrozen(): + media.parse_mode = DefaultValue.get_value(media.parse_mode) + data[key] = copy_list # 2) else: @@ -379,7 +385,10 @@ async def _post( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> Union[bool, JSONDict, None]: + ) -> Any: + # We know that the return type is Union[bool, JSONDict, List[JSONDict]], but it's hard + # to tell mypy which methods expects which of these return values and `Any` saves us a + # lot of `type: ignore` comments if data is None: data = {} @@ -410,7 +419,7 @@ async def _do_post( write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, - ) -> Union[bool, JSONDict, None]: + ) -> Union[bool, JSONDict, List[JSONDict]]: # This also converts datetimes into timestamps. # We don't do this earlier so that _insert_defaults (see above) has a chance to convert # to the default timezone in case this is called by ExtBot @@ -452,13 +461,15 @@ async def _send_message( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> Union[bool, Message]: + ) -> Any: """Protected method to send or edit messages of any type. It is here to reduce repetition of if-else closes in the different bot methods, i.e. this method takes care of adding its parameters to `data` if appropriate. Depending on the bot method, returns either `True` or the message. + However, it's hard to tell mypy which methods expects which of these return values and + using `Any` instead saves us a lot of `type: ignore` comments """ # We don't check if (DEFAULT_)None here, so that _post is able to insert the defaults # correctly, if necessary @@ -496,7 +507,7 @@ async def _send_message( if result is True: return result - return Message.de_json(result, self) # type: ignore[return-value, arg-type] + return Message.de_json(result, self) async def initialize(self) -> None: """Initialize resources used by this class. Currently calls :meth:`get_me` to @@ -668,7 +679,7 @@ async def get_me( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - self._bot_user = User.de_json(result, self) # type: ignore[arg-type] + self._bot_user = User.de_json(result, self) return self._bot_user # type: ignore[return-value] @_log @@ -731,7 +742,7 @@ async def send_message( """ data: JSONDict = {"chat_id": chat_id, "text": text, "entities": entities} - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendMessage", data, reply_to_message_id=reply_to_message_id, @@ -801,7 +812,7 @@ async def delete_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def forward_message( @@ -859,7 +870,7 @@ async def forward_message( "message_id": message_id, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "forwardMessage", data, disable_notification=disable_notification, @@ -950,7 +961,7 @@ async def send_photo( "photo": self._parse_file_input(photo, PhotoSize, filename=filename), } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendPhoto", data, reply_to_message_id=reply_to_message_id, @@ -1076,7 +1087,7 @@ async def send_audio( "thumb": self._parse_file_input(thumb, attach=True) if thumb else None, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendAudio", data, reply_to_message_id=reply_to_message_id, @@ -1195,7 +1206,7 @@ async def send_document( "thumb": self._parse_file_input(thumb, attach=True) if thumb else None, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendDocument", data, reply_to_message_id=reply_to_message_id, @@ -1274,7 +1285,7 @@ async def send_sticker( """ data: JSONDict = {"chat_id": chat_id, "sticker": self._parse_file_input(sticker, Sticker)} - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendSticker", data, reply_to_message_id=reply_to_message_id, @@ -1404,7 +1415,7 @@ async def send_video( "thumb": self._parse_file_input(thumb, attach=True) if thumb else None, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendVideo", data, reply_to_message_id=reply_to_message_id, @@ -1523,7 +1534,7 @@ async def send_video_note( "thumb": self._parse_file_input(thumb, attach=True) if thumb else None, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendVideoNote", data, reply_to_message_id=reply_to_message_id, @@ -1646,7 +1657,7 @@ async def send_animation( "thumb": self._parse_file_input(thumb, attach=True) if thumb else None, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendAnimation", data, reply_to_message_id=reply_to_message_id, @@ -1759,7 +1770,7 @@ async def send_voice( "duration": duration, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendVoice", data, reply_to_message_id=reply_to_message_id, @@ -1799,9 +1810,12 @@ async def send_media_group( caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, - ) -> List[Message]: + ) -> Tuple[Message, ...]: """Use this method to send a group of photos or videos as an album. + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + Note: If you supply a :paramref:`caption` (along with either :paramref:`parse_mode` or :paramref:`caption_entities`), then items in :paramref:`media` must have no captions, @@ -1848,7 +1862,7 @@ async def send_media_group( .. versionadded:: 20.0 Returns: - List[:class:`telegram.Message`]: An array of the sent Messages. + Tuple[:class:`telegram.Message`]: An array of the sent Messages. Raises: :class:`telegram.error.TelegramError` @@ -1867,10 +1881,11 @@ async def send_media_group( # Copy first item (to avoid mutation of original object), apply group caption to it. # This will lead to the group being shown with this caption. item_to_get_caption = copy.copy(media[0]) - item_to_get_caption.caption = caption - if parse_mode is not DEFAULT_NONE: - item_to_get_caption.parse_mode = parse_mode - item_to_get_caption.caption_entities = caption_entities + with item_to_get_caption._unfrozen(): + item_to_get_caption.caption = caption + if parse_mode is not DEFAULT_NONE: + item_to_get_caption.parse_mode = parse_mode + item_to_get_caption.caption_entities = parse_sequence_arg(caption_entities) # copy the list (just the references) to avoid mutating the original list media = media[:] @@ -1896,7 +1911,7 @@ async def send_media_group( api_kwargs=api_kwargs, ) - return Message.de_list(result, self) # type: ignore + return Message.de_list(result, self) @_log async def send_location( @@ -2000,7 +2015,7 @@ async def send_location( "proximity_alert_radius": proximity_alert_radius, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendLocation", data, reply_to_message_id=reply_to_message_id, @@ -2278,7 +2293,7 @@ async def send_venue( "google_place_type": google_place_type, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendVenue", data, reply_to_message_id=reply_to_message_id, @@ -2384,7 +2399,7 @@ async def send_contact( "vcard": vcard, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendContact", data, reply_to_message_id=reply_to_message_id, @@ -2450,7 +2465,7 @@ async def send_game( """ data: JSONDict = {"chat_id": chat_id, "game_short_name": game_short_name} - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendGame", data, reply_to_message_id=reply_to_message_id, @@ -2510,7 +2525,7 @@ async def send_chat_action( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result def _effective_inline_results( # skipcq: PYL-R0201 self, @@ -2582,23 +2597,30 @@ def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQ if hasattr(res, "parse_mode"): res = copy.copy(res) copied = True - res.parse_mode = DefaultValue.get_value(res.parse_mode) + with res._unfrozen(): + res.parse_mode = DefaultValue.get_value(res.parse_mode) if hasattr(res, "input_message_content") and res.input_message_content: if hasattr(res.input_message_content, "parse_mode"): if not copied: res = copy.copy(res) copied = True - res.input_message_content = copy.copy(res.input_message_content) - res.input_message_content.parse_mode = DefaultValue.get_value( - res.input_message_content.parse_mode - ) + + with res._unfrozen(): + res.input_message_content = copy.copy(res.input_message_content) + with res.input_message_content._unfrozen(): + res.input_message_content.parse_mode = DefaultValue.get_value( + res.input_message_content.parse_mode + ) if hasattr(res.input_message_content, "disable_web_page_preview"): if not copied: res = copy.copy(res) - res.input_message_content = copy.copy(res.input_message_content) - res.input_message_content.disable_web_page_preview = DefaultValue.get_value( - res.input_message_content.disable_web_page_preview - ) + + with res._unfrozen(): + res.input_message_content = copy.copy(res.input_message_content) + with res.input_message_content._unfrozen(): + res.input_message_content.disable_web_page_preview = DefaultValue.get_value( + res.input_message_content.disable_web_page_preview + ) return res @@ -2701,7 +2723,7 @@ async def answer_inline_query( "switch_pm_parameter": switch_pm_parameter, } - return await self._post( # type: ignore[return-value] + return await self._post( "answerInlineQuery", data, read_timeout=read_timeout, @@ -2756,7 +2778,7 @@ async def get_user_profile_photos( api_kwargs=api_kwargs, ) - return UserProfilePhotos.de_json(result, self) # type: ignore[arg-type,return-value] + return UserProfilePhotos.de_json(result, self) # type: ignore[return-value] @_log async def get_file( @@ -2819,9 +2841,9 @@ async def get_file( file_path = cast(dict, result).get("file_path") if file_path and not is_local_file(file_path): - result["file_path"] = f"{self._base_file_url}/{file_path}" # type: ignore[index] + result["file_path"] = f"{self._base_file_url}/{file_path}" - return File.de_json(result, self) # type: ignore[return-value, arg-type] + return File.de_json(result, self) # type: ignore[return-value] @_log async def ban_chat_member( @@ -2889,7 +2911,7 @@ async def ban_chat_member( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def ban_chat_sender_chat( @@ -2937,7 +2959,7 @@ async def ban_chat_sender_chat( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def unban_chat_member( @@ -2986,7 +3008,7 @@ async def unban_chat_member( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def unban_chat_sender_chat( @@ -3031,7 +3053,7 @@ async def unban_chat_sender_chat( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def answer_callback_query( @@ -3101,7 +3123,7 @@ async def answer_callback_query( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def edit_message_text( @@ -3385,9 +3407,12 @@ async def get_updates( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List[Update]: + ) -> Tuple[Update, ...]: """Use this method to receive incoming updates using long polling. + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + Note: 1. This method will not work if an outgoing webhook is set up. 2. In order to avoid getting duplicate updates, recalculate offset after each @@ -3420,7 +3445,7 @@ async def get_updates( a short period of time. Returns: - List[:class:`telegram.Update`] + Tuple[:class:`telegram.Update`] Raises: :class:`telegram.error.TelegramError` @@ -3456,7 +3481,7 @@ async def get_updates( else: self._logger.debug("No new updates found.") - return Update.de_list(result, self) # type: ignore[return-value] + return Update.de_list(result, self) @_log async def set_webhook( @@ -3577,7 +3602,7 @@ async def set_webhook( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def delete_webhook( @@ -3617,7 +3642,7 @@ async def delete_webhook( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def leave_chat( @@ -3656,7 +3681,7 @@ async def leave_chat( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def get_chat( @@ -3695,7 +3720,7 @@ async def get_chat( api_kwargs=api_kwargs, ) - return Chat.de_json(result, self) # type: ignore[return-value, arg-type] + return Chat.de_json(result, self) # type: ignore[return-value] @_log async def get_chat_administrators( @@ -3707,17 +3732,20 @@ async def get_chat_administrators( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List[ChatMember]: + ) -> Tuple[ChatMember, ...]: """ Use this method to get a list of administrators in a chat. .. seealso:: :attr:`telegram.Chat.get_administrators` + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| Returns: - List[:class:`telegram.ChatMember`]: On success, returns a list of ``ChatMember`` + Tuple[:class:`telegram.ChatMember`]: On success, returns a tuple of ``ChatMember`` objects that contains information about all chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. @@ -3736,7 +3764,7 @@ async def get_chat_administrators( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return ChatMember.de_list(result, self) # type: ignore + return ChatMember.de_list(result, self) @_log async def get_chat_member_count( @@ -3775,7 +3803,7 @@ async def get_chat_member_count( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def get_chat_member( @@ -3814,7 +3842,7 @@ async def get_chat_member( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return ChatMember.de_json(result, self) # type: ignore[return-value, arg-type] + return ChatMember.de_json(result, self) # type: ignore[return-value] @_log async def set_chat_sticker_set( @@ -3851,7 +3879,7 @@ async def set_chat_sticker_set( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def delete_chat_sticker_set( @@ -3885,7 +3913,7 @@ async def delete_chat_sticker_set( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result async def get_webhook_info( self, @@ -3913,7 +3941,7 @@ async def get_webhook_info( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return WebhookInfo.de_json(result, self) # type: ignore[return-value, arg-type] + return WebhookInfo.de_json(result, self) # type: ignore[return-value] @_log async def set_game_score( @@ -3993,11 +4021,14 @@ async def get_game_high_scores( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List[GameHighScore]: + ) -> Tuple[GameHighScore, ...]: """ Use this method to get data for high score tables. Will return the score of the specified user and several of their neighbors in a game. + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + Note: This method will currently return scores for the target user, plus two of their closest neighbors on each side. Will also return the top three users if the user and @@ -4015,7 +4046,7 @@ async def get_game_high_scores( :paramref:`message_id` are not specified. Identifier of the inline message. Returns: - List[:class:`telegram.GameHighScore`] + Tuple[:class:`telegram.GameHighScore`] Raises: :class:`telegram.error.TelegramError` @@ -4038,7 +4069,7 @@ async def get_game_high_scores( api_kwargs=api_kwargs, ) - return GameHighScore.de_list(result, self) # type: ignore + return GameHighScore.de_list(result, self) @_log async def send_invoice( @@ -4203,7 +4234,7 @@ async def send_invoice( "send_email_to_provider": send_email_to_provider, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendInvoice", data, reply_to_message_id=reply_to_message_id, @@ -4277,7 +4308,7 @@ async def answer_shipping_query( # pylint: disable=invalid-name api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def answer_pre_checkout_query( # pylint: disable=invalid-name @@ -4338,7 +4369,7 @@ async def answer_pre_checkout_query( # pylint: disable=invalid-name api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def answer_web_app_query( @@ -4385,7 +4416,7 @@ async def answer_web_app_query( api_kwargs=api_kwargs, ) - return SentWebAppMessage.de_json(api_result, self) # type: ignore[return-value, arg-type] + return SentWebAppMessage.de_json(api_result, self) # type: ignore[return-value] @_log async def restrict_chat_member( @@ -4445,7 +4476,7 @@ async def restrict_chat_member( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def promote_chat_member( @@ -4556,7 +4587,7 @@ async def promote_chat_member( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def set_chat_permissions( @@ -4598,7 +4629,7 @@ async def set_chat_permissions( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def set_chat_administrator_custom_title( @@ -4645,7 +4676,7 @@ async def set_chat_administrator_custom_title( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def export_chat_invite_link( @@ -4692,7 +4723,7 @@ async def export_chat_invite_link( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def create_chat_invite_link( @@ -4764,7 +4795,7 @@ async def create_chat_invite_link( api_kwargs=api_kwargs, ) - return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] + return ChatInviteLink.de_json(result, self) # type: ignore[return-value] @_log async def edit_chat_invite_link( @@ -4848,7 +4879,7 @@ async def edit_chat_invite_link( api_kwargs=api_kwargs, ) - return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] + return ChatInviteLink.de_json(result, self) # type: ignore[return-value] @_log async def revoke_chat_invite_link( @@ -4898,7 +4929,7 @@ async def revoke_chat_invite_link( api_kwargs=api_kwargs, ) - return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] + return ChatInviteLink.de_json(result, self) # type: ignore[return-value] @_log async def approve_chat_join_request( @@ -4944,7 +4975,7 @@ async def approve_chat_join_request( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def decline_chat_join_request( @@ -4990,7 +5021,7 @@ async def decline_chat_join_request( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def set_chat_photo( @@ -5040,7 +5071,7 @@ async def set_chat_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def delete_chat_photo( @@ -5080,7 +5111,7 @@ async def delete_chat_photo( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def set_chat_title( @@ -5124,7 +5155,7 @@ async def set_chat_title( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def set_chat_description( @@ -5169,7 +5200,7 @@ async def set_chat_description( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def pin_chat_message( @@ -5213,7 +5244,7 @@ async def pin_chat_message( "disable_notification": disable_notification, } - return await self._post( # type: ignore[return-value] + return await self._post( "pinChatMessage", data, read_timeout=read_timeout, @@ -5258,7 +5289,7 @@ async def unpin_chat_message( """ data: JSONDict = {"chat_id": chat_id, "message_id": message_id} - return await self._post( # type: ignore[return-value] + return await self._post( "unpinChatMessage", data, read_timeout=read_timeout, @@ -5300,7 +5331,7 @@ async def unpin_all_chat_messages( """ data: JSONDict = {"chat_id": chat_id} - return await self._post( # type: ignore[return-value] + return await self._post( "unpinAllChatMessages", data, read_timeout=read_timeout, @@ -5343,7 +5374,7 @@ async def get_sticker_set( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type] + return StickerSet.de_json(result, self) # type: ignore[return-value] @_log async def get_custom_emoji_stickers( @@ -5355,18 +5386,21 @@ async def get_custom_emoji_stickers( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List[Sticker]: + ) -> Tuple[Sticker, ...]: # skipcq: FLK-D207 """ Use this method to get information about emoji stickers by their identifiers. + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + Args: custom_emoji_ids (List[:obj:`str`]): List of custom emoji identifiers. At most :tg-const:`telegram.constants.CustomEmojiStickerLimit.\ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. Returns: - List[:class:`telegram.Sticker`] + Tuple[:class:`telegram.Sticker`] Raises: :class:`telegram.error.TelegramError` @@ -5382,7 +5416,7 @@ async def get_custom_emoji_stickers( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return Sticker.de_list(result, self) # type: ignore[return-value, arg-type] + return Sticker.de_list(result, self) @_log async def upload_sticker_file( @@ -5432,7 +5466,7 @@ async def upload_sticker_file( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return File.de_json(result, self) # type: ignore[return-value, arg-type] + return File.de_json(result, self) # type: ignore[return-value] @_log async def create_new_sticker_set( @@ -5552,7 +5586,7 @@ async def create_new_sticker_set( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def add_sticker_to_set( @@ -5650,7 +5684,7 @@ async def add_sticker_to_set( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def set_sticker_position_in_set( @@ -5687,7 +5721,7 @@ async def set_sticker_position_in_set( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def delete_sticker_from_set( @@ -5722,7 +5756,7 @@ async def delete_sticker_from_set( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def set_sticker_set_thumb( @@ -5782,7 +5816,7 @@ async def set_sticker_set_thumb( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def set_passport_data_errors( @@ -5827,7 +5861,7 @@ async def set_passport_data_errors( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def send_poll( @@ -5944,7 +5978,7 @@ async def send_poll( "close_date": close_date, } - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendPoll", data, reply_to_message_id=reply_to_message_id, @@ -6006,7 +6040,7 @@ async def stop_poll( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return Poll.de_json(result, self) # type: ignore[return-value, arg-type] + return Poll.de_json(result, self) # type: ignore[return-value] @_log async def send_dice( @@ -6072,7 +6106,7 @@ async def send_dice( """ data: JSONDict = {"chat_id": chat_id, "emoji": emoji} - return await self._send_message( # type: ignore[return-value] + return await self._send_message( "sendDice", data, reply_to_message_id=reply_to_message_id, @@ -6128,7 +6162,7 @@ async def get_my_default_administrator_rights( api_kwargs=api_kwargs, ) - return ChatAdministratorRights.de_json(result, self) # type: ignore[return-value,arg-type] + return ChatAdministratorRights.de_json(result, self) # type: ignore[return-value] @_log async def set_my_default_administrator_rights( @@ -6176,7 +6210,7 @@ async def set_my_default_administrator_rights( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def get_my_commands( @@ -6189,11 +6223,14 @@ async def get_my_commands( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List[BotCommand]: + ) -> Tuple[BotCommand, ...]: """ Use this method to get the current list of the bot's commands for the given scope and user language. + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + Args: scope (:class:`telegram.BotCommandScope`, optional): An object, describing scope of users. Defaults to :class:`telegram.BotCommandScopeDefault`. @@ -6206,8 +6243,8 @@ async def get_my_commands( .. versionadded:: 13.7 Returns: - List[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty - list is returned if commands are not set. + Tuple[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty + tuple is returned if commands are not set. Raises: :class:`telegram.error.TelegramError` @@ -6225,7 +6262,7 @@ async def get_my_commands( api_kwargs=api_kwargs, ) - return BotCommand.de_list(result, self) # type: ignore[return-value,arg-type] + return BotCommand.de_list(result, self) @_log async def set_my_commands( @@ -6282,7 +6319,7 @@ async def set_my_commands( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def delete_my_commands( @@ -6330,7 +6367,7 @@ async def delete_my_commands( api_kwargs=api_kwargs, ) - return result # type: ignore[return-value] + return result @_log async def log_out( @@ -6356,7 +6393,7 @@ async def log_out( :class:`telegram.error.TelegramError` """ - return await self._post( # type: ignore[return-value] + return await self._post( "logOut", read_timeout=read_timeout, write_timeout=write_timeout, @@ -6388,7 +6425,7 @@ async def close( :class:`telegram.error.TelegramError` """ - return await self._post( # type: ignore[return-value] + return await self._post( "close", read_timeout=read_timeout, write_timeout=write_timeout, @@ -6485,7 +6522,7 @@ async def copy_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return MessageId.de_json(result, self) # type: ignore[return-value, arg-type] + return MessageId.de_json(result, self) # type: ignore[return-value] @_log async def set_chat_menu_button( @@ -6520,7 +6557,7 @@ async def set_chat_menu_button( """ data: JSONDict = {"chat_id": chat_id, "menu_button": menu_button} - return await self._post( # type: ignore[return-value] + return await self._post( "setChatMenuButton", data, read_timeout=read_timeout, @@ -6569,7 +6606,7 @@ async def get_chat_menu_button( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return MenuButton.de_json(result, bot=self) # type: ignore[return-value, arg-type] + return MenuButton.de_json(result, bot=self) # type: ignore[return-value] @_log async def create_invoice_link( @@ -6684,7 +6721,7 @@ async def create_invoice_link( "send_email_to_provider": send_email_to_provider, } - return await self._post( # type: ignore[return-value] + return await self._post( "createInvoiceLink", data, read_timeout=read_timeout, @@ -6703,14 +6740,14 @@ async def get_forum_topic_icon_stickers( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List[Sticker]: + ) -> Tuple[Sticker, ...]: """Use this method to get custom emoji stickers, which can be used as a forum topic icon by any user. Requires no parameters. .. versionadded:: 20.0 Returns: - List[:class:`telegram.Sticker`] + Tuple[:class:`telegram.Sticker`] Raises: :class:`telegram.error.TelegramError` @@ -6724,7 +6761,7 @@ async def get_forum_topic_icon_stickers( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return Sticker.de_list(result, self) # type: ignore[return-value, arg-type] + return Sticker.de_list(result, self) @_log async def create_forum_topic( @@ -6786,7 +6823,7 @@ async def create_forum_topic( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - return ForumTopic.de_json(result, self) # type: ignore[return-value, arg-type] + return ForumTopic.de_json(result, self) # type: ignore[return-value] @_log async def edit_forum_topic( @@ -6836,7 +6873,7 @@ async def edit_forum_topic( "name": name, "icon_custom_emoji_id": icon_custom_emoji_id, } - return await self._post( # type: ignore[return-value] + return await self._post( "editForumTopic", data, read_timeout=read_timeout, @@ -6884,7 +6921,7 @@ async def close_forum_topic( "chat_id": chat_id, "message_thread_id": message_thread_id, } - return await self._post( # type: ignore[return-value] + return await self._post( "closeForumTopic", data, read_timeout=read_timeout, @@ -6932,7 +6969,7 @@ async def reopen_forum_topic( "chat_id": chat_id, "message_thread_id": message_thread_id, } - return await self._post( # type: ignore[return-value] + return await self._post( "reopenForumTopic", data, read_timeout=read_timeout, @@ -6979,7 +7016,7 @@ async def delete_forum_topic( "chat_id": chat_id, "message_thread_id": message_thread_id, } - return await self._post( # type: ignore[return-value] + return await self._post( "deleteForumTopic", data, read_timeout=read_timeout, @@ -7027,7 +7064,7 @@ async def unpin_all_forum_topic_messages( "chat_id": chat_id, "message_thread_id": message_thread_id, } - return await self._post( # type: ignore[return-value] + return await self._post( "unpinAllForumTopicMessages", data, read_timeout=read_timeout, @@ -7047,10 +7084,12 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # skipcq: PYL-W0613 return data def __eq__(self, other: object) -> bool: - return self.bot == other + if isinstance(other, self.__class__): + return self.bot == other.bot + return False def __hash__(self) -> int: - return hash(self.bot) + return hash((self.__class__, self.bot)) # camelCase aliases getMe = get_me diff --git a/telegram/_botcommand.py b/telegram/_botcommand.py index 650a2b59a7d..362d95fbcf7 100644 --- a/telegram/_botcommand.py +++ b/telegram/_botcommand.py @@ -55,6 +55,8 @@ def __init__(self, command: str, description: str, *, api_kwargs: JSONDict = Non self._id_attrs = (self.command, self.description) + self._freeze() + MIN_COMMAND: ClassVar[int] = constants.BotCommandLimit.MIN_COMMAND """:const:`telegram.constants.BotCommandLimit.MIN_COMMAND` diff --git a/telegram/_botcommandscope.py b/telegram/_botcommandscope.py index e04af64b250..bfe289cca67 100644 --- a/telegram/_botcommandscope.py +++ b/telegram/_botcommandscope.py @@ -80,6 +80,8 @@ def __init__(self, type: str, *, api_kwargs: JSONDict = None): self.type = type self._id_attrs = (self.type,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BotCommandScope"]: """Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes @@ -128,6 +130,7 @@ class BotCommandScopeDefault(BotCommandScope): def __init__(self, *, api_kwargs: JSONDict = None): super().__init__(type=BotCommandScope.DEFAULT, api_kwargs=api_kwargs) + self._freeze() class BotCommandScopeAllPrivateChats(BotCommandScope): @@ -143,6 +146,7 @@ class BotCommandScopeAllPrivateChats(BotCommandScope): def __init__(self, *, api_kwargs: JSONDict = None): super().__init__(type=BotCommandScope.ALL_PRIVATE_CHATS, api_kwargs=api_kwargs) + self._freeze() class BotCommandScopeAllGroupChats(BotCommandScope): @@ -157,6 +161,7 @@ class BotCommandScopeAllGroupChats(BotCommandScope): def __init__(self, *, api_kwargs: JSONDict = None): super().__init__(type=BotCommandScope.ALL_GROUP_CHATS, api_kwargs=api_kwargs) + self._freeze() class BotCommandScopeAllChatAdministrators(BotCommandScope): @@ -171,6 +176,7 @@ class BotCommandScopeAllChatAdministrators(BotCommandScope): def __init__(self, *, api_kwargs: JSONDict = None): super().__init__(type=BotCommandScope.ALL_CHAT_ADMINISTRATORS, api_kwargs=api_kwargs) + self._freeze() class BotCommandScopeChat(BotCommandScope): @@ -193,10 +199,11 @@ class BotCommandScopeChat(BotCommandScope): def __init__(self, chat_id: Union[str, int], *, api_kwargs: JSONDict = None): super().__init__(type=BotCommandScope.CHAT, api_kwargs=api_kwargs) - self.chat_id = ( - chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) - ) - self._id_attrs = (self.type, self.chat_id) + with self._unfrozen(): + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) + ) + self._id_attrs = (self.type, self.chat_id) class BotCommandScopeChatAdministrators(BotCommandScope): @@ -219,10 +226,11 @@ class BotCommandScopeChatAdministrators(BotCommandScope): def __init__(self, chat_id: Union[str, int], *, api_kwargs: JSONDict = None): super().__init__(type=BotCommandScope.CHAT_ADMINISTRATORS, api_kwargs=api_kwargs) - self.chat_id = ( - chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) - ) - self._id_attrs = (self.type, self.chat_id) + with self._unfrozen(): + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) + ) + self._id_attrs = (self.type, self.chat_id) class BotCommandScopeChatMember(BotCommandScope): @@ -248,8 +256,9 @@ class BotCommandScopeChatMember(BotCommandScope): def __init__(self, chat_id: Union[str, int], user_id: int, *, api_kwargs: JSONDict = None): super().__init__(type=BotCommandScope.CHAT_MEMBER, api_kwargs=api_kwargs) - self.chat_id = ( - chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) - ) - self.user_id = user_id - self._id_attrs = (self.type, self.chat_id, self.user_id) + with self._unfrozen(): + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) + ) + self.user_id = user_id + self._id_attrs = (self.type, self.chat_id, self.user_id) diff --git a/telegram/_callbackquery.py b/telegram/_callbackquery.py index 2a6fd1ba682..a6963f46146 100644 --- a/telegram/_callbackquery.py +++ b/telegram/_callbackquery.py @@ -134,6 +134,8 @@ def __init__( self._id_attrs = (self.id,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["CallbackQuery"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -590,7 +592,7 @@ async def get_game_high_scores( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List["GameHighScore"]: + ) -> Tuple["GameHighScore", ...]: """Shortcut for either:: await update.callback_query.message.get_game_high_score(*args, **kwargs) @@ -606,7 +608,7 @@ async def get_game_high_scores( :meth:`telegram.Message.get_game_high_scores`. Returns: - List[:class:`telegram.GameHighScore`] + Tuple[:class:`telegram.GameHighScore`] """ if self.inline_message_id: diff --git a/telegram/_chat.py b/telegram/_chat.py index 33fba0cf0d1..77fa665921e 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -20,7 +20,7 @@ """This module contains an object that represents a Telegram Chat.""" from datetime import datetime from html import escape -from typing import TYPE_CHECKING, ClassVar, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, ClassVar, List, Optional, Sequence, Tuple, Union from telegram import constants from telegram._chatlocation import ChatLocation @@ -30,6 +30,7 @@ from telegram._menubutton import MenuButton from telegram._telegramobject import TelegramObject from telegram._utils import enum +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import DVInput, FileInput, JSONDict, ODVInput, ReplyMarkup from telegram.helpers import escape_markdown @@ -153,7 +154,7 @@ class Chat(TelegramObject): (has topics_ enabled). .. versionadded:: 20.0 - active_usernames (List[:obj:`str`], optional): If set, the list of all `active chat + active_usernames (Sequence[:obj:`str`], optional): If set, the list of all `active chat usernames `_; for private chats, supergroups and channels. Returned only in :meth:`telegram.Bot.get_chat`. @@ -234,10 +235,12 @@ class Chat(TelegramObject): (has topics_ enabled). .. versionadded:: 20.0 - active_usernames (List[:obj:`str`]): Optional. If set, the list of all `active chat + active_usernames (Tuple[:obj:`str`]): Optional. If set, the list of all `active chat usernames `_; for private chats, supergroups and channels. Returned only in :meth:`telegram.Bot.get_chat`. + This list is empty if the chat has no active usernames or this chat instance was not + obtained via :meth:`~telegram.Bot.get_chat`. .. versionadded:: 20.0 emoji_status_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji @@ -318,7 +321,7 @@ def __init__( join_by_request: bool = None, has_restricted_voice_and_video_messages: bool = None, is_forum: bool = None, - active_usernames: List[str] = None, + active_usernames: Sequence[str] = None, emoji_status_custom_emoji_id: str = None, *, api_kwargs: JSONDict = None, @@ -352,11 +355,13 @@ def __init__( self.join_by_request = join_by_request self.has_restricted_voice_and_video_messages = has_restricted_voice_and_video_messages self.is_forum = is_forum - self.active_usernames = active_usernames + self.active_usernames = parse_sequence_arg(active_usernames) self.emoji_status_custom_emoji_id = emoji_status_custom_emoji_id self._id_attrs = (self.id,) + self._freeze() + @property def full_name(self) -> Optional[str]: """ @@ -546,7 +551,7 @@ async def get_administrators( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List["ChatMember"]: + ) -> Tuple["ChatMember", ...]: """Shortcut for:: await bot.get_chat_administrators(update.effective_chat.id, *args, **kwargs) @@ -555,7 +560,7 @@ async def get_administrators( :meth:`telegram.Bot.get_chat_administrators`. Returns: - List[:class:`telegram.ChatMember`]: A list of administrators in a chat. An Array of + Tuple[:class:`telegram.ChatMember`]: A tuple of administrators in a chat. An Array of :class:`telegram.ChatMember` objects that contains information about all chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. @@ -1292,7 +1297,7 @@ async def send_media_group( caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, - ) -> List["Message"]: + ) -> Tuple["Message", ...]: """Shortcut for:: await bot.send_media_group(update.effective_chat.id, *args, **kwargs) @@ -1300,7 +1305,8 @@ async def send_media_group( For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. Returns: - List[:class:`telegram.Message`]: On success, instance representing the message posted. + Tuple[:class:`telegram.Message`]: On success, a tuple of :class:`~telegram.Message` + instances that were sent is returned. """ return await self.get_bot().send_media_group( diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py index 32175cba586..6e5a657bd55 100644 --- a/telegram/_chatadministratorrights.py +++ b/telegram/_chatadministratorrights.py @@ -167,6 +167,8 @@ def __init__( self.can_manage_topics, ) + self._freeze() + @classmethod def all_rights(cls) -> "ChatAdministratorRights": """ diff --git a/telegram/_chatinvitelink.py b/telegram/_chatinvitelink.py index 0f8f6d2600a..a281b37cb67 100644 --- a/telegram/_chatinvitelink.py +++ b/telegram/_chatinvitelink.py @@ -141,6 +141,8 @@ def __init__( self.is_revoked, ) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatInviteLink"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_chatjoinrequest.py b/telegram/_chatjoinrequest.py index 2fbcfb96667..cf98ded2e1c 100644 --- a/telegram/_chatjoinrequest.py +++ b/telegram/_chatjoinrequest.py @@ -88,6 +88,8 @@ def __init__( self._id_attrs = (self.chat, self.from_user, self.date) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatJoinRequest"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_chatlocation.py b/telegram/_chatlocation.py index bbac4524ef2..0b885d436bb 100644 --- a/telegram/_chatlocation.py +++ b/telegram/_chatlocation.py @@ -62,6 +62,8 @@ def __init__( self._id_attrs = (self.location,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatLocation"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index a1db9db3666..678e682b7a3 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -98,6 +98,8 @@ def __init__( self._id_attrs = (self.user, self.status) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatMember"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -159,8 +161,9 @@ def __init__( api_kwargs: JSONDict = None, ): super().__init__(status=ChatMember.OWNER, user=user, api_kwargs=api_kwargs) - self.is_anonymous = is_anonymous - self.custom_title = custom_title + with self._unfrozen(): + self.is_anonymous = is_anonymous + self.custom_title = custom_title class ChatMemberAdministrator(ChatMember): @@ -295,20 +298,21 @@ def __init__( api_kwargs: JSONDict = None, ): super().__init__(status=ChatMember.ADMINISTRATOR, user=user, api_kwargs=api_kwargs) - self.can_be_edited = can_be_edited - self.is_anonymous = is_anonymous - self.can_manage_chat = can_manage_chat - self.can_delete_messages = can_delete_messages - self.can_manage_video_chats = can_manage_video_chats - self.can_restrict_members = can_restrict_members - self.can_promote_members = can_promote_members - self.can_change_info = can_change_info - self.can_invite_users = can_invite_users - self.can_post_messages = can_post_messages - self.can_edit_messages = can_edit_messages - self.can_pin_messages = can_pin_messages - self.can_manage_topics = can_manage_topics - self.custom_title = custom_title + with self._unfrozen(): + self.can_be_edited = can_be_edited + self.is_anonymous = is_anonymous + self.can_manage_chat = can_manage_chat + self.can_delete_messages = can_delete_messages + self.can_manage_video_chats = can_manage_video_chats + self.can_restrict_members = can_restrict_members + self.can_promote_members = can_promote_members + self.can_change_info = can_change_info + self.can_invite_users = can_invite_users + self.can_post_messages = can_post_messages + self.can_edit_messages = can_edit_messages + self.can_pin_messages = can_pin_messages + self.can_manage_topics = can_manage_topics + self.custom_title = custom_title class ChatMemberMember(ChatMember): @@ -337,6 +341,7 @@ def __init__( api_kwargs: JSONDict = None, ): super().__init__(status=ChatMember.MEMBER, user=user, api_kwargs=api_kwargs) + self._freeze() class ChatMemberRestricted(ChatMember): @@ -439,17 +444,18 @@ def __init__( api_kwargs: JSONDict = None, ): super().__init__(status=ChatMember.RESTRICTED, user=user, api_kwargs=api_kwargs) - self.is_member = is_member - self.can_change_info = can_change_info - self.can_invite_users = can_invite_users - self.can_pin_messages = can_pin_messages - self.can_send_messages = can_send_messages - self.can_send_media_messages = can_send_media_messages - self.can_send_polls = can_send_polls - self.can_send_other_messages = can_send_other_messages - self.can_add_web_page_previews = can_add_web_page_previews - self.can_manage_topics = can_manage_topics - self.until_date = until_date + with self._unfrozen(): + self.is_member = is_member + self.can_change_info = can_change_info + self.can_invite_users = can_invite_users + self.can_pin_messages = can_pin_messages + self.can_send_messages = can_send_messages + self.can_send_media_messages = can_send_media_messages + self.can_send_polls = can_send_polls + self.can_send_other_messages = can_send_other_messages + self.can_add_web_page_previews = can_add_web_page_previews + self.can_manage_topics = can_manage_topics + self.until_date = until_date class ChatMemberLeft(ChatMember): @@ -477,6 +483,7 @@ def __init__( api_kwargs: JSONDict = None, ): super().__init__(status=ChatMember.LEFT, user=user, api_kwargs=api_kwargs) + self._freeze() class ChatMemberBanned(ChatMember): @@ -510,4 +517,5 @@ def __init__( api_kwargs: JSONDict = None, ): super().__init__(status=ChatMember.BANNED, user=user, api_kwargs=api_kwargs) - self.until_date = until_date + with self._unfrozen(): + self.until_date = until_date diff --git a/telegram/_chatmemberupdated.py b/telegram/_chatmemberupdated.py index c497318a93f..f7f8833f991 100644 --- a/telegram/_chatmemberupdated.py +++ b/telegram/_chatmemberupdated.py @@ -108,6 +108,8 @@ def __init__( self.new_chat_member, ) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatMemberUpdated"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py index c05cfacbdd4..c9f62590640 100644 --- a/telegram/_chatpermissions.py +++ b/telegram/_chatpermissions.py @@ -142,6 +142,8 @@ def __init__( self.can_manage_topics, ) + self._freeze() + @classmethod def all_permissions(cls) -> "ChatPermissions": """ diff --git a/telegram/_choseninlineresult.py b/telegram/_choseninlineresult.py index 67d29d501f2..37ed70eb313 100644 --- a/telegram/_choseninlineresult.py +++ b/telegram/_choseninlineresult.py @@ -86,6 +86,8 @@ def __init__( self._id_attrs = (self.result_id,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChosenInlineResult"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_dice.py b/telegram/_dice.py index a25bdc992da..74086f6829f 100644 --- a/telegram/_dice.py +++ b/telegram/_dice.py @@ -88,6 +88,8 @@ def __init__(self, value: int, emoji: str, *, api_kwargs: JSONDict = None): self._id_attrs = (self.value, self.emoji) + self._freeze() + DICE: ClassVar[str] = constants.DiceEmoji.DICE # skipcq: PTC-W0052 """:const:`telegram.constants.DiceEmoji.DICE`""" DARTS: ClassVar[str] = constants.DiceEmoji.DARTS diff --git a/telegram/_files/animation.py b/telegram/_files/animation.py index 13ada07e15b..eb6f795414a 100644 --- a/telegram/_files/animation.py +++ b/telegram/_files/animation.py @@ -82,10 +82,11 @@ def __init__( thumb=thumb, api_kwargs=api_kwargs, ) - # Required - self.width = width - self.height = height - self.duration = duration - # Optional - self.mime_type = mime_type - self.file_name = file_name + with self._unfrozen(): + # Required + self.width = width + self.height = height + self.duration = duration + # Optional + self.mime_type = mime_type + self.file_name = file_name diff --git a/telegram/_files/audio.py b/telegram/_files/audio.py index a4a0cc06440..1b67748b34d 100644 --- a/telegram/_files/audio.py +++ b/telegram/_files/audio.py @@ -86,10 +86,11 @@ def __init__( thumb=thumb, api_kwargs=api_kwargs, ) - # Required - self.duration = duration - # Optional - self.performer = performer - self.title = title - self.mime_type = mime_type - self.file_name = file_name + with self._unfrozen(): + # Required + self.duration = duration + # Optional + self.performer = performer + self.title = title + self.mime_type = mime_type + self.file_name = file_name diff --git a/telegram/_files/chatphoto.py b/telegram/_files/chatphoto.py index fb1b005eda5..00d01438346 100644 --- a/telegram/_files/chatphoto.py +++ b/telegram/_files/chatphoto.py @@ -100,6 +100,8 @@ def __init__( self.big_file_unique_id, ) + self._freeze() + async def get_small_file( self, *, diff --git a/telegram/_files/contact.py b/telegram/_files/contact.py index a9e306fb49a..abc218f4a5b 100644 --- a/telegram/_files/contact.py +++ b/telegram/_files/contact.py @@ -66,3 +66,5 @@ def __init__( self.vcard = vcard self._id_attrs = (self.phone_number,) + + self._freeze() diff --git a/telegram/_files/document.py b/telegram/_files/document.py index e650ebde466..621154914f2 100644 --- a/telegram/_files/document.py +++ b/telegram/_files/document.py @@ -73,6 +73,7 @@ def __init__( thumb=thumb, api_kwargs=api_kwargs, ) - # Optional - self.mime_type = mime_type - self.file_name = file_name + with self._unfrozen(): + # Optional + self.mime_type = mime_type + self.file_name = file_name diff --git a/telegram/_files/file.py b/telegram/_files/file.py index 4f917679289..dbcfe2b4ca8 100644 --- a/telegram/_files/file.py +++ b/telegram/_files/file.py @@ -102,6 +102,8 @@ def __init__( self._id_attrs = (self.file_unique_id,) + self._freeze() + def _get_encoded_url(self) -> str: """Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string.""" sres = urllib_parse.urlsplit(str(self.file_path)) diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 75cd519629d..bf40b1ea21c 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.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/]. """Base class for Telegram InputMedia Objects.""" -from typing import List, Optional, Tuple, Union +from typing import Optional, Sequence, Union from telegram._files.animation import Animation from telegram._files.audio import Audio @@ -27,6 +27,7 @@ from telegram._files.video import Video from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.files import parse_file_input from telegram._utils.types import FileInput, JSONDict, ODVInput @@ -55,7 +56,11 @@ class InputMedia(TelegramObject): caption (:obj:`str`, optional): Caption of the media to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + parse_mode (:obj:`str`, optional): |parse_mode| Attributes: @@ -63,8 +68,13 @@ class InputMedia(TelegramObject): media (:obj:`str` | :class:`telegram.InputFile`): Media to send. caption (:obj:`str`): Optional. Caption of the media to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special - entities that appear in the caption. + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + """ __slots__ = ("caption", "caption_entities", "media", "parse_mode", "type") @@ -74,7 +84,7 @@ def __init__( media_type: str, media: Union[str, InputFile, MediaType], caption: str = None, - caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + caption_entities: Sequence[MessageEntity] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, *, api_kwargs: JSONDict = None, @@ -83,9 +93,11 @@ def __init__( self.type = media_type self.media = media self.caption = caption - self.caption_entities = caption_entities + self.caption_entities = parse_sequence_arg(caption_entities) self.parse_mode = parse_mode + self._freeze() + @staticmethod def _parse_thumb_input(thumb: Optional[FileInput]) -> Optional[Union[str, InputFile]]: # We use local_mode=True because we don't have access to the actual setting and want @@ -124,7 +136,11 @@ class InputMediaAnimation(InputMedia): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. duration (:obj:`int`, optional): Animation duration in seconds. @@ -134,8 +150,13 @@ class InputMediaAnimation(InputMedia): media (:obj:`str` | :class:`telegram.InputFile`): Animation to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special - entities that appear in the caption. + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. width (:obj:`int`): Optional. Animation width. height (:obj:`int`): Optional. Animation height. @@ -154,7 +175,7 @@ def __init__( width: int = None, height: int = None, duration: int = None, - caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + caption_entities: Sequence[MessageEntity] = None, filename: str = None, *, api_kwargs: JSONDict = None, @@ -177,10 +198,11 @@ def __init__( parse_mode, api_kwargs=api_kwargs, ) - self.thumb = self._parse_thumb_input(thumb) - self.width = width - self.height = height - self.duration = duration + with self._unfrozen(): + self.thumb = self._parse_thumb_input(thumb) + self.width = width + self.height = height + self.duration = duration class InputMediaPhoto(InputMedia): @@ -202,15 +224,22 @@ class InputMediaPhoto(InputMedia): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.PHOTO`. media (:obj:`str` | :class:`telegram.InputFile`): Photo to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special - entities that appear in the caption. + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| """ @@ -221,7 +250,7 @@ def __init__( media: Union[FileInput, PhotoSize], caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + caption_entities: Sequence[MessageEntity] = None, filename: str = None, *, api_kwargs: JSONDict = None, @@ -238,6 +267,8 @@ def __init__( api_kwargs=api_kwargs, ) + self._freeze() + class InputMediaVideo(InputMedia): """Represents a video to be sent. @@ -266,7 +297,11 @@ class InputMediaVideo(InputMedia): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. duration (:obj:`int`, optional): Video duration in seconds. @@ -283,8 +318,12 @@ class InputMediaVideo(InputMedia): media (:obj:`str` | :class:`telegram.InputFile`): Video file to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special - entities that appear in the caption. + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| width (:obj:`int`): Optional. Video width. height (:obj:`int`): Optional. Video height. duration (:obj:`int`): Optional. Video duration in seconds. @@ -306,7 +345,7 @@ def __init__( supports_streaming: bool = None, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, - caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + caption_entities: Sequence[MessageEntity] = None, filename: str = None, *, api_kwargs: JSONDict = None, @@ -330,11 +369,12 @@ def __init__( parse_mode, api_kwargs=api_kwargs, ) - self.width = width - self.height = height - self.duration = duration - self.thumb = self._parse_thumb_input(thumb) - self.supports_streaming = supports_streaming + with self._unfrozen(): + self.width = width + self.height = height + self.duration = duration + self.thumb = self._parse_thumb_input(thumb) + self.supports_streaming = supports_streaming class InputMediaAudio(InputMedia): @@ -361,7 +401,11 @@ class InputMediaAudio(InputMedia): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + duration (:obj:`int`): Duration of the audio in seconds as defined by sender. performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio tags. @@ -377,8 +421,12 @@ class InputMediaAudio(InputMedia): media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special - entities that appear in the caption. + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| duration (:obj:`int`): Duration of the audio in seconds. performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio tags. @@ -398,7 +446,7 @@ def __init__( duration: int = None, performer: str = None, title: str = None, - caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + caption_entities: Sequence[MessageEntity] = None, filename: str = None, *, api_kwargs: JSONDict = None, @@ -421,10 +469,11 @@ def __init__( parse_mode, api_kwargs=api_kwargs, ) - self.thumb = self._parse_thumb_input(thumb) - self.duration = duration - self.title = title - self.performer = performer + with self._unfrozen(): + self.thumb = self._parse_thumb_input(thumb) + self.duration = duration + self.title = title + self.performer = performer class InputMediaDocument(InputMedia): @@ -446,7 +495,11 @@ class InputMediaDocument(InputMedia): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ optional): |thumbdocstringnopath| @@ -461,8 +514,12 @@ class InputMediaDocument(InputMedia): media (:obj:`str` | :class:`telegram.InputFile`): File to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special - entities that appear in the caption. + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. disable_content_type_detection (:obj:`bool`): Optional. Disables automatic server-side content type detection for files uploaded using multipart/form-data. Always true, if @@ -479,7 +536,7 @@ def __init__( caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_content_type_detection: bool = None, - caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + caption_entities: Sequence[MessageEntity] = None, filename: str = None, *, api_kwargs: JSONDict = None, @@ -495,5 +552,6 @@ def __init__( parse_mode, api_kwargs=api_kwargs, ) - self.thumb = self._parse_thumb_input(thumb) - self.disable_content_type_detection = disable_content_type_detection + with self._unfrozen(): + self.thumb = self._parse_thumb_input(thumb) + self.disable_content_type_detection = disable_content_type_detection diff --git a/telegram/_files/location.py b/telegram/_files/location.py index 967eefdea76..59edda4e497 100644 --- a/telegram/_files/location.py +++ b/telegram/_files/location.py @@ -93,6 +93,8 @@ def __init__( self._id_attrs = (self.longitude, self.latitude) + self._freeze() + HORIZONTAL_ACCURACY: ClassVar[int] = constants.LocationLimit.HORIZONTAL_ACCURACY """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` diff --git a/telegram/_files/photosize.py b/telegram/_files/photosize.py index 86fc391426b..0628529578e 100644 --- a/telegram/_files/photosize.py +++ b/telegram/_files/photosize.py @@ -68,6 +68,7 @@ def __init__( file_size=file_size, api_kwargs=api_kwargs, ) - # Required - self.width = width - self.height = height + with self._unfrozen(): + # Required + self.width = width + self.height = height diff --git a/telegram/_files/sticker.py b/telegram/_files/sticker.py index 5c3e81d11af..f024652944b 100644 --- a/telegram/_files/sticker.py +++ b/telegram/_files/sticker.py @@ -17,14 +17,14 @@ # 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 represent stickers.""" - -from typing import TYPE_CHECKING, ClassVar, List, Optional +from typing import TYPE_CHECKING, ClassVar, Optional, Sequence from telegram import constants from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.file import File from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -150,18 +150,19 @@ def __init__( thumb=thumb, api_kwargs=api_kwargs, ) - # Required - self.width = width - self.height = height - self.is_animated = is_animated - self.is_video = is_video - self.type = type - # Optional - self.emoji = emoji - self.set_name = set_name - self.mask_position = mask_position - self.premium_animation = premium_animation - self.custom_emoji_id = custom_emoji_id + with self._unfrozen(): + # Required + self.width = width + self.height = height + self.is_animated = is_animated + self.is_video = is_video + self.type = type + # Optional + self.emoji = emoji + self.set_name = set_name + self.mask_position = mask_position + self.premium_animation = premium_animation + self.custom_emoji_id = custom_emoji_id REGULAR: ClassVar[str] = constants.StickerType.REGULAR """:const:`telegram.constants.StickerType.REGULAR`""" @@ -206,7 +207,11 @@ class StickerSet(TelegramObject): is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. .. versionadded:: 13.11 - stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + stickers (Sequence[:class:`telegram.Sticker`]): List of all set stickers. + + .. versionchanged:: 20.0 + |sequenceclassargs| + sticker_type (:obj:`str`): Type of stickers in the set, currently one of :attr:`telegram.Sticker.REGULAR`, :attr:`telegram.Sticker.MASK`, :attr:`telegram.Sticker.CUSTOM_EMOJI`. @@ -222,7 +227,11 @@ class StickerSet(TelegramObject): is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. .. versionadded:: 13.11 - stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + stickers (Tuple[:class:`telegram.Sticker`]): List of all set stickers. + + .. versionchanged:: 20.0 + |tupleclassattrs| + sticker_type (:obj:`str`): Type of stickers in the set. .. versionadded:: 20.0 @@ -246,7 +255,7 @@ def __init__( name: str, title: str, is_animated: bool, - stickers: List[Sticker], + stickers: Sequence[Sticker], is_video: bool, sticker_type: str, thumb: PhotoSize = None, @@ -258,13 +267,15 @@ def __init__( self.title = title self.is_animated = is_animated self.is_video = is_video - self.stickers = stickers + self.stickers = parse_sequence_arg(stickers) self.sticker_type = sticker_type # Optional self.thumb = thumb self._id_attrs = (self.name,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["StickerSet"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -339,3 +350,5 @@ def __init__( self.scale = scale self._id_attrs = (self.point, self.x_shift, self.y_shift, self.scale) + + self._freeze() diff --git a/telegram/_files/venue.py b/telegram/_files/venue.py index a87cfb57342..8106c275e5e 100644 --- a/telegram/_files/venue.py +++ b/telegram/_files/venue.py @@ -97,6 +97,8 @@ def __init__( self._id_attrs = (self.location, self.title) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Venue"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_files/video.py b/telegram/_files/video.py index c3c632f68b8..b305bf32530 100644 --- a/telegram/_files/video.py +++ b/telegram/_files/video.py @@ -82,10 +82,11 @@ def __init__( thumb=thumb, api_kwargs=api_kwargs, ) - # Required - self.width = width - self.height = height - self.duration = duration - # Optional - self.mime_type = mime_type - self.file_name = file_name + with self._unfrozen(): + # Required + self.width = width + self.height = height + self.duration = duration + # Optional + self.mime_type = mime_type + self.file_name = file_name diff --git a/telegram/_files/videonote.py b/telegram/_files/videonote.py index 5dd7941a169..31ec471f2c4 100644 --- a/telegram/_files/videonote.py +++ b/telegram/_files/videonote.py @@ -73,6 +73,7 @@ def __init__( thumb=thumb, api_kwargs=api_kwargs, ) - # Required - self.length = length - self.duration = duration + with self._unfrozen(): + # Required + self.length = length + self.duration = duration diff --git a/telegram/_files/voice.py b/telegram/_files/voice.py index 22346d3c095..47d6f7b6799 100644 --- a/telegram/_files/voice.py +++ b/telegram/_files/voice.py @@ -67,7 +67,8 @@ def __init__( file_size=file_size, api_kwargs=api_kwargs, ) - # Required - self.duration = duration - # Optional - self.mime_type = mime_type + with self._unfrozen(): + # Required + self.duration = duration + # Optional + self.mime_type = mime_type diff --git a/telegram/_forcereply.py b/telegram/_forcereply.py index 459d2a9eb40..1123bb60017 100644 --- a/telegram/_forcereply.py +++ b/telegram/_forcereply.py @@ -83,6 +83,8 @@ def __init__( self._id_attrs = (self.selective,) + self._freeze() + MIN_INPUT_FIELD_PLACEHOLDER: ClassVar[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER """:const:`telegram.constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER` diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py index 3cf3fe55be7..38684118dac 100644 --- a/telegram/_forumtopic.py +++ b/telegram/_forumtopic.py @@ -66,6 +66,8 @@ def __init__( self._id_attrs = (self.message_thread_id, self.name, self.icon_color) + self._freeze() + class ForumTopicCreated(TelegramObject): """ @@ -107,6 +109,8 @@ def __init__( self._id_attrs = (self.name, self.icon_color) + self._freeze() + class ForumTopicClosed(TelegramObject): """ diff --git a/telegram/_games/game.py b/telegram/_games/game.py index e7228d3957c..3691d3b1208 100644 --- a/telegram/_games/game.py +++ b/telegram/_games/game.py @@ -17,14 +17,14 @@ # 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 Game.""" - import sys -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence from telegram._files.animation import Animation from telegram._files.photosize import PhotoSize from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -42,30 +42,46 @@ class Game(TelegramObject): Args: title (:obj:`str`): Title of the game. description (:obj:`str`): Description of the game. - photo (List[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message - in chats. + photo (Sequence[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game + message in chats. + + .. versionchanged:: 20.0 + |sequenceclassargs| + text (:obj:`str`, optional): Brief description of the game or high scores included in the game message. Can be automatically edited to include current high scores for the game when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited using :meth:`telegram.Bot.edit_message_text`. 0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters. - text_entities (List[:class:`telegram.MessageEntity`], optional): Special entities that + text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities that appear in text, such as usernames, URLs, bot commands, etc. + + .. versionchanged:: 20.0 + |sequenceclassargs| + animation (:class:`telegram.Animation`, optional): Animation that will be displayed in the game message in chats. Upload via `BotFather `_. Attributes: title (:obj:`str`): Title of the game. description (:obj:`str`): Description of the game. - photo (List[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message - in chats. + photo (Tuple[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game + message in chats. + + .. versionchanged:: 20.0 + |tupleclassattrs| + text (:obj:`str`): Optional. Brief description of the game or high scores included in the game message. Can be automatically edited to include current high scores for the game when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited using :meth:`telegram.Bot.edit_message_text`. - text_entities (List[:class:`telegram.MessageEntity`]): Special entities that + text_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special entities that appear in text, such as usernames, URLs, bot commands, etc. This list is empty if the message does not contain text entities. + + .. versionchanged:: 20.0 + |tupleclassattrs| + animation (:class:`telegram.Animation`): Optional. Animation that will be displayed in the game message in chats. Upload via `BotFather `_. @@ -84,9 +100,9 @@ def __init__( self, title: str, description: str, - photo: List[PhotoSize], + photo: Sequence[PhotoSize], text: str = None, - text_entities: List[MessageEntity] = None, + text_entities: Sequence[MessageEntity] = None, animation: Animation = None, *, api_kwargs: JSONDict = None, @@ -95,14 +111,16 @@ def __init__( # Required self.title = title self.description = description - self.photo = photo + self.photo = parse_sequence_arg(photo) # Optionals self.text = text - self.text_entities = text_entities or [] + self.text_entities = parse_sequence_arg(text_entities) self.animation = animation self._id_attrs = (self.title, self.description, self.photo) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Game"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -178,6 +196,3 @@ def parse_text_entities(self, types: List[str] = None) -> Dict[MessageEntity, st for entity in self.text_entities if entity.type in types } - - def __hash__(self) -> int: - return hash((self.title, self.description, tuple(p for p in self.photo))) diff --git a/telegram/_games/gamehighscore.py b/telegram/_games/gamehighscore.py index ba1f788d75e..03a9401d82b 100644 --- a/telegram/_games/gamehighscore.py +++ b/telegram/_games/gamehighscore.py @@ -56,6 +56,8 @@ def __init__(self, position: int, user: User, score: int, *, api_kwargs: JSONDic self._id_attrs = (self.position, self.user, self.score) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["GameHighScore"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_inline/inlinekeyboardbutton.py b/telegram/_inline/inlinekeyboardbutton.py index 35f87ae9deb..34afb1e0ca3 100644 --- a/telegram/_inline/inlinekeyboardbutton.py +++ b/telegram/_inline/inlinekeyboardbutton.py @@ -202,6 +202,8 @@ def __init__( self._id_attrs = () self._set_id_attrs() + self._freeze() + def _set_id_attrs(self) -> None: self._id_attrs = ( self.text, @@ -239,8 +241,9 @@ def update_callback_data(self, callback_data: Union[str, object]) -> None: Args: callback_data (:class:`object`): The new callback data. """ - self.callback_data = callback_data - self._set_id_attrs() + with self._unfrozen(): + self.callback_data = callback_data + self._set_id_attrs() MIN_CALLBACK_DATA: ClassVar[int] = constants.InlineKeyboardButtonLimit.MIN_CALLBACK_DATA """:const:`telegram.constants.InlineKeyboardButtonLimit.MIN_CALLBACK_DATA` diff --git a/telegram/_inline/inlinekeyboardmarkup.py b/telegram/_inline/inlinekeyboardmarkup.py index 9fb6d4ee134..f4906097bf4 100644 --- a/telegram/_inline/inlinekeyboardmarkup.py +++ b/telegram/_inline/inlinekeyboardmarkup.py @@ -17,8 +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 InlineKeyboardMarkup.""" - -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, List, Optional, Sequence from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton from telegram._telegramobject import TelegramObject @@ -41,12 +40,20 @@ class InlineKeyboardMarkup(TelegramObject): * :any:`Inline Keyboard 2 ` Args: - inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows, - each represented by a list of InlineKeyboardButton objects. + inline_keyboard (Sequence[Sequence[:class:`telegram.InlineKeyboardButton`]]): Sequence of + button rows, each represented by a sequence of :class:`~telegram.InlineKeyboardButton` + objects. + + .. versionchanged:: 20.0 + |sequenceclassargs| Attributes: - inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows, - each represented by a list of InlineKeyboardButton objects. + inline_keyboard (Tuple[Tuple[:class:`telegram.InlineKeyboardButton`]]): Tuple of + button rows, each represented by a tuple of :class:`~telegram.InlineKeyboardButton` + objects. + + .. versionchanged:: 20.0 + |tupleclassattrs| """ @@ -54,21 +61,23 @@ class InlineKeyboardMarkup(TelegramObject): def __init__( self, - inline_keyboard: List[List[InlineKeyboardButton]], + inline_keyboard: Sequence[Sequence[InlineKeyboardButton]], *, api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) if not check_keyboard_type(inline_keyboard): raise ValueError( - "The parameter `inline_keyboard` should be a list of " - "list of InlineKeyboardButtons" + "The parameter `inline_keyboard` should be a sequence of sequences of " + "InlineKeyboardButtons" ) # Required - self.inline_keyboard = inline_keyboard + self.inline_keyboard = tuple(tuple(row) for row in inline_keyboard) self._id_attrs = (self.inline_keyboard,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineKeyboardMarkup"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -134,6 +143,3 @@ def from_column( """ button_grid = [[button] for button in button_column] return cls(button_grid, **kwargs) # type: ignore[arg-type] - - def __hash__(self) -> int: - return hash(tuple(tuple(button for button in row) for row in self.inline_keyboard)) diff --git a/telegram/_inline/inlinequery.py b/telegram/_inline/inlinequery.py index 963e6dfe1c6..abd2a40b205 100644 --- a/telegram/_inline/inlinequery.py +++ b/telegram/_inline/inlinequery.py @@ -106,6 +106,8 @@ def __init__( self._id_attrs = (self.id,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineQuery"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_inline/inlinequeryresult.py b/telegram/_inline/inlinequeryresult.py index 83a4e3e48eb..f3fa5881254 100644 --- a/telegram/_inline/inlinequeryresult.py +++ b/telegram/_inline/inlinequeryresult.py @@ -64,6 +64,8 @@ def __init__(self, type: str, id: str, *, api_kwargs: JSONDict = None): self._id_attrs = (self.id,) + self._freeze() + MIN_ID_LENGTH: ClassVar[int] = constants.InlineQueryResultLimit.MIN_ID_LENGTH """:const:`telegram.constants.InlineQueryResultLimit.MIN_ID_LENGTH` diff --git a/telegram/_inline/inlinequeryresultarticle.py b/telegram/_inline/inlinequeryresultarticle.py index 0d5d313b1ee..d77abf6a4db 100644 --- a/telegram/_inline/inlinequeryresultarticle.py +++ b/telegram/_inline/inlinequeryresultarticle.py @@ -102,14 +102,15 @@ def __init__( # Required super().__init__(InlineQueryResultType.ARTICLE, id, api_kwargs=api_kwargs) - self.title = title - self.input_message_content = input_message_content + with self._unfrozen(): + self.title = title + self.input_message_content = input_message_content - # Optional - self.reply_markup = reply_markup - self.url = url - self.hide_url = hide_url - self.description = description - self.thumb_url = thumb_url - self.thumb_width = thumb_width - self.thumb_height = thumb_height + # Optional + self.reply_markup = reply_markup + self.url = url + self.hide_url = hide_url + self.description = description + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height diff --git a/telegram/_inline/inlinequeryresultaudio.py b/telegram/_inline/inlinequeryresultaudio.py index 561ecd3b4ff..1c1bbe6d24c 100644 --- a/telegram/_inline/inlinequeryresultaudio.py +++ b/telegram/_inline/inlinequeryresultaudio.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultAudio.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -49,7 +49,10 @@ class InlineQueryResultAudio(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -68,7 +71,12 @@ class InlineQueryResultAudio(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -99,21 +107,22 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.AUDIO, id, api_kwargs=api_kwargs) - self.audio_url = audio_url - self.title = title + with self._unfrozen(): + self.audio_url = audio_url + self.title = title - # Optionals - self.performer = performer - self.audio_duration = audio_duration - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.performer = performer + self.audio_duration = audio_duration + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcachedaudio.py b/telegram/_inline/inlinequeryresultcachedaudio.py index db2afa6ddaa..370a9883313 100644 --- a/telegram/_inline/inlinequeryresultcachedaudio.py +++ b/telegram/_inline/inlinequeryresultcachedaudio.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedAudio.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -46,7 +46,11 @@ class InlineQueryResultCachedAudio(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -61,8 +65,13 @@ class InlineQueryResultCachedAudio(InlineQueryResult): caption (:obj:`str`): Optional. Caption, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. - parse_mode (:obj:`str`, optionals): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + parse_mode (:obj:`str`): Optional. |parse_mode| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -87,17 +96,18 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.AUDIO, id, api_kwargs=api_kwargs) - self.audio_file_id = audio_file_id + with self._unfrozen(): + self.audio_file_id = audio_file_id - # Optionals - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcacheddocument.py b/telegram/_inline/inlinequeryresultcacheddocument.py index ec897a05124..74cda79aca8 100644 --- a/telegram/_inline/inlinequeryresultcacheddocument.py +++ b/telegram/_inline/inlinequeryresultcacheddocument.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedDocument.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -48,7 +48,11 @@ class InlineQueryResultCachedDocument(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -66,7 +70,12 @@ class InlineQueryResultCachedDocument(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -95,19 +104,20 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.DOCUMENT, id, api_kwargs=api_kwargs) - self.title = title - self.document_file_id = document_file_id + with self._unfrozen(): + self.title = title + self.document_file_id = document_file_id - # Optionals - self.description = description - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.description = description + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcachedgif.py b/telegram/_inline/inlinequeryresultcachedgif.py index 9f463192ab4..2f5038d3a3e 100644 --- a/telegram/_inline/inlinequeryresultcachedgif.py +++ b/telegram/_inline/inlinequeryresultcachedgif.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedGif.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -48,7 +48,11 @@ class InlineQueryResultCachedGif(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -65,7 +69,12 @@ class InlineQueryResultCachedGif(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -92,18 +101,19 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.GIF, id, api_kwargs=api_kwargs) - self.gif_file_id = gif_file_id + with self._unfrozen(): + self.gif_file_id = gif_file_id - # Optionals - self.title = title - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.title = title + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcachedmpeg4gif.py b/telegram/_inline/inlinequeryresultcachedmpeg4gif.py index fd607900e4f..1857113832f 100644 --- a/telegram/_inline/inlinequeryresultcachedmpeg4gif.py +++ b/telegram/_inline/inlinequeryresultcachedmpeg4gif.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -48,7 +48,11 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -65,7 +69,12 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -92,18 +101,19 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.MPEG4GIF, id, api_kwargs=api_kwargs) - self.mpeg4_file_id = mpeg4_file_id + with self._unfrozen(): + self.mpeg4_file_id = mpeg4_file_id - # Optionals - self.title = title - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.title = title + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcachedphoto.py b/telegram/_inline/inlinequeryresultcachedphoto.py index 5cccb951863..30855e7129c 100644 --- a/telegram/_inline/inlinequeryresultcachedphoto.py +++ b/telegram/_inline/inlinequeryresultcachedphoto.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultPhoto""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -49,7 +49,11 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -67,7 +71,12 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -96,19 +105,20 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.PHOTO, id, api_kwargs=api_kwargs) - self.photo_file_id = photo_file_id + with self._unfrozen(): + self.photo_file_id = photo_file_id - # Optionals - self.title = title - self.description = description - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.title = title + self.description = description + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcachedsticker.py b/telegram/_inline/inlinequeryresultcachedsticker.py index ea1ed07dc1a..f4a121e6cab 100644 --- a/telegram/_inline/inlinequeryresultcachedsticker.py +++ b/telegram/_inline/inlinequeryresultcachedsticker.py @@ -71,8 +71,9 @@ def __init__( ): # Required super().__init__(InlineQueryResultType.STICKER, id, api_kwargs=api_kwargs) - self.sticker_file_id = sticker_file_id + with self._unfrozen(): + self.sticker_file_id = sticker_file_id - # Optionals - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcachedvideo.py b/telegram/_inline/inlinequeryresultcachedvideo.py index 6b59f338180..d82ca22a896 100644 --- a/telegram/_inline/inlinequeryresultcachedvideo.py +++ b/telegram/_inline/inlinequeryresultcachedvideo.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedVideo.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -49,7 +49,7 @@ class InlineQueryResultCachedVideo(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -67,7 +67,12 @@ class InlineQueryResultCachedVideo(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -96,19 +101,20 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.VIDEO, id, api_kwargs=api_kwargs) - self.video_file_id = video_file_id - self.title = title + with self._unfrozen(): + self.video_file_id = video_file_id + self.title = title - # Optionals - self.description = description - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.description = description + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcachedvoice.py b/telegram/_inline/inlinequeryresultcachedvoice.py index 112e1ae7418..19c5f4051bf 100644 --- a/telegram/_inline/inlinequeryresultcachedvoice.py +++ b/telegram/_inline/inlinequeryresultcachedvoice.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedVoice.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -47,7 +47,10 @@ class InlineQueryResultCachedVoice(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -64,7 +67,12 @@ class InlineQueryResultCachedVoice(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -91,18 +99,19 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.VOICE, id, api_kwargs=api_kwargs) - self.voice_file_id = voice_file_id - self.title = title + with self._unfrozen(): + self.voice_file_id = voice_file_id + self.title = title - # Optionals - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultcontact.py b/telegram/_inline/inlinequeryresultcontact.py index 63a9db86e8f..8a46d67d80e 100644 --- a/telegram/_inline/inlinequeryresultcontact.py +++ b/telegram/_inline/inlinequeryresultcontact.py @@ -101,14 +101,15 @@ def __init__( ): # Required super().__init__(InlineQueryResultType.CONTACT, id, api_kwargs=api_kwargs) - self.phone_number = phone_number - self.first_name = first_name + with self._unfrozen(): + self.phone_number = phone_number + self.first_name = first_name - # Optionals - self.last_name = last_name - self.vcard = vcard - self.reply_markup = reply_markup - self.input_message_content = input_message_content - self.thumb_url = thumb_url - self.thumb_width = thumb_width - self.thumb_height = thumb_height + # Optionals + self.last_name = last_name + self.vcard = vcard + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height diff --git a/telegram/_inline/inlinequeryresultdocument.py b/telegram/_inline/inlinequeryresultdocument.py index df0c1a81962..e262b38c4dd 100644 --- a/telegram/_inline/inlinequeryresultdocument.py +++ b/telegram/_inline/inlinequeryresultdocument.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultDocument""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -47,7 +47,11 @@ class InlineQueryResultDocument(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + document_url (:obj:`str`): A valid URL for the file. mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf" or "application/zip". @@ -70,7 +74,12 @@ class InlineQueryResultDocument(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| document_url (:obj:`str`): A valid URL for the file. mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf" or "application/zip". @@ -114,23 +123,24 @@ def __init__( thumb_width: int = None, thumb_height: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.DOCUMENT, id, api_kwargs=api_kwargs) - self.document_url = document_url - self.title = title - self.mime_type = mime_type + with self._unfrozen(): + self.document_url = document_url + self.title = title + self.mime_type = mime_type - # Optionals - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.description = description - self.reply_markup = reply_markup - self.input_message_content = input_message_content - self.thumb_url = thumb_url - self.thumb_width = thumb_width - self.thumb_height = thumb_height + # Optionals + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.description = description + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height diff --git a/telegram/_inline/inlinequeryresultgame.py b/telegram/_inline/inlinequeryresultgame.py index 9bf8e700495..31f9470926d 100644 --- a/telegram/_inline/inlinequeryresultgame.py +++ b/telegram/_inline/inlinequeryresultgame.py @@ -58,7 +58,8 @@ def __init__( ): # Required super().__init__(InlineQueryResultType.GAME, id, api_kwargs=api_kwargs) - self.id = id - self.game_short_name = game_short_name + with self._unfrozen(): + self.id = id + self.game_short_name = game_short_name - self.reply_markup = reply_markup + self.reply_markup = reply_markup diff --git a/telegram/_inline/inlinequeryresultgif.py b/telegram/_inline/inlinequeryresultgif.py index 39eab95ed47..9bbc236478c 100644 --- a/telegram/_inline/inlinequeryresultgif.py +++ b/telegram/_inline/inlinequeryresultgif.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultGif.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -54,7 +54,11 @@ class InlineQueryResultGif(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -77,7 +81,12 @@ class InlineQueryResultGif(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -114,24 +123,25 @@ def __init__( gif_duration: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb_mime_type: str = None, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.GIF, id, api_kwargs=api_kwargs) - self.gif_url = gif_url - self.thumb_url = thumb_url + with self._unfrozen(): + self.gif_url = gif_url + self.thumb_url = thumb_url - # Optionals - self.gif_width = gif_width - self.gif_height = gif_height - self.gif_duration = gif_duration - self.title = title - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content - self.thumb_mime_type = thumb_mime_type + # Optionals + self.gif_width = gif_width + self.gif_height = gif_height + self.gif_duration = gif_duration + self.title = title + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_mime_type = thumb_mime_type diff --git a/telegram/_inline/inlinequeryresultlocation.py b/telegram/_inline/inlinequeryresultlocation.py index 02a27d0c99a..145d5138134 100644 --- a/telegram/_inline/inlinequeryresultlocation.py +++ b/telegram/_inline/inlinequeryresultlocation.py @@ -127,22 +127,23 @@ def __init__( ): # Required super().__init__(constants.InlineQueryResultType.LOCATION, id, api_kwargs=api_kwargs) - self.latitude = latitude - self.longitude = longitude - self.title = title - - # Optionals - self.live_period = live_period - self.reply_markup = reply_markup - self.input_message_content = input_message_content - self.thumb_url = thumb_url - self.thumb_width = thumb_width - self.thumb_height = thumb_height - self.horizontal_accuracy = horizontal_accuracy - self.heading = heading - self.proximity_alert_radius = ( - int(proximity_alert_radius) if proximity_alert_radius else None - ) + with self._unfrozen(): + self.latitude = latitude + self.longitude = longitude + self.title = title + + # Optionals + self.live_period = live_period + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height + self.horizontal_accuracy = horizontal_accuracy + self.heading = heading + self.proximity_alert_radius = ( + int(proximity_alert_radius) if proximity_alert_radius else None + ) HORIZONTAL_ACCURACY: ClassVar[int] = constants.LocationLimit.HORIZONTAL_ACCURACY """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` diff --git a/telegram/_inline/inlinequeryresultmpeg4gif.py b/telegram/_inline/inlinequeryresultmpeg4gif.py index 448a9c91b87..5656da94a24 100644 --- a/telegram/_inline/inlinequeryresultmpeg4gif.py +++ b/telegram/_inline/inlinequeryresultmpeg4gif.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -54,7 +54,11 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -77,7 +81,13 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -114,24 +124,25 @@ def __init__( mpeg4_duration: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb_mime_type: str = None, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.MPEG4GIF, id, api_kwargs=api_kwargs) - self.mpeg4_url = mpeg4_url - self.thumb_url = thumb_url - - # Optional - self.mpeg4_width = mpeg4_width - self.mpeg4_height = mpeg4_height - self.mpeg4_duration = mpeg4_duration - self.title = title - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content - self.thumb_mime_type = thumb_mime_type + with self._unfrozen(): + self.mpeg4_url = mpeg4_url + self.thumb_url = thumb_url + + # Optional + self.mpeg4_width = mpeg4_width + self.mpeg4_height = mpeg4_height + self.mpeg4_duration = mpeg4_duration + self.title = title + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_mime_type = thumb_mime_type diff --git a/telegram/_inline/inlinequeryresultphoto.py b/telegram/_inline/inlinequeryresultphoto.py index 918dd16a20b..4517e7acfe1 100644 --- a/telegram/_inline/inlinequeryresultphoto.py +++ b/telegram/_inline/inlinequeryresultphoto.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultPhoto.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -52,7 +52,11 @@ class InlineQueryResultPhoto(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the @@ -74,7 +78,12 @@ class InlineQueryResultPhoto(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the @@ -109,22 +118,23 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.PHOTO, id, api_kwargs=api_kwargs) - self.photo_url = photo_url - self.thumb_url = thumb_url + with self._unfrozen(): + self.photo_url = photo_url + self.thumb_url = thumb_url - # Optionals - self.photo_width = photo_width - self.photo_height = photo_height - self.title = title - self.description = description - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optionals + self.photo_width = photo_width + self.photo_height = photo_height + self.title = title + self.description = description + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultvenue.py b/telegram/_inline/inlinequeryresultvenue.py index 6c16808406d..9b6359b456c 100644 --- a/telegram/_inline/inlinequeryresultvenue.py +++ b/telegram/_inline/inlinequeryresultvenue.py @@ -124,18 +124,19 @@ def __init__( # Required super().__init__(InlineQueryResultType.VENUE, id, api_kwargs=api_kwargs) - self.latitude = latitude - self.longitude = longitude - self.title = title - self.address = address + with self._unfrozen(): + self.latitude = latitude + self.longitude = longitude + self.title = title + self.address = address - # Optional - self.foursquare_id = foursquare_id - self.foursquare_type = foursquare_type - self.google_place_id = google_place_id - self.google_place_type = google_place_type - self.reply_markup = reply_markup - self.input_message_content = input_message_content - self.thumb_url = thumb_url - self.thumb_width = thumb_width - self.thumb_height = thumb_height + # Optional + self.foursquare_id = foursquare_id + self.foursquare_type = foursquare_type + self.google_place_id = google_place_id + self.google_place_type = google_place_type + self.reply_markup = reply_markup + self.input_message_content = input_message_content + self.thumb_url = thumb_url + self.thumb_width = thumb_width + self.thumb_height = thumb_height diff --git a/telegram/_inline/inlinequeryresultvideo.py b/telegram/_inline/inlinequeryresultvideo.py index 903865e2e3e..5fd617f7550 100644 --- a/telegram/_inline/inlinequeryresultvideo.py +++ b/telegram/_inline/inlinequeryresultvideo.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultVideo.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -54,7 +54,11 @@ class InlineQueryResultVideo(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + video_width (:obj:`int`, optional): Video width. video_height (:obj:`int`, optional): Video height. video_duration (:obj:`int`, optional): Video duration in seconds. @@ -75,18 +79,24 @@ class InlineQueryResultVideo(InlineQueryResult): mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4". thumb_url (:obj:`str`): URL of the thumbnail (JPEG only) for the video. title (:obj:`str`): Title for the result. - caption (:obj:`str`, optional): Caption of the video to be sent, + caption (:obj:`str`): Optional. Caption of the video to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. - parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| - video_width (:obj:`int`, optional): Video width. - video_height (:obj:`int`, optional): Video height. - video_duration (:obj:`int`, optional): Video duration in seconds. - description (:obj:`str`, optional): Short description of the result. + parse_mode (:obj:`str`): Optional. |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + + video_width (:obj:`int`): Optional. Video width. + video_height (:obj:`int`): Optional. Video height. + video_duration (:obj:`int`): Optional. Video duration in seconds. + description (:obj:`str`): Optional. Short description of the result. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. - input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the message to be sent instead of the video. This field is required if InlineQueryResultVideo is used to send an HTML-page as a result (e.g., a YouTube video). @@ -124,25 +134,26 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.VIDEO, id, api_kwargs=api_kwargs) - self.video_url = video_url - self.mime_type = mime_type - self.thumb_url = thumb_url - self.title = title - - # Optional - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.video_width = video_width - self.video_height = video_height - self.video_duration = video_duration - self.description = description - self.reply_markup = reply_markup - self.input_message_content = input_message_content + with self._unfrozen(): + self.video_url = video_url + self.mime_type = mime_type + self.thumb_url = thumb_url + self.title = title + + # Optional + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.video_width = video_width + self.video_height = video_height + self.video_duration = video_duration + self.description = description + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inlinequeryresultvoice.py b/telegram/_inline/inlinequeryresultvoice.py index a74bfd5d82a..6a4d52109cc 100644 --- a/telegram/_inline/inlinequeryresultvoice.py +++ b/telegram/_inline/inlinequeryresultvoice.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultVoice.""" - -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING, Sequence from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram.constants import InlineQueryResultType @@ -48,7 +48,11 @@ class InlineQueryResultVoice(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + voice_duration (:obj:`int`, optional): Recording duration in seconds. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. @@ -66,7 +70,12 @@ class InlineQueryResultVoice(InlineQueryResult): 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| voice_duration (:obj:`int`): Optional. Recording duration in seconds. reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. @@ -96,20 +105,21 @@ def __init__( reply_markup: InlineKeyboardMarkup = None, input_message_content: "InputMessageContent" = None, parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + caption_entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): # Required super().__init__(InlineQueryResultType.VOICE, id, api_kwargs=api_kwargs) - self.voice_url = voice_url - self.title = title + with self._unfrozen(): + self.voice_url = voice_url + self.title = title - # Optional - self.voice_duration = voice_duration - self.caption = caption - self.parse_mode = parse_mode - self.caption_entities = caption_entities - self.reply_markup = reply_markup - self.input_message_content = input_message_content + # Optional + self.voice_duration = voice_duration + self.caption = caption + self.parse_mode = parse_mode + self.caption_entities = parse_sequence_arg(caption_entities) + self.reply_markup = reply_markup + self.input_message_content = input_message_content diff --git a/telegram/_inline/inputcontactmessagecontent.py b/telegram/_inline/inputcontactmessagecontent.py index 0c675568d25..132d8f85b11 100644 --- a/telegram/_inline/inputcontactmessagecontent.py +++ b/telegram/_inline/inputcontactmessagecontent.py @@ -65,3 +65,5 @@ def __init__( self.vcard = vcard self._id_attrs = (self.phone_number,) + + self._freeze() diff --git a/telegram/_inline/inputinvoicemessagecontent.py b/telegram/_inline/inputinvoicemessagecontent.py index ce65c717162..cfdf0db980a 100644 --- a/telegram/_inline/inputinvoicemessagecontent.py +++ b/telegram/_inline/inputinvoicemessagecontent.py @@ -17,11 +17,11 @@ # 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 a class that represents a Telegram InputInvoiceMessageContent.""" - -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional, Sequence from telegram._inline.inputmessagecontent import InputMessageContent from telegram._payment.labeledprice import LabeledPrice +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -52,20 +52,30 @@ class InputInvoiceMessageContent(InputMessageContent): `@Botfather `_. currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on `currencies `_ - prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a list of + prices (Sequence[:class:`telegram.LabeledPrice`]): Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + + .. versionchanged:: 20.0 + |sequenceclassargs| + max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the *smallest* units of the currency (integer, **not** float/double). For example, for a maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the ``exp`` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to ``0``. - suggested_tip_amounts (List[:obj:`int`], optional): An array of suggested + suggested_tip_amounts (Sequence[:obj:`int`], optional): An array of suggested amounts of tip in the *smallest* units of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed :attr:`max_tip_amount`. + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + provider_data (:obj:`str`, optional): An object for data about the invoice, which will be shared with the payment provider. A detailed description of the required fields should be provided by the payment provider. @@ -104,12 +114,20 @@ class InputInvoiceMessageContent(InputMessageContent): `@Botfather `_. currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on `currencies `_ - prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a list of + prices (Tuple[:class:`telegram.LabeledPrice`]): Price breakdown, a list of components. + + .. versionchanged:: 20.0 + |tupleclassattrs| + max_tip_amount (:obj:`int`): Optional. The maximum accepted amount for tips in the smallest units of the currency (integer, not float/double). - suggested_tip_amounts (List[:obj:`int`]): Optional. An array of suggested + suggested_tip_amounts (Tuple[:obj:`int`]): Optional. An array of suggested amounts of tip in the smallest units of the currency (integer, not float/double). + + .. versionchanged:: 20.0 + |tupleclassattrs| + provider_data (:obj:`str`): Optional. An object for data about the invoice, which will be shared with the payment provider. photo_url (:obj:`str`): Optional. URL of the product photo for the invoice. @@ -163,9 +181,9 @@ def __init__( payload: str, provider_token: str, currency: str, - prices: List[LabeledPrice], + prices: Sequence[LabeledPrice], max_tip_amount: int = None, - suggested_tip_amounts: List[int] = None, + suggested_tip_amounts: Sequence[int] = None, provider_data: str = None, photo_url: str = None, photo_size: int = None, @@ -188,10 +206,10 @@ def __init__( self.payload = payload self.provider_token = provider_token self.currency = currency - self.prices = prices + self.prices = parse_sequence_arg(prices) # Optionals self.max_tip_amount = max_tip_amount - self.suggested_tip_amounts = suggested_tip_amounts + self.suggested_tip_amounts = parse_sequence_arg(suggested_tip_amounts) self.provider_data = provider_data self.photo_url = photo_url self.photo_size = photo_size @@ -214,19 +232,7 @@ def __init__( self.prices, ) - def __hash__(self) -> int: - # we override this as self.prices is a list and not hashable - prices = tuple(self.prices) - return hash( - ( - self.title, - self.description, - self.payload, - self.provider_token, - self.currency, - prices, - ) - ) + self._freeze() @classmethod def de_json( diff --git a/telegram/_inline/inputlocationmessagecontent.py b/telegram/_inline/inputlocationmessagecontent.py index 717749db208..f3806dfae6b 100644 --- a/telegram/_inline/inputlocationmessagecontent.py +++ b/telegram/_inline/inputlocationmessagecontent.py @@ -97,6 +97,8 @@ def __init__( self._id_attrs = (self.latitude, self.longitude) + self._freeze() + HORIZONTAL_ACCURACY: ClassVar[int] = constants.LocationLimit.HORIZONTAL_ACCURACY """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` diff --git a/telegram/_inline/inputtextmessagecontent.py b/telegram/_inline/inputtextmessagecontent.py index cbde72169ba..e2a7825595a 100644 --- a/telegram/_inline/inputtextmessagecontent.py +++ b/telegram/_inline/inputtextmessagecontent.py @@ -17,11 +17,11 @@ # 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 the classes that represent Telegram InputTextMessageContent.""" - -from typing import List, Tuple, Union +from typing import Sequence from telegram._inline.inputmessagecontent import InputMessageContent from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -42,7 +42,11 @@ class InputTextMessageContent(InputMessageContent): :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after entities parsing. parse_mode (:obj:`str`, optional): |parse_mode| - entities (List[:class:`telegram.MessageEntity`], optional): |caption_entities| + entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in the sent message. @@ -51,7 +55,12 @@ class InputTextMessageContent(InputMessageContent): 1-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| - entities (List[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in the sent message. @@ -64,7 +73,7 @@ def __init__( message_text: str, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, - entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, + entities: Sequence[MessageEntity] = None, *, api_kwargs: JSONDict = None, ): @@ -73,7 +82,9 @@ def __init__( self.message_text = message_text # Optionals self.parse_mode = parse_mode - self.entities = entities + self.entities = parse_sequence_arg(entities) self.disable_web_page_preview = disable_web_page_preview self._id_attrs = (self.message_text,) + + self._freeze() diff --git a/telegram/_inline/inputvenuemessagecontent.py b/telegram/_inline/inputvenuemessagecontent.py index b6adeece5f5..b44679bc589 100644 --- a/telegram/_inline/inputvenuemessagecontent.py +++ b/telegram/_inline/inputvenuemessagecontent.py @@ -101,3 +101,5 @@ def __init__( self.longitude, self.title, ) + + self._freeze() diff --git a/telegram/_keyboardbutton.py b/telegram/_keyboardbutton.py index a0511e17e67..0368496b92f 100644 --- a/telegram/_keyboardbutton.py +++ b/telegram/_keyboardbutton.py @@ -107,6 +107,8 @@ def __init__( self.web_app, ) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["KeyboardButton"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_keyboardbuttonpolltype.py b/telegram/_keyboardbuttonpolltype.py index ae78389804a..9a26930d5aa 100644 --- a/telegram/_keyboardbuttonpolltype.py +++ b/telegram/_keyboardbuttonpolltype.py @@ -48,3 +48,5 @@ def __init__( self.type = type self._id_attrs = (self.type,) + + self._freeze() diff --git a/telegram/_loginurl.py b/telegram/_loginurl.py index d71d8dfbdb5..ec2654ee42a 100644 --- a/telegram/_loginurl.py +++ b/telegram/_loginurl.py @@ -88,3 +88,5 @@ def __init__( self.request_write_access = request_write_access self._id_attrs = (self.url,) + + self._freeze() diff --git a/telegram/_menubutton.py b/telegram/_menubutton.py index 4a7a84c0552..ea10434e658 100644 --- a/telegram/_menubutton.py +++ b/telegram/_menubutton.py @@ -62,6 +62,8 @@ def __init__( self._id_attrs = (self.type,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["MenuButton"]: """Converts JSON data to the appropriate :class:`MenuButton` object, i.e. takes @@ -114,6 +116,7 @@ class MenuButtonCommands(MenuButton): def __init__(self, *, api_kwargs: JSONDict = None): super().__init__(type=constants.MenuButtonType.COMMANDS, api_kwargs=api_kwargs) + self._freeze() class MenuButtonWebApp(MenuButton): @@ -144,10 +147,11 @@ class MenuButtonWebApp(MenuButton): def __init__(self, text: str, web_app: WebAppInfo, *, api_kwargs: JSONDict = None): super().__init__(type=constants.MenuButtonType.WEB_APP, api_kwargs=api_kwargs) - self.text = text - self.web_app = web_app + with self._unfrozen(): + self.text = text + self.web_app = web_app - self._id_attrs = (self.type, self.text, self.web_app) + self._id_attrs = (self.type, self.text, self.web_app) @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["MenuButtonWebApp"]: @@ -174,3 +178,4 @@ class MenuButtonDefault(MenuButton): def __init__(self, *, api_kwargs: JSONDict = None): super().__init__(type=constants.MenuButtonType.DEFAULT, api_kwargs=api_kwargs) + self._freeze() diff --git a/telegram/_message.py b/telegram/_message.py index a81be39bd2e..3f84539f4bf 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -21,7 +21,7 @@ import datetime import sys from html import escape -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, Union from telegram._chat import Chat from telegram._dice import Dice @@ -48,6 +48,7 @@ from telegram._proximityalerttriggered import ProximityAlertTriggered from telegram._telegramobject import TelegramObject from telegram._user import User +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.datetime import from_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.types import DVInput, FileInput, JSONDict, ODVInput, ReplyMarkup @@ -140,15 +141,23 @@ class Message(TelegramObject): message belongs to. text (:obj:`str`, optional): For text messages, the actual UTF-8 text of the message, 0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters. - entities (List[:class:`telegram.MessageEntity`], optional): For text messages, special + entities (Sequence[:class:`telegram.MessageEntity`], optional): For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text. See :attr:`parse_entity` and :attr:`parse_entities` methods for how to use properly. This list is empty if the message does not contain entities. - caption_entities (List[:class:`telegram.MessageEntity`], optional): For messages with a + + .. versionchanged:: 20.0 + |sequenceclassargs| + + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): For messages with a Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the caption. See :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` methods for how to use properly. This list is empty if the message does not contain caption entities. + + .. versionchanged:: 20.0 + |sequenceclassargs| + audio (:class:`telegram.Audio`, optional): Message is an audio file, information about the file. document (:class:`telegram.Document`, optional): Message is a general file, information @@ -157,8 +166,12 @@ class Message(TelegramObject): 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. - photo (List[: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. + 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. + + .. versionchanged:: 20.0 + |sequenceclassargs| + sticker (:class:`telegram.Sticker`, optional): Message is a sticker, information about the sticker. video (:class:`telegram.Video`, optional): Message is a video, information about the @@ -167,9 +180,13 @@ class Message(TelegramObject): the file. video_note (:class:`telegram.VideoNote`, optional): Message is a video note, information about the video message. - new_chat_members (List[:class:`telegram.User`], optional): New members that were added to - the group or supergroup and information about them (the bot itself may be one of these - members). This list is empty if the message does not contain new chat members. + new_chat_members (Sequence[:class:`telegram.User`], optional): New members that were added + to the group or supergroup and information about them (the bot itself may be one of + these members). This list is empty if the message does not contain new chat members. + + .. versionchanged:: 20.0 + |sequenceclassargs| + caption (:obj:`str`, optional): Caption for the animation, audio, document, photo, video or voice, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. contact (:class:`telegram.Contact`, optional): Message is a shared contact, information @@ -182,8 +199,12 @@ class Message(TelegramObject): left_chat_member (:class:`telegram.User`, optional): A member was removed from the group, information about them (this member may be the bot itself). new_chat_title (:obj:`str`, optional): A chat title was changed to this value. - new_chat_photo (List[:class:`telegram.PhotoSize`], optional): A chat photo was changed to - this value. This list is empty if the message does not contain a new chat photo. + new_chat_photo (Sequence[:class:`telegram.PhotoSize`], optional): A chat photo was changed + to this value. This list is empty if the message does not contain a new chat photo. + + .. versionchanged:: 20.0 + |sequenceclassargs| + delete_chat_photo (:obj:`bool`, optional): Service message: The chat photo was deleted. group_chat_created (:obj:`bool`, optional): Service message: The group has been created. supergroup_chat_created (:obj:`bool`, optional): Service message: The supergroup has been @@ -309,15 +330,23 @@ class Message(TelegramObject): message belongs to. text (:obj:`str`): Optional. For text messages, the actual UTF-8 text of the message, 0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters. - entities (List[:class:`telegram.MessageEntity`]): Optional. For text messages, special + entities (Tuple[:class:`telegram.MessageEntity`]): Optional. For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text. See :attr:`parse_entity` and :attr:`parse_entities` methods for how to use properly. This list is empty if the message does not contain entities. - caption_entities (List[:class:`telegram.MessageEntity`]): Optional. For messages with a + + .. versionchanged:: 20.0 + |tupleclassattrs| + + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. For messages with a Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the caption. See :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` methods for how to use properly. This list is empty if the message does not contain caption entities. + + .. versionchanged:: 20.0 + |tupleclassattrs| + audio (:class:`telegram.Audio`): Optional. Message is an audio file, information about the file. document (:class:`telegram.Document`): Optional. Message is a general file, information @@ -326,8 +355,12 @@ class Message(TelegramObject): 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. - photo (List[: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. + 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. + + .. versionchanged:: 20.0 + |tupleclassattrs| + sticker (:class:`telegram.Sticker`): Optional. Message is a sticker, information about the sticker. video (:class:`telegram.Video`): Optional. Message is a video, information about the @@ -336,9 +369,13 @@ class Message(TelegramObject): the file. video_note (:class:`telegram.VideoNote`): Optional. Message is a video note, information about the video message. - new_chat_members (List[:class:`telegram.User`]): Optional. New members that were added to - the group or supergroup and information about them (the bot itself may be one of these - members). This list is empty if the message does not contain new chat members. + new_chat_members (Tuple[:class:`telegram.User`]): Optional. New members that were added + to the group or supergroup and information about them (the bot itself may be one of + these members). This list is empty if the message does not contain new chat members. + + .. versionchanged:: 20.0 + |tupleclassattrs| + caption (:obj:`str`): Optional. Caption for the animation, audio, document, photo, video or voice, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. contact (:class:`telegram.Contact`): Optional. Message is a shared contact, information @@ -351,8 +388,12 @@ class Message(TelegramObject): left_chat_member (:class:`telegram.User`): Optional. A member was removed from the group, information about them (this member may be the bot itself). new_chat_title (:obj:`str`): Optional. A chat title was changed to this value. - new_chat_photo (List[:class:`telegram.PhotoSize`]): A chat photo was changed to + new_chat_photo (Tuple[:class:`telegram.PhotoSize`]): A chat photo was changed to this value. This list is empty if the message does not contain a new chat photo. + + .. versionchanged:: 20.0 + |tupleclassattrs| + delete_chat_photo (:obj:`bool`): Optional. Service message: The chat photo was deleted. group_chat_created (:obj:`bool`): Optional. Service message: The group has been created. supergroup_chat_created (:obj:`bool`): Optional. Service message: The supergroup has been @@ -528,24 +569,24 @@ def __init__( reply_to_message: "Message" = None, edit_date: datetime.datetime = None, text: str = None, - entities: List["MessageEntity"] = None, - caption_entities: List["MessageEntity"] = None, + entities: Sequence["MessageEntity"] = None, + caption_entities: Sequence["MessageEntity"] = None, audio: Audio = None, document: Document = None, game: Game = None, - photo: List[PhotoSize] = None, + photo: Sequence[PhotoSize] = None, sticker: Sticker = None, video: Video = None, voice: Voice = None, video_note: VideoNote = None, - new_chat_members: List[User] = None, + new_chat_members: Sequence[User] = None, caption: str = None, contact: Contact = None, location: Location = None, venue: Venue = None, left_chat_member: User = None, new_chat_title: str = None, - new_chat_photo: List[PhotoSize] = None, + new_chat_photo: Sequence[PhotoSize] = None, delete_chat_photo: bool = None, group_chat_created: bool = None, supergroup_chat_created: bool = None, @@ -601,12 +642,12 @@ def __init__( self.edit_date = edit_date self.has_protected_content = has_protected_content self.text = text - self.entities = entities or [] - self.caption_entities = caption_entities or [] + self.entities = parse_sequence_arg(entities) + self.caption_entities = parse_sequence_arg(caption_entities) self.audio = audio self.game = game self.document = document - self.photo = photo or [] + self.photo = parse_sequence_arg(photo) self.sticker = sticker self.video = video self.voice = voice @@ -615,10 +656,10 @@ def __init__( self.contact = contact self.location = location self.venue = venue - self.new_chat_members = new_chat_members or [] + self.new_chat_members = parse_sequence_arg(new_chat_members) self.left_chat_member = left_chat_member self.new_chat_title = new_chat_title - self.new_chat_photo = new_chat_photo or [] + self.new_chat_photo = parse_sequence_arg(new_chat_photo) self.delete_chat_photo = bool(delete_chat_photo) self.group_chat_created = bool(group_chat_created) self.supergroup_chat_created = bool(supergroup_chat_created) @@ -657,6 +698,8 @@ def __init__( self._id_attrs = (self.message_id, self.chat) + self._freeze() + @property def chat_id(self) -> int: """:obj:`int`: Shortcut for :attr:`telegram.Chat.id` for :attr:`chat`.""" @@ -1095,7 +1138,7 @@ async def reply_media_group( caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, - ) -> List["Message"]: + ) -> Tuple["Message", ...]: """Shortcut for:: await bot.send_media_group(update.effective_message.chat_id, *args, **kwargs) @@ -1109,7 +1152,7 @@ async def reply_media_group( chats. Returns: - List[:class:`telegram.Message`]: An array of the sent Messages. + Tuple[:class:`telegram.Message`]: An array of the sent Messages. Raises: :class:`telegram.error.TelegramError` @@ -2596,7 +2639,7 @@ async def get_game_high_scores( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List["GameHighScore"]: + ) -> Tuple["GameHighScore", ...]: """Shortcut for:: await bot.get_game_high_scores( @@ -2612,7 +2655,7 @@ async def get_game_high_scores( behaviour is undocumented and might be changed by Telegram. Returns: - List[:class:`telegram.GameHighScore`] + Tuple[:class:`telegram.GameHighScore`] """ return await self.get_bot().get_game_high_scores( chat_id=self.chat_id, diff --git a/telegram/_messageautodeletetimerchanged.py b/telegram/_messageautodeletetimerchanged.py index a6e6e67e6cf..085acb346c2 100644 --- a/telegram/_messageautodeletetimerchanged.py +++ b/telegram/_messageautodeletetimerchanged.py @@ -54,3 +54,5 @@ def __init__( self.message_auto_delete_time = message_auto_delete_time self._id_attrs = (self.message_auto_delete_time,) + + self._freeze() diff --git a/telegram/_messageentity.py b/telegram/_messageentity.py index 72438b1aa1a..d2d83b25901 100644 --- a/telegram/_messageentity.py +++ b/telegram/_messageentity.py @@ -115,6 +115,8 @@ def __init__( self._id_attrs = (self.type, self.offset, self.length) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["MessageEntity"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_messageid.py b/telegram/_messageid.py index 42db325bff6..e870e97d2a6 100644 --- a/telegram/_messageid.py +++ b/telegram/_messageid.py @@ -42,3 +42,5 @@ def __init__(self, message_id: int, *, api_kwargs: JSONDict = None): self.message_id = message_id self._id_attrs = (self.message_id,) + + self._freeze() diff --git a/telegram/_passport/credentials.py b/telegram/_passport/credentials.py index 8f8c2b85986..d2d86c24604 100644 --- a/telegram/_passport/credentials.py +++ b/telegram/_passport/credentials.py @@ -19,7 +19,7 @@ # pylint: disable=missing-module-docstring, redefined-builtin import json from base64 import b64decode -from typing import TYPE_CHECKING, List, Optional, no_type_check +from typing import TYPE_CHECKING, Optional, Sequence, no_type_check try: from cryptography.hazmat.backends import default_backend @@ -38,6 +38,7 @@ CRYPTO_INSTALLED = False from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict from telegram.error import PassportDecryptionError @@ -155,6 +156,8 @@ def __init__( self._decrypted_secret: Optional[str] = None self._decrypted_data: Optional["Credentials"] = None + self._freeze() + @property def decrypted_secret(self) -> str: """ @@ -226,6 +229,8 @@ def __init__( self.secure_data = secure_data self.nonce = nonce + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Credentials"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -312,6 +317,8 @@ def __init__( self.passport = passport self.personal_details = personal_details + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SecureData"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -356,14 +363,23 @@ class SecureValue(TelegramObject): selfie (:class:`telegram.FileCredentials`): Optional. Credentials for encrypted selfie of the user with a document. Can be available for "passport", "driver_license", "identity_card" and "internal_passport". - translation (List[:class:`telegram.FileCredentials`]): Optional. Credentials for an + translation (Tuple[:class:`telegram.FileCredentials`]): Optional. Credentials for an encrypted translation of the document. Available for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration". - files (List[:class:`telegram.FileCredentials`]): Optional. Credentials for encrypted + + .. versionchanged:: 20.0 + |tupleclassattrs| + + files (Tuple[:class:`telegram.FileCredentials`]): Optional. Credentials for encrypted files. Available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + """ __slots__ = ("data", "front_side", "reverse_side", "selfie", "files", "translation") @@ -374,8 +390,8 @@ def __init__( front_side: "FileCredentials" = None, reverse_side: "FileCredentials" = None, selfie: "FileCredentials" = None, - files: List["FileCredentials"] = None, - translation: List["FileCredentials"] = None, + files: Sequence["FileCredentials"] = None, + translation: Sequence["FileCredentials"] = None, *, api_kwargs: JSONDict = None, ): @@ -384,8 +400,10 @@ def __init__( self.front_side = front_side self.reverse_side = reverse_side self.selfie = selfie - self.files = files - self.translation = translation + self.files = parse_sequence_arg(files) + self.translation = parse_sequence_arg(translation) + + self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SecureValue"]: @@ -414,12 +432,13 @@ def __init__( self, hash: str, secret: str, *, api_kwargs: JSONDict = None # skipcq: PYL-W0622 ): super().__init__(api_kwargs=api_kwargs) - self.hash = hash - self.secret = secret + with self._unfrozen(): + self.hash = hash + self.secret = secret - # Aliases just to be sure - self.file_hash = self.hash - self.data_hash = self.hash + # Aliases just to be sure + self.file_hash = self.hash + self.data_hash = self.hash class DataCredentials(_CredentialsBase): @@ -440,6 +459,7 @@ class DataCredentials(_CredentialsBase): def __init__(self, data_hash: str, secret: str, *, api_kwargs: JSONDict = None): super().__init__(hash=data_hash, secret=secret, api_kwargs=api_kwargs) + self._freeze() class FileCredentials(_CredentialsBase): @@ -460,3 +480,4 @@ class FileCredentials(_CredentialsBase): def __init__(self, file_hash: str, secret: str, *, api_kwargs: JSONDict = None): super().__init__(hash=file_hash, secret=secret, api_kwargs=api_kwargs) + self._freeze() diff --git a/telegram/_passport/data.py b/telegram/_passport/data.py index f256f273dac..ff015214644 100644 --- a/telegram/_passport/data.py +++ b/telegram/_passport/data.py @@ -84,6 +84,8 @@ def __init__( self.last_name_native = last_name_native self.middle_name_native = middle_name_native + self._freeze() + class ResidentialAddress(TelegramObject): """ @@ -127,6 +129,8 @@ def __init__( self.country_code = country_code self.post_code = post_code + self._freeze() + class IdDocumentData(TelegramObject): """ @@ -149,3 +153,5 @@ def __init__( super().__init__(api_kwargs=api_kwargs) self.document_no = document_no self.expiry_date = expiry_date + + self._freeze() diff --git a/telegram/_passport/encryptedpassportelement.py b/telegram/_passport/encryptedpassportelement.py index 5b460c1343f..6e03c7cc760 100644 --- a/telegram/_passport/encryptedpassportelement.py +++ b/telegram/_passport/encryptedpassportelement.py @@ -18,12 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram EncryptedPassportElement.""" from base64 import b64decode -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional, Sequence from telegram._passport.credentials import decrypt_json from telegram._passport.data import IdDocumentData, PersonalDetails, ResidentialAddress from telegram._passport.passportfile import PassportFile from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -58,9 +59,14 @@ class EncryptedPassportElement(TelegramObject): "phone_number" type. email (:obj:`str`, optional): User's verified email address, available only for "email" type. - files (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files + files (Sequence[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted + files with documents provided by the user, available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. + + .. versionchanged:: 20.0 + |sequenceclassargs| + front_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the front side of the document, provided by the user. Available for "passport", "driver_license", "identity_card" and "internal_passport". @@ -70,12 +76,16 @@ class EncryptedPassportElement(TelegramObject): selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the selfie of the user holding a document, provided by the user; available for "passport", "driver_license", "identity_card" and "internal_passport". - translation (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted + translation (Sequence[:class:`telegram.PassportFile`], optional): Array of + encrypted/decrypted files with translated versions of documents provided by the user. Available if requested for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. + .. versionchanged:: 20.0 + |sequenceclassargs| + Attributes: type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license", "identity_card", "internal_passport", "address", "utility_bill", "bank_statement", @@ -91,9 +101,16 @@ class EncryptedPassportElement(TelegramObject): "phone_number" type. email (:obj:`str`): Optional. User's verified email address, available only for "email" type. - files (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files + files (Tuple[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted + files with documents provided by the user, available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + front_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the front side of the document, provided by the user. Available for "passport", "driver_license", "identity_card" and "internal_passport". @@ -103,12 +120,18 @@ class EncryptedPassportElement(TelegramObject): selfie (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the selfie of the user holding a document, provided by the user; available for "passport", "driver_license", "identity_card" and "internal_passport". - translation (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted + translation (Tuple[:class:`telegram.PassportFile`]): Optional. Array of + encrypted/decrypted files with translated versions of documents provided by the user. Available if requested for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + """ __slots__ = ( @@ -131,11 +154,11 @@ def __init__( data: PersonalDetails = None, phone_number: str = None, email: str = None, - files: List[PassportFile] = None, + files: Sequence[PassportFile] = None, front_side: PassportFile = None, reverse_side: PassportFile = None, selfie: PassportFile = None, - translation: List[PassportFile] = None, + translation: Sequence[PassportFile] = None, credentials: "Credentials" = None, # pylint: disable=unused-argument *, api_kwargs: JSONDict = None, @@ -148,11 +171,11 @@ def __init__( self.data = data self.phone_number = phone_number self.email = email - self.files = files + self.files = parse_sequence_arg(files) self.front_side = front_side self.reverse_side = reverse_side self.selfie = selfie - self.translation = translation + self.translation = parse_sequence_arg(translation) self.hash = hash self._id_attrs = ( @@ -166,6 +189,8 @@ def __init__( self.selfie, ) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["EncryptedPassportElement"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_passport/passportdata.py b/telegram/_passport/passportdata.py index f4508a1b877..35fcf8e8aa4 100644 --- a/telegram/_passport/passportdata.py +++ b/telegram/_passport/passportdata.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """Contains information about Telegram Passport data shared with the bot by the user.""" - -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional, Sequence, Tuple from telegram._passport.credentials import EncryptedCredentials from telegram._passport.encryptedpassportelement import EncryptedPassportElement from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -39,13 +39,23 @@ class PassportData(TelegramObject): attribute :attr:`telegram.Credentials.nonce`. Args: - data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information - about documents and other Telegram Passport elements that was shared with the bot. + data (Sequence[:class:`telegram.EncryptedPassportElement`]): Array with encrypted + information about documents and other Telegram Passport elements that was shared with + the bot. + + .. versionchanged:: 20.0 + |sequenceclassargs| + credentials (:class:`telegram.EncryptedCredentials`)): Encrypted credentials. Attributes: - data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information - about documents and other Telegram Passport elements that was shared with the bot. + data (Tuple[:class:`telegram.EncryptedPassportElement`]): Array with encrypted + information about documents and other Telegram Passport elements that was shared with + the bot. + + .. versionchanged:: 20.0 + |tupleclassattrs| + credentials (:class:`telegram.EncryptedCredentials`): Encrypted credentials. @@ -55,19 +65,21 @@ class PassportData(TelegramObject): def __init__( self, - data: List[EncryptedPassportElement], + data: Sequence[EncryptedPassportElement], credentials: EncryptedCredentials, *, api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) - self.data = data + self.data = parse_sequence_arg(data) self.credentials = credentials - self._decrypted_data: Optional[List[EncryptedPassportElement]] = None + self._decrypted_data: Optional[Tuple[EncryptedPassportElement]] = None self._id_attrs = tuple([x.type for x in data] + [credentials.hash]) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PassportData"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -82,23 +94,26 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PassportData return super().de_json(data=data, bot=bot) @property - def decrypted_data(self) -> List[EncryptedPassportElement]: + def decrypted_data(self) -> Tuple[EncryptedPassportElement, ...]: """ - List[:class:`telegram.EncryptedPassportElement`]: Lazily decrypt and return information + Tuple[:class:`telegram.EncryptedPassportElement`]: Lazily decrypt and return information about documents and other Telegram Passport elements which were shared with the bot. + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + Raises: telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ if self._decrypted_data is None: - self._decrypted_data = [ - EncryptedPassportElement.de_json_decrypted( # type: ignore[misc] + self._decrypted_data = tuple( # type: ignore[assignment] + EncryptedPassportElement.de_json_decrypted( element.to_dict(), self.get_bot(), self.decrypted_credentials ) for element in self.data - ] - return self._decrypted_data + ) + return self._decrypted_data # type: ignore[return-value] @property def decrypted_credentials(self) -> "Credentials": diff --git a/telegram/_passport/passportelementerrors.py b/telegram/_passport/passportelementerrors.py index d61fc62f545..58ce566a7c8 100644 --- a/telegram/_passport/passportelementerrors.py +++ b/telegram/_passport/passportelementerrors.py @@ -54,6 +54,8 @@ def __init__(self, source: str, type: str, message: str, *, api_kwargs: JSONDict self._id_attrs = (self.source, self.type) + self._freeze() + class PassportElementErrorDataField(PassportElementError): """ @@ -95,10 +97,17 @@ def __init__( ): # Required super().__init__("data", type, message, api_kwargs=api_kwargs) - self.field_name = field_name - self.data_hash = data_hash + with self._unfrozen(): + self.field_name = field_name + self.data_hash = data_hash - self._id_attrs = (self.source, self.type, self.field_name, self.data_hash, self.message) + self._id_attrs = ( + self.source, + self.type, + self.field_name, + self.data_hash, + self.message, + ) class PassportElementErrorFile(PassportElementError): @@ -131,9 +140,10 @@ class PassportElementErrorFile(PassportElementError): def __init__(self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict = None): # Required super().__init__("file", type, message, api_kwargs=api_kwargs) - self.file_hash = file_hash + with self._unfrozen(): + self.file_hash = file_hash - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorFiles(PassportElementError): @@ -166,9 +176,10 @@ class PassportElementErrorFiles(PassportElementError): def __init__(self, type: str, file_hashes: str, message: str, *, api_kwargs: JSONDict = None): # Required super().__init__("files", type, message, api_kwargs=api_kwargs) - self.file_hashes = file_hashes + with self._unfrozen(): + self.file_hashes = file_hashes - self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes) + self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes) class PassportElementErrorFrontSide(PassportElementError): @@ -201,9 +212,10 @@ class PassportElementErrorFrontSide(PassportElementError): def __init__(self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict = None): # Required super().__init__("front_side", type, message, api_kwargs=api_kwargs) - self.file_hash = file_hash + with self._unfrozen(): + self.file_hash = file_hash - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorReverseSide(PassportElementError): @@ -236,9 +248,10 @@ class PassportElementErrorReverseSide(PassportElementError): def __init__(self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict = None): # Required super().__init__("reverse_side", type, message, api_kwargs=api_kwargs) - self.file_hash = file_hash + with self._unfrozen(): + self.file_hash = file_hash - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorSelfie(PassportElementError): @@ -269,9 +282,10 @@ class PassportElementErrorSelfie(PassportElementError): def __init__(self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict = None): # Required super().__init__("selfie", type, message, api_kwargs=api_kwargs) - self.file_hash = file_hash + with self._unfrozen(): + self.file_hash = file_hash - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorTranslationFile(PassportElementError): @@ -306,9 +320,10 @@ class PassportElementErrorTranslationFile(PassportElementError): def __init__(self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict = None): # Required super().__init__("translation_file", type, message, api_kwargs=api_kwargs) - self.file_hash = file_hash + with self._unfrozen(): + self.file_hash = file_hash - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorTranslationFiles(PassportElementError): @@ -343,9 +358,10 @@ class PassportElementErrorTranslationFiles(PassportElementError): def __init__(self, type: str, file_hashes: str, message: str, *, api_kwargs: JSONDict = None): # Required super().__init__("translation_files", type, message, api_kwargs=api_kwargs) - self.file_hashes = file_hashes + with self._unfrozen(): + self.file_hashes = file_hashes - self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes) + self._id_attrs = (self.source, self.type, self.message) + tuple(file_hashes) class PassportElementErrorUnspecified(PassportElementError): @@ -374,6 +390,7 @@ class PassportElementErrorUnspecified(PassportElementError): def __init__(self, type: str, element_hash: str, message: str, *, api_kwargs: JSONDict = None): # Required super().__init__("unspecified", type, message, api_kwargs=api_kwargs) - self.element_hash = element_hash + with self._unfrozen(): + self.element_hash = element_hash - self._id_attrs = (self.source, self.type, self.element_hash, self.message) + self._id_attrs = (self.source, self.type, self.element_hash, self.message) diff --git a/telegram/_passport/passportfile.py b/telegram/_passport/passportfile.py index 41586483670..376d4470596 100644 --- a/telegram/_passport/passportfile.py +++ b/telegram/_passport/passportfile.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Encrypted PassportFile.""" -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, List, Optional, Tuple from telegram._telegramobject import TelegramObject from telegram._utils.defaultvalue import DEFAULT_NONE @@ -87,6 +87,8 @@ def __init__( self._id_attrs = (self.file_unique_id,) + self._freeze() + @classmethod def de_json_decrypted( cls, data: Optional[JSONDict], bot: "Bot", credentials: "FileCredentials" @@ -115,26 +117,35 @@ def de_json_decrypted( @classmethod def de_list_decrypted( cls, data: Optional[List[JSONDict]], bot: "Bot", credentials: List["FileCredentials"] - ) -> List[Optional["PassportFile"]]: + ) -> Tuple[Optional["PassportFile"], ...]: """Variant of :meth:`telegram.TelegramObject.de_list` that also takes into account passport credentials. + .. versionchanged:: 20.0 + + * Returns a tuple instead of a list. + * Filters out any :obj:`None` values + Args: - data (Dict[:obj:`str`, ...]): The JSON data. + data (List[Dict[:obj:`str`, ...]]): The JSON data. bot (:class:`telegram.Bot`): The bot associated with these objects. credentials (:class:`telegram.FileCredentials`): The credentials Returns: - List[:class:`telegram.PassportFile`]: + Tuple[:class:`telegram.PassportFile`]: """ if not data: - return [] - - return [ - cls.de_json_decrypted(passport_file, bot, credentials[i]) - for i, passport_file in enumerate(data) - ] + return () + + return tuple( + obj + for obj in ( + cls.de_json_decrypted(passport_file, bot, credentials[i]) + for i, passport_file in enumerate(data) + ) + if obj is not None + ) async def get_file( self, diff --git a/telegram/_payment/invoice.py b/telegram/_payment/invoice.py index bdc325d1e66..22bc17e7abe 100644 --- a/telegram/_payment/invoice.py +++ b/telegram/_payment/invoice.py @@ -87,6 +87,8 @@ def __init__( self.total_amount, ) + self._freeze() + MIN_TITLE_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_TITLE_LENGTH """:const:`telegram.constants.InvoiceLimit.MIN_TITLE_LENGTH` diff --git a/telegram/_payment/labeledprice.py b/telegram/_payment/labeledprice.py index 91811ca103c..ca3e295d08f 100644 --- a/telegram/_payment/labeledprice.py +++ b/telegram/_payment/labeledprice.py @@ -54,3 +54,5 @@ def __init__(self, label: str, amount: int, *, api_kwargs: JSONDict = None): self.amount = amount self._id_attrs = (self.label, self.amount) + + self._freeze() diff --git a/telegram/_payment/orderinfo.py b/telegram/_payment/orderinfo.py index c402ad3e5bb..0e7ec30bcf0 100644 --- a/telegram/_payment/orderinfo.py +++ b/telegram/_payment/orderinfo.py @@ -68,6 +68,8 @@ def __init__( self._id_attrs = (self.name, self.phone_number, self.email, self.shipping_address) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["OrderInfo"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_payment/precheckoutquery.py b/telegram/_payment/precheckoutquery.py index af009beeb6e..fbea2ebcda5 100644 --- a/telegram/_payment/precheckoutquery.py +++ b/telegram/_payment/precheckoutquery.py @@ -100,6 +100,8 @@ def __init__( self._id_attrs = (self.id,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PreCheckoutQuery"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_payment/shippingaddress.py b/telegram/_payment/shippingaddress.py index 9f1f4d8b897..207189ca115 100644 --- a/telegram/_payment/shippingaddress.py +++ b/telegram/_payment/shippingaddress.py @@ -83,3 +83,5 @@ def __init__( self.street_line2, self.post_code, ) + + self._freeze() diff --git a/telegram/_payment/shippingoption.py b/telegram/_payment/shippingoption.py index e4275ad1ca5..3f53bdcbf06 100644 --- a/telegram/_payment/shippingoption.py +++ b/telegram/_payment/shippingoption.py @@ -17,10 +17,10 @@ # 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 ShippingOption.""" - -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Sequence from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -39,12 +39,18 @@ class ShippingOption(TelegramObject): Args: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. - prices (List[:class:`telegram.LabeledPrice`]): List of price portions. + prices (Sequence[:class:`telegram.LabeledPrice`]): List of price portions. + + .. versionchanged:: 20.0 + |sequenceclassargs| Attributes: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. - prices (List[:class:`telegram.LabeledPrice`]): List of price portions. + prices (Tuple[:class:`telegram.LabeledPrice`]): List of price portions. + + .. versionchanged:: 20.0 + |tupleclassattrs| """ @@ -54,7 +60,7 @@ def __init__( self, id: str, # pylint: disable=redefined-builtin title: str, - prices: List["LabeledPrice"], + prices: Sequence["LabeledPrice"], *, api_kwargs: JSONDict = None, ): @@ -62,6 +68,8 @@ def __init__( self.id = id # pylint: disable=invalid-name self.title = title - self.prices = prices + self.prices = parse_sequence_arg(prices) self._id_attrs = (self.id,) + + self._freeze() diff --git a/telegram/_payment/shippingquery.py b/telegram/_payment/shippingquery.py index 09d9892c697..1bcee1f83b2 100644 --- a/telegram/_payment/shippingquery.py +++ b/telegram/_payment/shippingquery.py @@ -74,6 +74,8 @@ def __init__( self._id_attrs = (self.id,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ShippingQuery"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_payment/successfulpayment.py b/telegram/_payment/successfulpayment.py index cfbd7fd01fd..3cc30af7fd3 100644 --- a/telegram/_payment/successfulpayment.py +++ b/telegram/_payment/successfulpayment.py @@ -95,6 +95,8 @@ def __init__( self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SuccessfulPayment"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_poll.py b/telegram/_poll.py index 87feee5a3e8..b9d8eb73a42 100644 --- a/telegram/_poll.py +++ b/telegram/_poll.py @@ -17,16 +17,16 @@ # 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 Poll.""" - import datetime import sys -from typing import TYPE_CHECKING, ClassVar, Dict, List, Optional +from typing import TYPE_CHECKING, ClassVar, Dict, List, Optional, Sequence from telegram import constants from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.datetime import from_timestamp from telegram._utils.types import JSONDict @@ -64,6 +64,8 @@ def __init__(self, text: str, voter_count: int, *, api_kwargs: JSONDict = None): self._id_attrs = (self.text, self.voter_count) + self._freeze() + MIN_LENGTH: ClassVar[int] = constants.PollLimit.MIN_OPTION_LENGTH """:const:`telegram.constants.PollLimit.MIN_OPTION_LENGTH` @@ -86,28 +88,37 @@ class PollAnswer(TelegramObject): Args: poll_id (:obj:`str`): Unique poll identifier. user (:class:`telegram.User`): The user, who changed the answer to the poll. - option_ids (List[:obj:`int`]): 0-based identifiers of answer options, chosen by the user. - May be empty if the user retracted their vote. + option_ids (Sequence[:obj:`int`]): 0-based identifiers of answer options, chosen by the + user. May be empty if the user retracted their vote. + + .. versionchanged:: 20.0 + |sequenceclassargs| Attributes: poll_id (:obj:`str`): Unique poll identifier. user (:class:`telegram.User`): The user, who changed the answer to the poll. - option_ids (List[:obj:`int`]): Identifiers of answer options, chosen by the user. + option_ids (Tuple[:obj:`int`]): Identifiers of answer options, chosen by the user. May be + empty if the user retracted their vote. + + .. versionchanged:: 20.0 + |tupleclassattrs| """ __slots__ = ("option_ids", "user", "poll_id") def __init__( - self, poll_id: str, user: User, option_ids: List[int], *, api_kwargs: JSONDict = None + self, poll_id: str, user: User, option_ids: Sequence[int], *, api_kwargs: JSONDict = None ): super().__init__(api_kwargs=api_kwargs) self.poll_id = poll_id self.user = user - self.option_ids = option_ids + self.option_ids = parse_sequence_arg(option_ids) self._id_attrs = (self.poll_id, self.user, tuple(self.option_ids)) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PollAnswer"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -135,7 +146,10 @@ class Poll(TelegramObject): id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`- :tg-const:`telegram.Poll.MAX_QUESTION_LENGTH` characters. - options (List[:class:`PollOption`]): List of poll options. + options (Sequence[:class:`PollOption`]): List of poll options. + + .. versionchanged:: 20.0 + |sequenceclassargs| is_closed (:obj:`bool`): :obj:`True`, if the poll is closed. is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous. type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`. @@ -146,12 +160,15 @@ class Poll(TelegramObject): explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters. - explanation_entities (List[:class:`telegram.MessageEntity`], optional): Special entities - like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. - This list is empty if the message does not contain explanation entities. + explanation_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special + entities like usernames, URLs, bot commands, etc. that appear in the + :attr:`explanation`. This list is empty if the message does not contain explanation + entities. .. versionchanged:: 20.0 - This attribute is now always a (possibly empty) list and never :obj:`None`. + + * This attribute is now always a (possibly empty) list and never :obj:`None`. + * |sequenceclassargs| open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active after creation. close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the @@ -161,7 +178,10 @@ class Poll(TelegramObject): id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`- :tg-const:`telegram.Poll.MAX_QUESTION_LENGTH` characters. - options (List[:class:`PollOption`]): List of poll options. + options (Tuple[:class:`PollOption`]): List of poll options. + + .. versionchanged:: 20.0 + |tupleclassattrs| total_voter_count (:obj:`int`): Total number of users that voted in the poll. is_closed (:obj:`bool`): :obj:`True`, if the poll is closed. is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous. @@ -173,10 +193,13 @@ class Poll(TelegramObject): explanation (:obj:`str`): Optional. Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters. - explanation_entities (List[:class:`telegram.MessageEntity`]): Special entities + explanation_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. This list is empty if the message does not contain explanation entities. + .. versionchanged:: 20.0 + |tupleclassattrs| + .. versionchanged:: 20.0 This attribute is now always a (possibly empty) list and never :obj:`None`. open_period (:obj:`int`): Optional. Amount of time in seconds the poll will be active @@ -206,7 +229,7 @@ def __init__( self, id: str, # pylint: disable=redefined-builtin question: str, - options: List[PollOption], + options: Sequence[PollOption], total_voter_count: int, is_closed: bool, is_anonymous: bool, @@ -214,7 +237,7 @@ def __init__( allows_multiple_answers: bool, correct_option_id: int = None, explanation: str = None, - explanation_entities: List[MessageEntity] = None, + explanation_entities: Sequence[MessageEntity] = None, open_period: int = None, close_date: datetime.datetime = None, *, @@ -223,7 +246,7 @@ def __init__( super().__init__(api_kwargs=api_kwargs) self.id = id # pylint: disable=invalid-name self.question = question - self.options = options + self.options = parse_sequence_arg(options) self.total_voter_count = total_voter_count self.is_closed = is_closed self.is_anonymous = is_anonymous @@ -231,12 +254,14 @@ def __init__( self.allows_multiple_answers = allows_multiple_answers self.correct_option_id = correct_option_id self.explanation = explanation - self.explanation_entities = explanation_entities or [] + self.explanation_entities = parse_sequence_arg(explanation_entities) self.open_period = open_period self.close_date = close_date self._id_attrs = (self.id,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Poll"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_proximityalerttriggered.py b/telegram/_proximityalerttriggered.py index b19d76fa758..8c31e2025f2 100644 --- a/telegram/_proximityalerttriggered.py +++ b/telegram/_proximityalerttriggered.py @@ -59,6 +59,8 @@ def __init__( self._id_attrs = (self.traveler, self.watcher, self.distance) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ProximityAlertTriggered"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_replykeyboardmarkup.py b/telegram/_replykeyboardmarkup.py index ca391f4bf66..2a481c8e6a8 100644 --- a/telegram/_replykeyboardmarkup.py +++ b/telegram/_replykeyboardmarkup.py @@ -41,8 +41,8 @@ class ReplyKeyboardMarkup(TelegramObject): * :any:`Conversation Bot 2 ` Args: - keyboard (List[List[:obj:`str` | :class:`telegram.KeyboardButton`]]): Array of button rows, - each represented by an Array of :class:`telegram.KeyboardButton` objects. + keyboard (Sequence[Sequence[:obj:`str` | :class:`telegram.KeyboardButton`]]): Array of + button rows, each represented by an Array of :class:`telegram.KeyboardButton` objects. resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is always of the @@ -70,7 +70,8 @@ class ReplyKeyboardMarkup(TelegramObject): .. versionadded:: 13.7 Attributes: - keyboard (List[List[:class:`telegram.KeyboardButton` | :obj:`str`]]): Array of button rows. + keyboard (Tuple[Tuple[:class:`telegram.KeyboardButton` | :obj:`str`]]): Array of button + rows. resize_keyboard (:obj:`bool`): Optional. Requests clients to resize the keyboard. one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as it's been used. @@ -103,20 +104,15 @@ def __init__( super().__init__(api_kwargs=api_kwargs) if not check_keyboard_type(keyboard): raise ValueError( - "The parameter `keyboard` should be a list of list of " + "The parameter `keyboard` should be a sequence of sequences of " "strings or KeyboardButtons" ) # Required - self.keyboard = [] - for row in keyboard: - button_row = [] - for button in row: - if isinstance(button, KeyboardButton): - button_row.append(button) # telegram.KeyboardButton - else: - button_row.append(KeyboardButton(button)) # str - self.keyboard.append(button_row) + self.keyboard = tuple( + tuple(KeyboardButton(button) if isinstance(button, str) else button for button in row) + for row in keyboard + ) # Optionals self.resize_keyboard = resize_keyboard @@ -126,6 +122,8 @@ def __init__( self._id_attrs = (self.keyboard,) + self._freeze() + @classmethod def from_button( cls, @@ -282,16 +280,6 @@ def from_column( **kwargs, # type: ignore[arg-type] ) - def __hash__(self) -> int: - return hash( - ( - tuple(tuple(button for button in row) for row in self.keyboard), - self.resize_keyboard, - self.one_time_keyboard, - self.selective, - ) - ) - MIN_INPUT_FIELD_PLACEHOLDER: ClassVar[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER """:const:`telegram.constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER` diff --git a/telegram/_replykeyboardremove.py b/telegram/_replykeyboardremove.py index 7765c37f774..7464830f65c 100644 --- a/telegram/_replykeyboardremove.py +++ b/telegram/_replykeyboardremove.py @@ -63,3 +63,5 @@ def __init__(self, selective: bool = None, *, api_kwargs: JSONDict = None): self.remove_keyboard = True # Optionals self.selective = selective + + self._freeze() diff --git a/telegram/_sentwebappmessage.py b/telegram/_sentwebappmessage.py index 4b8e4fbb968..19876664a47 100644 --- a/telegram/_sentwebappmessage.py +++ b/telegram/_sentwebappmessage.py @@ -49,3 +49,5 @@ def __init__(self, inline_message_id: str = None, *, api_kwargs: JSONDict = None self.inline_message_id = inline_message_id self._id_attrs = (self.inline_message_id,) + + self._freeze() diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index 6ca8c8af36b..a15e1d79c86 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -20,16 +20,20 @@ import datetime import inspect import json +from collections.abc import Sized +from contextlib import contextmanager from copy import deepcopy from itertools import chain +from types import MappingProxyType from typing import ( TYPE_CHECKING, + Any, Dict, Iterator, List, + Mapping, Optional, Set, - Sized, Tuple, Type, TypeVar, @@ -61,6 +65,9 @@ class TelegramObject: * String representations objects of this type was overhauled. See :meth:`__repr__` for details. As this class doesn't implement :meth:`object.__str__`, the default implementation will be used, which is equivalent to :meth:`__repr__`. + * Objects of this class (or subclasses) are now immutable. This means that you can't set + or delete attributes anymore. Moreover, attributes that were formerly of type + :obj:`list` are now of type :obj:`tuple`. Arguments: api_kwargs (Dict[:obj:`str`, any], optional): |toapikwargsarg| @@ -68,13 +75,13 @@ class TelegramObject: .. versionadded:: 20.0 Attributes: - api_kwargs (Dict[:obj:`str`, any]): |toapikwargsattr| + api_kwargs (:obj:`types.MappingProxyType` [:obj:`str`, any]): |toapikwargsattr| .. versionadded:: 20.0 """ - __slots__ = ("_id_attrs", "_bot", "api_kwargs") + __slots__ = ("_id_attrs", "_bot", "_frozen", "api_kwargs") # Used to cache the names of the parameters of the __init__ method of the class # Must be a private attribute to avoid name clashes between subclasses @@ -85,14 +92,34 @@ class TelegramObject: __INIT_PARAMS_CHECK: Optional[Type["TelegramObject"]] = None def __init__(self, *, api_kwargs: JSONDict = None) -> None: + self._frozen: bool = False self._id_attrs: Tuple[object, ...] = () self._bot: Optional["Bot"] = None # We don't do anything with api_kwargs here - see docstring of _apply_api_kwargs - self.api_kwargs: JSONDict = api_kwargs or {} + self.api_kwargs: Mapping[str, Any] = MappingProxyType(api_kwargs or {}) - def _apply_api_kwargs(self) -> None: + def _freeze(self) -> None: + self._frozen = True + + def _unfreeze(self) -> None: + self._frozen = False + + @contextmanager + def _unfrozen(self: Tele_co) -> Iterator[Tele_co]: + """Context manager to temporarily unfreeze the object. For internal use only. + + Note: + with to._unfrozen() as other_to: + assert to is other_to + """ + self._unfreeze() + yield self + self._freeze() + + def _apply_api_kwargs(self, api_kwargs: JSONDict) -> None: """Loops through the api kwargs and for every key that exists as attribute of the object (and is None), it moves the value from `api_kwargs` to the attribute. + *Edits `api_kwargs` in place!* This method is currently only called in the unpickling process, i.e. not on "normal" init. This is because @@ -105,9 +132,39 @@ def _apply_api_kwargs(self) -> None: then you can pass everything as proper argument. """ # we convert to list to ensure that the list doesn't change length while we loop - for key in list(self.api_kwargs.keys()): + for key in list(api_kwargs.keys()): if getattr(self, key, True) is None: - setattr(self, key, self.api_kwargs.pop(key)) + setattr(self, key, api_kwargs.pop(key)) + + def __setattr__(self, key: str, value: object) -> None: + """Overrides :meth:`object.__setattr__` to prevent the overriding of attributes. + + Raises: + :exc:`AttributeError` + """ + # protected attributes can always be set for convenient internal use + if key[0] == "_" or not getattr(self, "_frozen", True): + super().__setattr__(key, value) + return + + raise AttributeError( + f"Attribute `{key}` of class `{self.__class__.__name__}` can't be set!" + ) + + def __delattr__(self, key: str) -> None: + """Overrides :meth:`object.__delattr__` to prevent the deletion of attributes. + + Raises: + :exc:`AttributeError` + """ + # protected attributes can always be set for convenient internal use + if key[0] == "_" or not getattr(self, "_frozen", True): + super().__delattr__(key) + return + + raise AttributeError( + f"Attribute `{key}` of class `{self.__class__.__name__}` can't be deleted!" + ) def __repr__(self) -> str: """Gives a string representation of this object in the form @@ -130,6 +187,9 @@ def __repr__(self) -> str: if not self.api_kwargs: # Drop api_kwargs from the representation, if empty as_dict.pop("api_kwargs", None) + else: + # Otherwise, we want to skip the "mappingproxy" part of the repr + as_dict["api_kwargs"] = dict(self.api_kwargs) contents = ", ".join( f"{k}={as_dict[k]!r}" @@ -189,7 +249,11 @@ def __getstate__(self) -> Dict[str, Union[str, object]]: Returns: state (Dict[:obj:`str`, :obj:`object`]): The state of the object. """ - return self._get_attrs(include_private=True, recursive=False, remove_bot=True) + out = self._get_attrs(include_private=True, recursive=False, remove_bot=True) + # MappingProxyType is not pickable, so we convert it to a dict and revert in + # __setstate__ + out["api_kwargs"] = dict(self.api_kwargs) + return out def __setstate__(self, state: dict) -> None: """ @@ -208,19 +272,36 @@ def __setstate__(self, state: dict) -> None: Args: state (:obj:`dict`): The data to set as attributes of this object. """ + self._unfreeze() + # Make sure that we have a `_bot` attribute. This is necessary, since __getstate__ omits # this as Bots are not pickable. setattr(self, "_bot", None) - setattr(self, "api_kwargs", state.pop("api_kwargs", {})) # assign api_kwargs first + # get api_kwargs first because we may need to add entries to it (see try-except below) + api_kwargs = state.pop("api_kwargs", {}) + # get _frozen before the loop to avoid setting it to True in the loop + frozen = state.pop("_frozen", False) for key, val in state.items(): + try: setattr(self, key, val) - except AttributeError: # catch cases when old attributes are removed from new versions - self.api_kwargs[key] = val # add it to api_kwargs as fallback + except AttributeError: + # catch cases when old attributes are removed from new versions + api_kwargs[key] = val # add it to api_kwargs as fallback - self._apply_api_kwargs() + # For api_kwargs we first apply any kwargs that are already attributes of the object + # and then set the rest as MappingProxyType attribute. Converting to MappingProxyType + # is necessary, since __getstate__ converts it to a dict as MPT is not pickable. + self._apply_api_kwargs(api_kwargs) + setattr(self, "api_kwargs", MappingProxyType(api_kwargs)) + + # Apply freezing if necessary + # we .get(…) the setting for backwards compatibility with objects that were pickled + # before the freeze feature was introduced + if frozen: + self._freeze() def __deepcopy__(self: Tele_co, memodict: dict) -> Tele_co: """ @@ -243,9 +324,19 @@ def __deepcopy__(self: Tele_co, memodict: dict) -> Tele_co: result = cls.__new__(cls) # create a new instance memodict[id(self)] = result # save the id of the object in the dict - for k in self._get_attrs_names( - include_private=True - ): # now we set the attributes in the deepcopied object + setattr(result, "_frozen", False) # unfreeze the new object for setting the attributes + + # now we set the attributes in the deepcopied object + for k in self._get_attrs_names(include_private=True): + if k == "_frozen": + # Setting the frozen status to True would prevent the attributes from being set + continue + if k == "api_kwargs": + # Need to copy api_kwargs manually, since it's a MappingProxyType is not + # pickable and deepcopy uses the pickle interface + setattr(result, k, MappingProxyType(deepcopy(dict(self.api_kwargs), memodict))) + continue + try: setattr(result, k, deepcopy(getattr(self, k), memodict)) except AttributeError: @@ -254,6 +345,10 @@ def __deepcopy__(self: Tele_co, memodict: dict) -> Tele_co: # did not have the attribute yet. continue + # Apply freezing if necessary + if self._frozen: + result._freeze() + result.set_bot(bot) # Assign the bots back self.set_bot(bot) return result @@ -372,21 +467,26 @@ def _de_json( @classmethod def de_list( cls: Type[Tele_co], data: Optional[List[JSONDict]], bot: "Bot" - ) -> List[Optional[Tele_co]]: - """Converts JSON data to a list of Telegram objects. + ) -> Tuple[Tele_co, ...]: + """Converts a list of JSON objects to a tuple of Telegram objects. + + .. versionchanged:: 20.0 + + * Returns a tuple instead of a list. + * Filters out any :obj:`None` values. Args: - data (Dict[:obj:`str`, ...]): The JSON data. + data (List[Dict[:obj:`str`, ...]]): The JSON data. bot (:class:`telegram.Bot`): The bot associated with these objects. Returns: - A list of Telegram objects. + A tuple of Telegram objects. """ if not data: - return [] + return () - return [cls.de_json(d, bot) for d in data] + return tuple(obj for obj in (cls.de_json(d, bot) for d in data) if obj is not None) def to_json(self) -> str: """Gives a JSON representation of object. @@ -403,7 +503,9 @@ def to_dict(self, recursive: bool = True) -> JSONDict: """Gives representation of object as :obj:`dict`. .. versionchanged:: 20.0 - Now includes all entries of :attr:`api_kwargs`. + + * Now includes all entries of :attr:`api_kwargs`. + * Attributes whose values are empty sequences are no longer included. Args: recursive (:obj:`bool`, optional): If :obj:`True`, will convert any TelegramObjects @@ -420,13 +522,19 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # Now we should convert TGObjects to dicts inside objects such as sequences, and convert # datetimes to timestamps. This mostly eliminates the need for subclasses to override # `to_dict` + pop_keys: Set[str] = set() for key, value in out.items(): - if isinstance(value, (tuple, list)) and value: + if isinstance(value, (tuple, list)): + if not value: + # not popping directly to avoid changing the dict size during iteration + pop_keys.add(key) + continue + val = [] # empty list to append our converted values to for item in value: if hasattr(item, "to_dict"): val.append(item.to_dict(recursive=recursive)) - # This branch is useful for e.g. List[List[PhotoSize|KeyboardButton]] + # This branch is useful for e.g. Tuple[Tuple[PhotoSize|KeyboardButton]] elif isinstance(item, (tuple, list)): val.append( [ @@ -441,6 +549,9 @@ def to_dict(self, recursive: bool = True) -> JSONDict: elif isinstance(value, datetime.datetime): out[key] = to_timestamp(value) + for key in pop_keys: + out.pop(key) + # Effectively "unpack" api_kwargs into `out`: out.update(out.pop("api_kwargs", {})) # type: ignore[call-overload] return out diff --git a/telegram/_update.py b/telegram/_update.py index e29597b3029..55355bce2a7 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -268,6 +268,8 @@ def __init__( self._id_attrs = (self.update_id,) + self._freeze() + @property def effective_user(self) -> Optional["User"]: """ diff --git a/telegram/_user.py b/telegram/_user.py index 015ec93234e..ba0e2364757 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -158,6 +158,8 @@ def __init__( self._id_attrs = (self.id,) + self._freeze() + @property def name(self) -> str: """:obj:`str`: Convenience property. If available, returns the user's :attr:`username` @@ -482,7 +484,7 @@ async def send_media_group( caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, - ) -> List["Message"]: + ) -> Tuple["Message", ...]: """Shortcut for:: await bot.send_media_group(update.effective_user.id, *args, **kwargs) @@ -490,7 +492,8 @@ async def send_media_group( For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. Returns: - List[:class:`telegram.Message`:] On success, instance representing the message posted. + Tuple[:class:`telegram.Message`:] On success, a tuple of :class:`~telegram.Message` + instances that were sent is returned. """ return await self.get_bot().send_media_group( diff --git a/telegram/_userprofilephotos.py b/telegram/_userprofilephotos.py index 244c89fa2f2..f6cf3dd5a06 100644 --- a/telegram/_userprofilephotos.py +++ b/telegram/_userprofilephotos.py @@ -17,8 +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 UserProfilePhotos.""" - -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional, Sequence from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject @@ -36,27 +35,40 @@ class UserProfilePhotos(TelegramObject): Args: total_count (:obj:`int`): Total number of profile pictures the target user has. - photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures (in up to 4 - sizes each). + photos (Sequence[Sequence[:class:`telegram.PhotoSize`]]): Requested profile pictures (in up + to 4 sizes each). + + .. versionchanged:: 20.0 + |sequenceclassargs| Attributes: total_count (:obj:`int`): Total number of profile pictures. - photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures. + photos (Tuple[Tuple[:class:`telegram.PhotoSize`]]): Requested profile pictures (in up + to 4 sizes each). + + .. versionchanged:: 20.0 + |tupleclassattrs| """ __slots__ = ("photos", "total_count") def __init__( - self, total_count: int, photos: List[List[PhotoSize]], *, api_kwargs: JSONDict = None + self, + total_count: int, + photos: Sequence[Sequence[PhotoSize]], + *, + api_kwargs: JSONDict = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.total_count = total_count - self.photos = photos + self.photos = tuple(tuple(sizes) for sizes in photos) self._id_attrs = (self.total_count, self.photos) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["UserProfilePhotos"]: """See :meth:`telegram.TelegramObject.de_json`.""" @@ -68,6 +80,3 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["UserProfileP data["photos"] = [PhotoSize.de_list(photo, bot) for photo in data["photos"]] return super().de_json(data=data, bot=bot) - - def __hash__(self) -> int: - return hash(tuple(tuple(p for p in photo) for photo in self.photos)) diff --git a/telegram/_utils/argumentparsing.py b/telegram/_utils/argumentparsing.py new file mode 100644 index 00000000000..8052574f644 --- /dev/null +++ b/telegram/_utils/argumentparsing.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# 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 helper functions related to parsing arguments for classes and methods. + +Warning: + Contents of this module are intended to be used internally by the library and *not* by the + user. Changes to this module are not considered breaking changes and may not be documented in + the changelog. +""" +from typing import Optional, Sequence, Tuple, TypeVar + +T = TypeVar("T") + + +def parse_sequence_arg(arg: Optional[Sequence[T]]) -> Tuple[T, ...]: + """Parses an optional sequence into a tuple + + Args: + arg (:obj:`Sequence`): The sequence to parse. + + Returns: + :obj:`Tuple`: The sequence converted to a tuple or an empty tuple. + """ + return tuple(arg) if arg else () diff --git a/telegram/_utils/markup.py b/telegram/_utils/markup.py index 888937ba601..26b07a77fde 100644 --- a/telegram/_utils/markup.py +++ b/telegram/_utils/markup.py @@ -27,15 +27,19 @@ class ``telegram.ReplyMarkup``. user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ +from collections.abc import Sequence def check_keyboard_type(keyboard: object) -> bool: """Checks if the keyboard provided is of the correct type - A list of lists. Implicitly tested in the init-tests of `{Inline, Reply}KeyboardMarkup` """ - if not isinstance(keyboard, list): + # string and bytes may actually be used for ReplyKeyboardMarkup in which case each button + # would contain a single character. But that use case should be discouraged and we don't + # allow it here. + if not isinstance(keyboard, Sequence) or isinstance(keyboard, (str, bytes)): return False for row in keyboard: - if not isinstance(row, list): + if not isinstance(row, Sequence) or isinstance(row, (str, bytes)): return False return True diff --git a/telegram/_videochat.py b/telegram/_videochat.py index 60a0ab4ff0e..a32a74785c3 100644 --- a/telegram/_videochat.py +++ b/telegram/_videochat.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects related to Telegram video chats.""" - import datetime as dtm -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional, Sequence from telegram._telegramobject import TelegramObject from telegram._user import User +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.datetime import from_timestamp from telegram._utils.types import JSONDict @@ -76,6 +76,8 @@ def __init__( self.duration = duration self._id_attrs = (self.duration,) + self._freeze() + class VideoChatParticipantsInvited(TelegramObject): """ @@ -89,10 +91,16 @@ class VideoChatParticipantsInvited(TelegramObject): This class was renamed from ``VoiceChatParticipantsInvited`` in accordance to Bot API 6.0. Args: - users (List[:class:`telegram.User`]): New members that were invited to the video chat. + users (Sequence[:class:`telegram.User`]): New members that were invited to the video chat. + + .. versionchanged:: 20.0 + |sequenceclassargs| Attributes: - users (List[:class:`telegram.User`]): New members that were invited to the video chat. + users (Tuple[:class:`telegram.User`]): New members that were invited to the video chat. + + .. versionchanged:: 20.0 + |tupleclassattrs| """ @@ -100,14 +108,16 @@ class VideoChatParticipantsInvited(TelegramObject): def __init__( self, - users: List[User], + users: Sequence[User], *, api_kwargs: JSONDict = None, ) -> None: super().__init__(api_kwargs=api_kwargs) - self.users = users + self.users = parse_sequence_arg(users) self._id_attrs = (self.users,) + self._freeze() + @classmethod def de_json( cls, data: Optional[JSONDict], bot: "Bot" @@ -121,9 +131,6 @@ def de_json( data["users"] = User.de_list(data.get("users", []), bot) return super().de_json(data=data, bot=bot) - def __hash__(self) -> int: - return hash(None) if self.users is None else hash(tuple(self.users)) - class VideoChatScheduled(TelegramObject): """This object represents a service message about a video chat scheduled in the chat. @@ -156,6 +163,8 @@ def __init__( self._id_attrs = (self.start_date,) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["VideoChatScheduled"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/_webappdata.py b/telegram/_webappdata.py index 085a3d6899c..250ca3f6f10 100644 --- a/telegram/_webappdata.py +++ b/telegram/_webappdata.py @@ -58,3 +58,5 @@ def __init__(self, data: str, button_text: str, *, api_kwargs: JSONDict = None): self.button_text = button_text self._id_attrs = (self.data, self.button_text) + + self._freeze() diff --git a/telegram/_webappinfo.py b/telegram/_webappinfo.py index b09c416c410..2aca3c16eac 100644 --- a/telegram/_webappinfo.py +++ b/telegram/_webappinfo.py @@ -53,3 +53,5 @@ def __init__(self, url: str, *, api_kwargs: JSONDict = None): self.url = url self._id_attrs = (self.url,) + + self._freeze() diff --git a/telegram/_webhookinfo.py b/telegram/_webhookinfo.py index 5b1defb442f..dd878317f35 100644 --- a/telegram/_webhookinfo.py +++ b/telegram/_webhookinfo.py @@ -17,10 +17,10 @@ # 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 WebhookInfo.""" - -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional, Sequence from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.datetime import from_timestamp from telegram._utils.types import JSONDict @@ -55,8 +55,13 @@ class WebhookInfo(TelegramObject): most recent error that happened when trying to deliver an update via webhook. max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery. - allowed_updates (List[:obj:`str`], optional): A list of update types the bot is subscribed - to. Defaults to all update types, except :attr:`telegram.Update.chat_member`. + allowed_updates (Sequence[:obj:`str`], optional): A list of update types the bot is + subscribed to. Defaults to all update types, except + :attr:`telegram.Update.chat_member`. + + .. versionchanged:: 20.0 + |sequenceclassargs| + last_synchronization_error_date (:obj:`int`, optional): Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters. @@ -73,8 +78,14 @@ class WebhookInfo(TelegramObject): most recent error that happened when trying to deliver an update via webhook. max_connections (:obj:`int`): Optional. Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery. - allowed_updates (List[:obj:`str`]): Optional. A list of update types the bot is subscribed - to. Defaults to all update types, except :attr:`telegram.Update.chat_member`. + allowed_updates (Tuple[:obj:`str`]): Optional. A list of update types the bot is + subscribed to. Defaults to all update types, except + :attr:`telegram.Update.chat_member`. + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| last_synchronization_error_date (:obj:`int`): Optional. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters. @@ -101,7 +112,7 @@ def __init__( last_error_date: int = None, last_error_message: str = None, max_connections: int = None, - allowed_updates: List[str] = None, + allowed_updates: Sequence[str] = None, ip_address: str = None, last_synchronization_error_date: int = None, *, @@ -118,7 +129,7 @@ def __init__( self.last_error_date = last_error_date self.last_error_message = last_error_message self.max_connections = max_connections - self.allowed_updates = allowed_updates + self.allowed_updates = parse_sequence_arg(allowed_updates) self.last_synchronization_error_date = last_synchronization_error_date self._id_attrs = ( @@ -133,6 +144,8 @@ def __init__( self.last_synchronization_error_date, ) + self._freeze() + @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["WebhookInfo"]: """See :meth:`telegram.TelegramObject.de_json`.""" diff --git a/telegram/ext/_aioratelimiter.py b/telegram/ext/_aioratelimiter.py index d6b8a22938b..300e8fabdd7 100644 --- a/telegram/ext/_aioratelimiter.py +++ b/telegram/ext/_aioratelimiter.py @@ -23,7 +23,7 @@ import contextlib import logging import sys -from typing import Any, AsyncIterator, Callable, Coroutine, Dict, Optional, Union +from typing import Any, AsyncIterator, Callable, Coroutine, Dict, List, Optional, Union try: from aiolimiter import AsyncLimiter @@ -187,10 +187,10 @@ async def _run_request( self, chat: bool, group: Union[str, int, bool], - callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]], + callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, List[JSONDict]]]], args: Any, kwargs: Dict[str, Any], - ) -> Union[bool, JSONDict, None]: + ) -> Union[bool, JSONDict, List[JSONDict]]: base_context = self._base_limiter if (chat and self._base_limiter) else null_context() group_context = ( self._get_group_limiter(group) if group and self._group_max_rate else null_context() @@ -206,13 +206,13 @@ async def _run_request( # mypy doesn't understand that the last run of the for loop raises an exception async def process_request( # type: ignore[return] self, - callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]], + callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, List[JSONDict]]]], args: Any, kwargs: Dict[str, Any], endpoint: str, # skipcq: PYL-W0613 data: Dict[str, Any], rate_limit_args: Optional[int], - ) -> Union[bool, JSONDict, None]: + ) -> Union[bool, JSONDict, List[JSONDict]]: """ Processes a request by applying rate limiting. diff --git a/telegram/ext/_baseratelimiter.py b/telegram/ext/_baseratelimiter.py index b64b76da5f9..4726d8e336d 100644 --- a/telegram/ext/_baseratelimiter.py +++ b/telegram/ext/_baseratelimiter.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains a class that allows to rate limit requests to the Bot API.""" from abc import ABC, abstractmethod -from typing import Any, Callable, Coroutine, Dict, Generic, Optional, Union +from typing import Any, Callable, Coroutine, Dict, Generic, List, Optional, Union from telegram._utils.types import JSONDict from telegram.ext._utils.types import RLARGS @@ -58,13 +58,13 @@ async def shutdown(self) -> None: @abstractmethod async def process_request( self, - callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]], + callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, List[JSONDict]]]], args: Any, kwargs: Dict[str, Any], endpoint: str, data: Dict[str, Any], rate_limit_args: Optional[RLARGS], - ) -> Union[bool, JSONDict, None]: + ) -> Union[bool, JSONDict, List[JSONDict]]: """ Process a request. Must be implemented by a subclass. diff --git a/telegram/ext/_callbackdatacache.py b/telegram/ext/_callbackdatacache.py index 7e694bf10e6..c62d4db6255 100644 --- a/telegram/ext/_callbackdatacache.py +++ b/telegram/ext/_callbackdatacache.py @@ -381,7 +381,8 @@ def process_callback_query(self, callback_query: CallbackQuery) -> None: # Get the cached callback data for the CallbackQuery keyboard_uuid, button_data = self.__get_keyboard_uuid_and_button_data(data) - callback_query.data = button_data # type: ignore[assignment] + with callback_query._unfrozen(): + callback_query.data = button_data # type: ignore[assignment] # Map the callback queries ID to the keyboards UUID for later use if not mapped and not isinstance(button_data, InvalidCallbackData): diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 5af53d805bf..358e023e45c 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -22,6 +22,7 @@ from datetime import datetime from typing import ( TYPE_CHECKING, + Any, Callable, Dict, Generic, @@ -211,20 +212,21 @@ def __init__( private_key_password=private_key_password, local_mode=local_mode, ) - self._defaults = defaults - self._rate_limiter = rate_limiter - self._callback_data_cache: Optional[CallbackDataCache] = None + with self._unfrozen(): + self._defaults = defaults + self._rate_limiter = rate_limiter + self._callback_data_cache: Optional[CallbackDataCache] = None - # set up callback_data - if arbitrary_callback_data is False: - return + # set up callback_data + if arbitrary_callback_data is False: + return - if not isinstance(arbitrary_callback_data, bool): - maxsize = cast(int, arbitrary_callback_data) - else: - maxsize = 1024 + if not isinstance(arbitrary_callback_data, bool): + maxsize = cast(int, arbitrary_callback_data) + else: + maxsize = 1024 - self._callback_data_cache = CallbackDataCache(bot=self, maxsize=maxsize) + self._callback_data_cache = CallbackDataCache(bot=self, maxsize=maxsize) @property def callback_data_cache(self) -> Optional[CallbackDataCache]: @@ -291,7 +293,7 @@ async def _do_post( write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, - ) -> Union[bool, JSONDict, None]: + ) -> Union[bool, JSONDict, List[JSONDict]]: """Order of method calls is: Bot.some_method -> Bot._post -> Bot._do_post. So we can override Bot._do_post to add rate limiting. """ @@ -380,14 +382,17 @@ def _insert_defaults(self, data: Dict[str, object]) -> None: elif isinstance(val, InputMedia) and val.parse_mode is DEFAULT_NONE: # Copy object as not to edit it in-place val = copy(val) - val.parse_mode = self.defaults.parse_mode if self.defaults else None + with val._unfrozen(): + val.parse_mode = self.defaults.parse_mode if self.defaults else None data[key] = val elif key == "media" and isinstance(val, list): # Copy objects as not to edit them in-place copy_list = [copy(media) for media in val] for media in copy_list: if media.parse_mode is DEFAULT_NONE: - media.parse_mode = self.defaults.parse_mode if self.defaults else None + with media._unfrozen(): + media.parse_mode = self.defaults.parse_mode if self.defaults else None + data[key] = copy_list def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]: @@ -479,7 +484,7 @@ async def _send_message( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> Union[bool, Message]: + ) -> Any: # We override this method to call self._replace_keyboard and self._insert_callback_data. # This covers most methods that have a reply_markup result = await super()._send_message( @@ -517,7 +522,7 @@ async def get_updates( connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> List[Update]: + ) -> Tuple[Update, ...]: updates = await super().get_updates( offset=offset, limit=limit, @@ -563,8 +568,10 @@ def _effective_inline_results( # We build a new result in case the user wants to use the same object in # different places new_result = copy(result) - markup = self._replace_keyboard(result.reply_markup) - new_result.reply_markup = markup # type: ignore[attr-defined] + with new_result._unfrozen(): + markup = self._replace_keyboard(result.reply_markup) + new_result.reply_markup = markup + results.append(new_result) return results, next_offset @@ -579,8 +586,9 @@ def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQ copied = False if hasattr(res, "parse_mode") and res.parse_mode is DEFAULT_NONE: res = copy(res) - copied = True - res.parse_mode = self.defaults.parse_mode if self.defaults else None + with res._unfrozen(): + copied = True + res.parse_mode = self.defaults.parse_mode if self.defaults else None if hasattr(res, "input_message_content") and res.input_message_content: if ( hasattr(res.input_message_content, "parse_mode") @@ -589,18 +597,20 @@ def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQ if not copied: res = copy(res) copied = True - res.input_message_content.parse_mode = ( - self.defaults.parse_mode if self.defaults else None - ) + with res.input_message_content._unfrozen(): + res.input_message_content.parse_mode = ( + self.defaults.parse_mode if self.defaults else None + ) if ( hasattr(res.input_message_content, "disable_web_page_preview") and res.input_message_content.disable_web_page_preview is DEFAULT_NONE ): if not copied: res = copy(res) - res.input_message_content.disable_web_page_preview = ( - self.defaults.disable_web_page_preview if self.defaults else None - ) + with res.input_message_content._unfrozen(): + res.input_message_content.disable_web_page_preview = ( + self.defaults.disable_web_page_preview if self.defaults else None + ) return res @@ -1496,7 +1506,7 @@ async def get_chat_administrators( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, rate_limit_args: RLARGS = None, - ) -> List[ChatMember]: + ) -> Tuple[ChatMember, ...]: return await super().get_chat_administrators( chat_id=chat_id, read_timeout=read_timeout, @@ -1599,7 +1609,7 @@ async def get_forum_topic_icon_stickers( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, rate_limit_args: RLARGS = None, - ) -> List[Sticker]: + ) -> Tuple[Sticker, ...]: return await super().get_forum_topic_icon_stickers( read_timeout=read_timeout, write_timeout=write_timeout, @@ -1621,7 +1631,7 @@ async def get_game_high_scores( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, rate_limit_args: RLARGS = None, - ) -> List[GameHighScore]: + ) -> Tuple[GameHighScore, ...]: return await super().get_game_high_scores( user_id=user_id, chat_id=chat_id, @@ -1663,7 +1673,7 @@ async def get_my_commands( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, rate_limit_args: RLARGS = None, - ) -> List[BotCommand]: + ) -> Tuple[BotCommand, ...]: return await super().get_my_commands( scope=scope, language_code=language_code, @@ -1724,7 +1734,7 @@ async def get_custom_emoji_stickers( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, rate_limit_args: RLARGS = None, - ) -> List[Sticker]: + ) -> Tuple[Sticker, ...]: return await super().get_custom_emoji_stickers( custom_emoji_ids=custom_emoji_ids, read_timeout=read_timeout, @@ -2439,7 +2449,7 @@ async def send_media_group( caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, - ) -> List[Message]: + ) -> Tuple[Message, ...]: return await super().send_media_group( chat_id=chat_id, media=media, diff --git a/telegram/ext/_picklepersistence.py b/telegram/ext/_picklepersistence.py index 6ad23ebdd5e..42eb3241123 100644 --- a/telegram/ext/_picklepersistence.py +++ b/telegram/ext/_picklepersistence.py @@ -63,6 +63,9 @@ def _custom_reduction(cls: TelegramObj) -> Tuple[Callable, Tuple[Type[TelegramOb works as intended. """ data = cls._get_attrs(include_private=True) # pylint: disable=protected-access + # MappingProxyType is not pickable, so we convert it to a dict + # no need to convert back to MPT in _reconstruct_to, since it's done in __setstate__ + data["api_kwargs"] = dict(data["api_kwargs"]) # type: ignore[arg-type] return _reconstruct_to, (cls.__class__, data) diff --git a/telegram/request/_baserequest.py b/telegram/request/_baserequest.py index b544e45e09f..d4df2cfb3a0 100644 --- a/telegram/request/_baserequest.py +++ b/telegram/request/_baserequest.py @@ -23,7 +23,7 @@ from contextlib import AbstractAsyncContextManager from http import HTTPStatus from types import TracebackType -from typing import ClassVar, Optional, Tuple, Type, TypeVar, Union +from typing import ClassVar, List, Optional, Tuple, Type, TypeVar, Union from telegram._utils.defaultvalue import DEFAULT_NONE as _DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -132,7 +132,7 @@ async def post( write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, - ) -> Union[JSONDict, bool]: + ) -> Union[JSONDict, List[JSONDict], bool]: """Makes a request to the Bot API handles the return code and parses the answer. Warning: @@ -161,7 +161,7 @@ async def post( :attr:`DEFAULT_NONE`. Returns: - Dict[:obj:`str`, ...]: The JSON response of the Bot API. + The JSON response of the Bot API. """ result = await self._request_wrapper( diff --git a/tests/auxil/bot_method_checks.py b/tests/auxil/bot_method_checks.py index 35d750c603e..289ed35747e 100644 --- a/tests/auxil/bot_method_checks.py +++ b/tests/auxil/bot_method_checks.py @@ -277,7 +277,7 @@ async def check_defaults_handling( kwargs["tzinfo"] = pytz.timezone("America/New_York") defaults_custom_defaults = Defaults(**kwargs) - expected_return_values = [None, []] if return_value is None else [return_value] + expected_return_values = [None, ()] if return_value is None else [return_value] async def make_assertion( url, request_data: RequestData, df_value=DEFAULT_NONE, *args, **kwargs diff --git a/tests/conftest.py b/tests/conftest.py index 19e8828e131..06e12e98c4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -127,11 +127,17 @@ async def _request_wrapper( class DictExtBot(ExtBot): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Makes it easier to work with the bot in tests + self._unfreeze() class DictBot(Bot): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Makes it easier to work with the bot in tests + self._unfreeze() class DictApplication(Application): diff --git a/tests/test_animation.py b/tests/test_animation.py index aab4c248931..98c2184eaba 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -165,7 +165,7 @@ async def test_send_animation_caption_entities(self, bot, chat_id, animation): ) assert message.caption == test_string - assert message.caption_entities == entities + assert message.caption_entities == tuple(entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) diff --git a/tests/test_application.py b/tests/test_application.py index 35dedb7fbec..698c40bbc09 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -483,7 +483,7 @@ def two(update, context): u = make_message_update(message="test") await app.process_update(u) self.received = None - u.message.text = "something" + u = make_message_update(message="something") await app.process_update(u) def test_add_handler_errors(self, app): @@ -553,7 +553,7 @@ async def test_add_handlers(self, app): app.add_handler(msg_handler_set_count, 1) app.add_handlers((msg_handler_inc_count, msg_handler_inc_count), 1) - photo_update = make_message_update(message=Message(2, None, None, photo=True)) + photo_update = make_message_update(message=Message(2, None, None, photo=(True,))) async with app: await app.start() @@ -1437,6 +1437,7 @@ async def post_init(app: Application) -> None: events.append("post_init") app = Application.builder().token(bot.token).post_init(post_init).build() + app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr( app, "initialize", call_after(app.initialize, lambda _: events.append("init")) @@ -1479,6 +1480,7 @@ async def post_shutdown(app: Application) -> None: events.append("post_shutdown") app = Application.builder().token(bot.token).post_shutdown(post_shutdown).build() + app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr( app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown")) @@ -1659,6 +1661,7 @@ async def post_init(app: Application) -> None: events.append("post_init") app = Application.builder().token(bot.token).post_init(post_init).build() + app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) monkeypatch.setattr( @@ -1718,6 +1721,7 @@ async def post_shutdown(app: Application) -> None: events.append("post_shutdown") app = Application.builder().token(bot.token).post_shutdown(post_shutdown).build() + app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) monkeypatch.setattr( diff --git a/tests/test_audio.py b/tests/test_audio.py index f09e68f1c3f..9619e7ef5a1 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -191,7 +191,7 @@ async def test_send_audio_caption_entities(self, bot, chat_id, audio): ) assert message.caption == test_string - assert message.caption_entities == entities + assert message.caption_entities == tuple(entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) diff --git a/tests/test_bot.py b/tests/test_bot.py index 3d07adf8c38..f8489bbd03c 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -92,13 +92,15 @@ async def message(bot, chat_id): to_reply_to = await bot.send_message( chat_id, "Text", disable_web_page_preview=True, disable_notification=True ) - return await bot.send_message( + out = await bot.send_message( chat_id, "Text", reply_to_message_id=to_reply_to.message_id, disable_web_page_preview=True, disable_notification=True, ) + out._unfreeze() + return out @pytest.fixture(scope="class") @@ -390,8 +392,8 @@ async def test_get_me_and_properties_not_initialized(self, bot: Bot, attribute): async def test_equality(self): async with make_bot(token=FALLBACKS[0]["token"]) as a, make_bot( token=FALLBACKS[0]["token"] - ) as b, make_bot(token=FALLBACKS[1]["token"]) as c: - d = Update(123456789) + ) as b, make_bot(token=FALLBACKS[1]["token"]) as c, Bot(token=FALLBACKS[0]["token"]) as d: + e = Update(123456789) assert a == b assert hash(a) == hash(b) @@ -403,6 +405,9 @@ async def test_equality(self): assert a != d assert hash(a) != hash(d) + assert a != e + assert hash(a) != hash(e) + @pytest.mark.flaky(3, 1) async def test_to_dict(self, bot): to_dict_bot = bot.to_dict() @@ -687,7 +692,7 @@ async def test_send_and_stop_poll(self, bot, super_group_id, reply_markup): assert message_quiz.poll.type == Poll.QUIZ assert message_quiz.poll.is_closed assert message_quiz.poll.explanation == "Here is a link" - assert message_quiz.poll.explanation_entities == explanation_entities + assert message_quiz.poll.explanation_entities == tuple(explanation_entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize( @@ -741,6 +746,7 @@ async def test_send_close_date_default_tz(self, tz_bot, super_group_id): close_date=close_date, read_timeout=60, ) + msg.poll._unfreeze() # Sometimes there can be a few seconds delay, so don't let the test fail due to that- msg.poll.close_date = msg.poll.close_date.astimezone(aware_close_date.tzinfo) assert abs(msg.poll.close_date - aware_close_date) <= dtm.timedelta(seconds=5) @@ -775,7 +781,7 @@ async def test_send_poll_explanation_entities(self, bot, chat_id): ) assert message.poll.explanation == test_string - assert message.poll.explanation_entities == entities + assert message.poll.explanation_entities == tuple(entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) @@ -795,11 +801,11 @@ async def test_send_poll_default_parse_mode(self, default_bot, super_group_id): explanation=explanation_markdown, ) assert message.poll.explanation == explanation - assert message.poll.explanation_entities == [ + assert message.poll.explanation_entities == ( MessageEntity(MessageEntity.ITALIC, 0, 6), MessageEntity(MessageEntity.BOLD, 7, 4), MessageEntity(MessageEntity.CODE, 12, 4), - ] + ) message = await default_bot.send_poll( chat_id=super_group_id, @@ -812,7 +818,7 @@ async def test_send_poll_default_parse_mode(self, default_bot, super_group_id): explanation_parse_mode=None, ) assert message.poll.explanation == explanation_markdown - assert message.poll.explanation_entities == [] + assert message.poll.explanation_entities == () message = await default_bot.send_poll( chat_id=super_group_id, @@ -825,7 +831,7 @@ async def test_send_poll_default_parse_mode(self, default_bot, super_group_id): explanation_parse_mode="HTML", ) assert message.poll.explanation == explanation_markdown - assert message.poll.explanation_entities == [] + assert message.poll.explanation_entities == () @pytest.mark.flaky(3, 1) @pytest.mark.parametrize( @@ -1611,7 +1617,7 @@ async def test_edit_message_text_entities(self, bot, message): ) assert message.text == test_string - assert message.entities == entities + assert message.entities == tuple(entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) @@ -1684,7 +1690,7 @@ async def test_edit_message_caption_entities(self, bot, media_message): ) assert message.caption == test_string - assert message.caption_entities == entities + assert message.caption_entities == tuple(entities) # edit_message_media is tested in test_inputmedia @@ -1759,7 +1765,7 @@ async def test_get_updates(self, bot): await bot.delete_webhook() # make sure there is no webhook set if webhook tests failed updates = await bot.get_updates(timeout=1) - assert isinstance(updates, list) + assert isinstance(updates, tuple) if updates: assert isinstance(updates[0], Update) @@ -1791,7 +1797,7 @@ async def post(*args, **kwargs): monkeypatch.setattr(BaseRequest, "post", post) updates = await bot.get_updates(timeout=1) - assert isinstance(updates, list) + assert isinstance(updates, tuple) assert len(updates) == 1 assert isinstance(updates[0].callback_query.data, InvalidCallbackData) @@ -1827,7 +1833,7 @@ async def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot, use_ip live_info = await bot.get_webhook_info() assert live_info.url == url assert live_info.max_connections == max_connections - assert live_info.allowed_updates == allowed_updates + assert live_info.allowed_updates == tuple(allowed_updates) assert live_info.ip_address == ip assert live_info.has_custom_certificate == use_ip @@ -1916,7 +1922,7 @@ async def test_get_chat(self, bot, super_group_id): @pytest.mark.flaky(3, 1) async def test_get_chat_administrators(self, bot, channel_id): admins = await bot.get_chat_administrators(channel_id) - assert isinstance(admins, list) + assert isinstance(admins, tuple) for a in admins: assert a.status in ("administrator", "creator") @@ -2561,7 +2567,7 @@ async def test_send_message_entities(self, bot, chat_id): ] message = await bot.send_message(chat_id=chat_id, text=test_string, entities=entities) assert message.text == test_string - assert message.entities == entities + assert message.entities == tuple(entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) @@ -2675,7 +2681,7 @@ async def test_get_set_chat_menu_button(self, bot, chat_id): async def test_set_and_get_my_commands(self, bot): commands = [BotCommand("cmd1", "descr1"), ["cmd2", "descr2"]] await bot.set_my_commands([]) - assert await bot.get_my_commands() == [] + assert await bot.get_my_commands() == () assert await bot.set_my_commands(commands) for i, bc in enumerate(await bot.get_my_commands()): @@ -3075,6 +3081,7 @@ async def test_arbitrary_callback_data_pinned_message_reply_to_message( None, reply_markup=bot.callback_data_cache.process_keyboard(reply_markup), ) + message._unfreeze() # We do to_dict -> de_json to make sure those aren't the same objects message.pinned_message = Message.de_json(message.to_dict(), bot) @@ -3098,7 +3105,7 @@ async def post(*args, **kwargs): await bot.delete_webhook() # make sure there is no webhook set if webhook tests failed updates = await bot.get_updates(timeout=1) - assert isinstance(updates, list) + assert isinstance(updates, tuple) assert len(updates) == 1 effective_message = updates[0][message_type] @@ -3165,7 +3172,7 @@ async def post(*args, **kwargs): await bot.delete_webhook() # make sure there is no webhook set if webhook tests failed updates = await bot.get_updates(timeout=1) - assert isinstance(updates, list) + assert isinstance(updates, tuple) assert len(updates) == 1 message = updates[0][message_type] diff --git a/tests/test_callbackdatacache.py b/tests/test_callbackdatacache.py index f7fdb5eb751..2c2872ec017 100644 --- a/tests/test_callbackdatacache.py +++ b/tests/test_callbackdatacache.py @@ -185,6 +185,7 @@ def test_process_callback_query(self, callback_data_cache, data, message, invali chat = Chat(1, "private") effective_message = Message(message_id=1, date=datetime.now(), chat=chat, reply_markup=out) + effective_message._unfreeze() effective_message.reply_to_message = deepcopy(effective_message) effective_message.pinned_message = deepcopy(effective_message) cq_id = uuid4().hex diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 108a812d31e..13b6133e298 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -39,6 +39,7 @@ def callback_query(bot, request): game_short_name=TestCallbackQuery.game_short_name, ) cbq.set_bot(bot) + cbq._unfreeze() if request.param == "message": cbq.message = TestCallbackQuery.message cbq.message.set_bot(bot) diff --git a/tests/test_callbackqueryhandler.py b/tests/test_callbackqueryhandler.py index 200c88ae714..b51aac66a93 100644 --- a/tests/test_callbackqueryhandler.py +++ b/tests/test_callbackqueryhandler.py @@ -66,7 +66,10 @@ def false_update(request): @pytest.fixture(scope="function") def callback_query(bot): - return Update(0, callback_query=CallbackQuery(2, User(1, "", False), None, data="test data")) + update = Update(0, callback_query=CallbackQuery(2, User(1, "", False), None, data="test data")) + update._unfreeze() + update.callback_query._unfreeze() + return update class TestCallbackQueryHandler: diff --git a/tests/test_chat.py b/tests/test_chat.py index b95ca586f71..a8418ec9f6b 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -53,6 +53,7 @@ def chat(bot): emoji_status_custom_emoji_id=TestChat.emoji_status_custom_emoji_id, ) chat.set_bot(bot) + chat._unfreeze() return chat @@ -138,7 +139,7 @@ def test_de_json(self, bot): "all_members_are_administrators": self.all_members_are_administrators } assert chat.is_forum == self.is_forum - assert chat.active_usernames == self.active_usernames + assert chat.active_usernames == tuple(self.active_usernames) assert chat.emoji_status_custom_emoji_id == self.emoji_status_custom_emoji_id def test_to_dict(self, chat): @@ -163,9 +164,18 @@ def test_to_dict(self, chat): == chat.has_restricted_voice_and_video_messages ) assert chat_dict["is_forum"] == chat.is_forum - assert chat_dict["active_usernames"] == chat.active_usernames + assert chat_dict["active_usernames"] == list(chat.active_usernames) assert chat_dict["emoji_status_custom_emoji_id"] == chat.emoji_status_custom_emoji_id + def test_always_tuples_attributes(self): + chat = Chat( + id=123, + title="title", + type=Chat.PRIVATE, + ) + assert isinstance(chat.active_usernames, tuple) + assert chat.active_usernames == () + def test_enum_init(self): chat = Chat(id=1, type="foo") assert chat.type == "foo" diff --git a/tests/test_chatjoinrequesthandler.py b/tests/test_chatjoinrequesthandler.py index 4a2fd9da4d2..14cf25f9482 100644 --- a/tests/test_chatjoinrequesthandler.py +++ b/tests/test_chatjoinrequesthandler.py @@ -162,6 +162,7 @@ def test_with_username(self, chat_join_request_update): handler = ChatJoinRequestHandler(self.callback, username=["@user_b"]) assert not handler.check_update(chat_join_request_update) + chat_join_request_update.chat_join_request.from_user._unfreeze() chat_join_request_update.chat_join_request.from_user.username = None assert not handler.check_update(chat_join_request_update) diff --git a/tests/test_chatmemberhandler.py b/tests/test_chatmemberhandler.py index 1b8b3e5819d..dd78bcae1cb 100644 --- a/tests/test_chatmemberhandler.py +++ b/tests/test_chatmemberhandler.py @@ -82,7 +82,9 @@ def chat_member_updated(): @pytest.fixture(scope="function") def chat_member(bot, chat_member_updated): - return Update(0, my_chat_member=chat_member_updated) + update = Update(0, my_chat_member=chat_member_updated) + update._unfreeze() + return update class TestChatMemberHandler: diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py index 659291e43cf..29cc9e5824f 100644 --- a/tests/test_chatmemberupdated.py +++ b/tests/test_chatmemberupdated.py @@ -216,7 +216,10 @@ def test_difference_required(self, user, chat): # We deliberately change an optional argument here to make sure that comparison doesn't # just happens by id/required args new_user = User(1, "First name", False, last_name="last name") - new_chat_member.user = new_user + new_chat_member = ChatMember(new_user, "new_status") + chat_member_updated = ChatMemberUpdated( + chat, user, datetime.datetime.utcnow(), old_chat_member, new_chat_member + ) assert chat_member_updated.difference() == { "status": ("old_status", "new_status"), "user": (user, new_user), diff --git a/tests/test_choseninlineresult.py b/tests/test_choseninlineresult.py index 884c5bba500..636aeceeeec 100644 --- a/tests/test_choseninlineresult.py +++ b/tests/test_choseninlineresult.py @@ -24,7 +24,9 @@ @pytest.fixture(scope="class") def user(): - return User(1, "First name", False) + user = User(1, "First name", False) + user._unfreeze() + return user @pytest.fixture(scope="class") diff --git a/tests/test_choseninlineresulthandler.py b/tests/test_choseninlineresulthandler.py index be384a00e9e..746b82d71f9 100644 --- a/tests/test_choseninlineresulthandler.py +++ b/tests/test_choseninlineresulthandler.py @@ -68,10 +68,13 @@ def false_update(request): @pytest.fixture(scope="class") def chosen_inline_result(): - return Update( + out = Update( 1, chosen_inline_result=ChosenInlineResult("result_id", User(1, "test_user", False), "query"), ) + out._unfreeze() + out.chosen_inline_result._unfreeze() + return out class TestChosenInlineResultHandler: diff --git a/tests/test_commandhandler.py b/tests/test_commandhandler.py index f88c7e39f7b..8b6454c989b 100644 --- a/tests/test_commandhandler.py +++ b/tests/test_commandhandler.py @@ -104,7 +104,7 @@ async def _test_context_args_or_regex(self, app, handler, text): app.add_handler(handler) update = make_command_update(text, bot=app.bot) assert not await self.response(app, update) - update.message.text += " one two" + update = make_command_update(text + " one two", bot=app.bot) assert await self.response(app, update) def _test_edited(self, message, handler_edited, handler_not_edited): diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index fcb00889129..93f64963e08 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -518,6 +518,8 @@ async def callback(_, __): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY @@ -569,6 +571,8 @@ async def test_conversation_handler_end(self, caplog, app, bot, user1): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) @@ -608,6 +612,8 @@ async def test_conversation_handler_fallback(self, app, bot, user1, user2): entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/eat"))], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) @@ -660,6 +666,8 @@ async def callback(_, __): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) try: @@ -693,6 +701,8 @@ async def test_conversation_handler_per_chat(self, app, bot, user1, user2): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) @@ -739,6 +749,8 @@ async def test_conversation_handler_per_user(self, app, bot, user1): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() # First check that updates without user won't be handled message.from_user = None @@ -801,6 +813,7 @@ async def two(update, context): ) if message: message.set_bot(bot) + message._unfreeze() inline_message_id = "42" if inline else None async with app: @@ -822,6 +835,7 @@ async def two(update, context): inline_message_id=inline_message_id, ) cbq_2.set_bot(bot) + cbq_2._unfreeze() await app.process_update(Update(update_id=0, callback_query=cbq_1)) # Make sure that we're in the correct state @@ -861,6 +875,8 @@ async def test_end_on_first_message(self, app, bot, user1): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert handler.check_update(Update(update_id=0, message=message)) @@ -887,6 +903,8 @@ async def test_end_on_first_message_non_blocking_handler(self, app, bot, user1): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # give the task a chance to finish @@ -906,6 +924,7 @@ async def test_none_on_first_message(self, app, bot, user1): # User starts the state machine and a callback function returns None message = Message(0, None, self.group, from_user=user1, text="/start") message.set_bot(bot) + message._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # Check that the same message is accepted again, i.e. the conversation immediately @@ -933,6 +952,8 @@ async def test_none_on_first_message_non_blocking_handler(self, app, bot, user1) ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # Give the task a chance to finish @@ -958,6 +979,7 @@ async def test_channel_message_without_chat(self, bot): ) message = Message(0, date=None, chat=Chat(0, Chat.CHANNEL, "Misses Test")) message.set_bot(bot) + message._unfreeze() update = Update(0, channel_post=message) assert not handler.check_update(update) @@ -971,6 +993,7 @@ async def test_all_update_types(self, app, bot, user1): ) message = Message(0, None, self.group, from_user=user1, text="ignore") message.set_bot(bot) + message._unfreeze() callback_query = CallbackQuery(0, user1, None, message=message, data="data") callback_query.set_bot(bot) chosen_inline_result = ChosenInlineResult(0, user1, "query") @@ -1011,6 +1034,8 @@ async def test_no_running_job_queue_warning(self, app, bot, user1, recwarn, jq): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) @@ -1053,6 +1078,8 @@ class DictJB(JobQueue): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.start() @@ -1101,6 +1128,8 @@ async def raise_error(*a, **kw): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() # start the conversation async with app: await app.process_update(Update(update_id=0, message=message)) @@ -1149,6 +1178,8 @@ async def raise_error(*a, **kw): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() # start the conversation async with app: await app.process_update(Update(update_id=0, message=message)) @@ -1255,6 +1286,8 @@ def timeout(*a, **kw): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: # start the conversation await app.process_update(Update(update_id=0, message=message)) @@ -1300,6 +1333,8 @@ def timeout(*args, **kwargs): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() brew_message = Message( 0, None, @@ -1348,6 +1383,8 @@ async def start_callback(u, c): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() update = Update(update_id=0, message=message) async def timeout_callback(u, c): @@ -1407,6 +1444,8 @@ async def test_conversation_timeout_keeps_extending(self, app, bot, user1): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.start() @@ -1458,6 +1497,8 @@ async def test_conversation_timeout_two_users(self, app, bot, user1, user2): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.start() @@ -1518,6 +1559,8 @@ async def test_conversation_handler_timeout_state(self, app, bot, user1): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.start() @@ -1591,6 +1634,8 @@ async def test_conversation_handler_timeout_state_context(self, app, bot, user1) ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.start() @@ -1673,6 +1718,8 @@ async def slowbrew(_update, context): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.start() @@ -1722,6 +1769,8 @@ async def test_nested_conversation_handler(self, app, bot, user1, user2): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY @@ -1850,6 +1899,8 @@ def test_callback(u, c): ], ) message.set_bot(bot) + message._unfreeze() + message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY @@ -2183,6 +2234,7 @@ async def blocking(_, __): text="/start", from_user=user1, ) + message._unfreeze() async with app: await app.process_update(Update(0, message=message)) diff --git a/tests/test_document.py b/tests/test_document.py index 5a46b83a896..1600eb45e0a 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -178,7 +178,7 @@ async def test_send_document_caption_entities(self, bot, chat_id, document): ) assert message.caption == test_string - assert message.caption_entities == entities + assert message.caption_entities == tuple(entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) diff --git a/tests/test_encryptedpassportelement.py b/tests/test_encryptedpassportelement.py index 2e67fe29d5a..f57193d1a7a 100644 --- a/tests/test_encryptedpassportelement.py +++ b/tests/test_encryptedpassportelement.py @@ -60,7 +60,7 @@ def test_expected_values(self, encrypted_passport_element): assert encrypted_passport_element.data == self.data assert encrypted_passport_element.phone_number == self.phone_number assert encrypted_passport_element.email == self.email - assert encrypted_passport_element.files == self.files + assert encrypted_passport_element.files == tuple(self.files) assert encrypted_passport_element.front_side == self.front_side assert encrypted_passport_element.reverse_side == self.reverse_side assert encrypted_passport_element.selfie == self.selfie @@ -90,6 +90,11 @@ def test_to_dict(self, encrypted_passport_element): == encrypted_passport_element.selfie.to_dict() ) + def test_attributes_always_tuple(self): + element = EncryptedPassportElement(self.type_, self.hash) + assert element.files == () + assert element.translation == () + def test_equality(self): a = EncryptedPassportElement(self.type_, self.hash, data=self.data) b = EncryptedPassportElement(self.type_, self.hash, data=self.data) diff --git a/tests/test_file.py b/tests/test_file.py index 618ad7a58ef..91fd3a3c880 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -36,6 +36,7 @@ def file(bot): file_size=TestFile.file_size, ) file.set_bot(bot) + file._unfreeze() return file diff --git a/tests/test_filters.py b/tests/test_filters.py index a79a7be0916..41ed1cecd09 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -39,7 +39,7 @@ @pytest.fixture(scope="function") def update(): - return Update( + update = Update( 0, Message( 0, @@ -52,6 +52,15 @@ def update(): forward_from_chat=Chat(0, "Channel"), ), ) + update._unfreeze() + update.message._unfreeze() + update.message.chat._unfreeze() + update.message.from_user._unfreeze() + update.message.via_bot._unfreeze() + update.message.sender_chat._unfreeze() + update.message.forward_from._unfreeze() + update.message.forward_from_chat._unfreeze() + return update @pytest.fixture(scope="function", params=MessageEntity.ALL_TYPES) @@ -618,6 +627,7 @@ def test_filters_document_type(self, update): update.message.document = Document( "file_id", "unique_id", mime_type="application/vnd.android.package-archive" ) + update.message.document._unfreeze() assert filters.Document.APK.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.DOC.check_update(update) @@ -727,6 +737,7 @@ def test_filters_file_extension_basic(self, update): file_name="file.jpg", mime_type="image/jpeg", ) + update.message.document._unfreeze() assert filters.Document.FileExtension("jpg").check_update(update) assert not filters.Document.FileExtension("jpeg").check_update(update) assert not filters.Document.FileExtension("file.jpg").check_update(update) @@ -750,6 +761,7 @@ def test_filters_file_extension_minds_dots(self, update): file_name="file.jpg", mime_type="image/jpeg", ) + update.message.document._unfreeze() assert not filters.Document.FileExtension(".jpg").check_update(update) assert not filters.Document.FileExtension("e.jpg").check_update(update) assert not filters.Document.FileExtension("file.jpg").check_update(update) @@ -779,6 +791,7 @@ def test_filters_file_extension_none_arg(self, update): file_name="file.jpg", mime_type="image/jpeg", ) + update.message.document._unfreeze() assert not filters.Document.FileExtension(None).check_update(update) update.message.document.file_name = "file" @@ -798,6 +811,7 @@ def test_filters_file_extension_case_sensitivity(self, update): file_name="file.jpg", mime_type="image/jpeg", ) + update.message.document._unfreeze() assert filters.Document.FileExtension("JPG").check_update(update) assert filters.Document.FileExtension("jpG").check_update(update) @@ -845,6 +859,7 @@ def test_filters_photo(self, update): def test_filters_sticker(self, update): assert not filters.Sticker.ALL.check_update(update) update.message.sticker = Sticker("1", "uniq", 1, 2, False, False, Sticker.REGULAR) + update.message.sticker._unfreeze() assert filters.Sticker.ALL.check_update(update) assert filters.Sticker.STATIC.check_update(update) assert not filters.Sticker.VIDEO.check_update(update) diff --git a/tests/test_game.py b/tests/test_game.py index 5c7e96814ef..dc775016467 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -24,7 +24,7 @@ @pytest.fixture(scope="function") def game(): - return Game( + game = Game( TestGame.title, TestGame.description, TestGame.photo, @@ -32,6 +32,8 @@ def game(): text_entities=TestGame.text_entities, animation=TestGame.animation, ) + game._unfreeze() + return game class TestGame: @@ -61,7 +63,7 @@ def test_de_json_required(self, bot): assert game.title == self.title assert game.description == self.description - assert game.photo == self.photo + assert game.photo == tuple(self.photo) def test_de_json_all(self, bot): json_dict = { @@ -77,9 +79,9 @@ def test_de_json_all(self, bot): assert game.title == self.title assert game.description == self.description - assert game.photo == self.photo + assert game.photo == tuple(self.photo) assert game.text == self.text - assert game.text_entities == self.text_entities + assert game.text_entities == tuple(self.text_entities) assert game.animation == self.animation def test_to_dict(self, game): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index fa1f2a40cf9..21765c0e5d8 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -125,7 +125,7 @@ def build_test_message(kwargs): config.update(**kwargs) return Message(**config) - message = build_test_message({message_type: True}) + message = build_test_message({message_type: (True,)}) # tuple for array-type args entity = message if entity_type is Message else Update(1, message=message) assert helpers.effective_message_type(entity) == message_type diff --git a/tests/test_inlinekeyboardmarkup.py b/tests/test_inlinekeyboardmarkup.py index 58beb9e5b00..d852f685bf7 100644 --- a/tests/test_inlinekeyboardmarkup.py +++ b/tests/test_inlinekeyboardmarkup.py @@ -86,13 +86,19 @@ def test_from_column(self): assert len(inline_keyboard_markup[1]) == 1 def test_expected_values(self, inline_keyboard_markup): - assert inline_keyboard_markup.inline_keyboard == self.inline_keyboard + assert inline_keyboard_markup.inline_keyboard == tuple( + tuple(row) for row in self.inline_keyboard + ) def test_wrong_keyboard_inputs(self): with pytest.raises(ValueError): InlineKeyboardMarkup( [[InlineKeyboardButton("b1", "1")], InlineKeyboardButton("b2", "2")] ) + with pytest.raises(ValueError): + InlineKeyboardMarkup("strings_are_not_allowed") + with pytest.raises(ValueError): + InlineKeyboardMarkup(["strings_are_not_allowed_in_the_rows_either"]) with pytest.raises(ValueError): InlineKeyboardMarkup(InlineKeyboardButton("b1", "1")) @@ -121,8 +127,10 @@ async def make_assertion( assert bool("'switch_inline_query': ''" in str(data["reply_markup"])) assert bool("'switch_inline_query_current_chat': ''" in str(data["reply_markup"])) + inline_keyboard_markup.inline_keyboard[0][0]._unfreeze() inline_keyboard_markup.inline_keyboard[0][0].callback_data = None inline_keyboard_markup.inline_keyboard[0][0].switch_inline_query = "" + inline_keyboard_markup.inline_keyboard[0][1]._unfreeze() inline_keyboard_markup.inline_keyboard[0][1].callback_data = None inline_keyboard_markup.inline_keyboard[0][1].switch_inline_query_current_chat = "" diff --git a/tests/test_inlinequeryhandler.py b/tests/test_inlinequeryhandler.py index cf55b59fe0f..39f119f06ad 100644 --- a/tests/test_inlinequeryhandler.py +++ b/tests/test_inlinequeryhandler.py @@ -69,7 +69,7 @@ def false_update(request): @pytest.fixture(scope="function") def inline_query(bot): - return Update( + update = Update( 0, inline_query=InlineQuery( "id", @@ -79,6 +79,9 @@ def inline_query(bot): location=Location(latitude=-23.691288, longitude=-46.788279), ), ) + update._unfreeze() + update.inline_query._unfreeze() + return update class TestInlineQueryHandler: @@ -143,6 +146,7 @@ async def test_context_pattern(self, app, inline_query): update = Update( update_id=0, inline_query=InlineQuery(id="id", from_user=None, query="", offset="") ) + update.inline_query._unfreeze() assert not handler.check_update(update) update.inline_query.query = "not_a_match" assert not handler.check_update(update) diff --git a/tests/test_inlinequeryresultaudio.py b/tests/test_inlinequeryresultaudio.py index 4d52de0e22b..e7056f62465 100644 --- a/tests/test_inlinequeryresultaudio.py +++ b/tests/test_inlinequeryresultaudio.py @@ -73,7 +73,7 @@ def test_expected_values(self, inline_query_result_audio): assert inline_query_result_audio.audio_duration == self.audio_duration assert inline_query_result_audio.caption == self.caption assert inline_query_result_audio.parse_mode == self.parse_mode - assert inline_query_result_audio.caption_entities == self.caption_entities + assert inline_query_result_audio.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_audio.input_message_content.to_dict() == self.input_message_content.to_dict() @@ -107,6 +107,10 @@ def test_to_dict(self, inline_query_result_audio): == inline_query_result_audio.reply_markup.to_dict() ) + def test_caption_entities_always_tuple(self): + inline_query_result_audio = InlineQueryResultAudio(self.id_, self.audio_url, self.title) + assert inline_query_result_audio.caption_entities == () + def test_equality(self): a = InlineQueryResultAudio(self.id_, self.audio_url, self.title) b = InlineQueryResultAudio(self.id_, self.title, self.title) diff --git a/tests/test_inlinequeryresultcachedaudio.py b/tests/test_inlinequeryresultcachedaudio.py index d673193922d..ac654e915a5 100644 --- a/tests/test_inlinequeryresultcachedaudio.py +++ b/tests/test_inlinequeryresultcachedaudio.py @@ -64,7 +64,7 @@ def test_expected_values(self, inline_query_result_cached_audio): assert inline_query_result_cached_audio.audio_file_id == self.audio_file_id assert inline_query_result_cached_audio.caption == self.caption assert inline_query_result_cached_audio.parse_mode == self.parse_mode - assert inline_query_result_cached_audio.caption_entities == self.caption_entities + assert inline_query_result_cached_audio.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_audio.input_message_content.to_dict() == self.input_message_content.to_dict() @@ -73,6 +73,10 @@ def test_expected_values(self, inline_query_result_cached_audio): inline_query_result_cached_audio.reply_markup.to_dict() == self.reply_markup.to_dict() ) + def test_caption_entities_always_tuple(self): + audio = InlineQueryResultCachedAudio(self.id_, self.audio_file_id) + assert audio.caption_entities == () + def test_to_dict(self, inline_query_result_cached_audio): inline_query_result_cached_audio_dict = inline_query_result_cached_audio.to_dict() diff --git a/tests/test_inlinequeryresultcacheddocument.py b/tests/test_inlinequeryresultcacheddocument.py index 98ae3638355..e728d333d17 100644 --- a/tests/test_inlinequeryresultcacheddocument.py +++ b/tests/test_inlinequeryresultcacheddocument.py @@ -69,7 +69,7 @@ def test_expected_values(self, inline_query_result_cached_document): assert inline_query_result_cached_document.title == self.title assert inline_query_result_cached_document.caption == self.caption assert inline_query_result_cached_document.parse_mode == self.parse_mode - assert inline_query_result_cached_document.caption_entities == self.caption_entities + assert inline_query_result_cached_document.caption_entities == tuple(self.caption_entities) assert inline_query_result_cached_document.description == self.description assert ( inline_query_result_cached_document.input_message_content.to_dict() @@ -80,6 +80,10 @@ def test_expected_values(self, inline_query_result_cached_document): == self.reply_markup.to_dict() ) + def test_caption_entities_always_tuple(self): + test = InlineQueryResultCachedDocument(self.id_, self.title, self.document_file_id) + assert test.caption_entities == () + def test_to_dict(self, inline_query_result_cached_document): inline_query_result_cached_document_dict = inline_query_result_cached_document.to_dict() diff --git a/tests/test_inlinequeryresultcachedgif.py b/tests/test_inlinequeryresultcachedgif.py index 01fa426e09c..7af70a7daa7 100644 --- a/tests/test_inlinequeryresultcachedgif.py +++ b/tests/test_inlinequeryresultcachedgif.py @@ -66,13 +66,17 @@ def test_expected_values(self, inline_query_result_cached_gif): assert inline_query_result_cached_gif.title == self.title assert inline_query_result_cached_gif.caption == self.caption assert inline_query_result_cached_gif.parse_mode == self.parse_mode - assert inline_query_result_cached_gif.caption_entities == self.caption_entities + assert inline_query_result_cached_gif.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_gif.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_cached_gif.reply_markup.to_dict() == self.reply_markup.to_dict() + def test_caption_entities_always_tuple(self): + result = InlineQueryResultCachedGif(self.id_, self.gif_file_id) + assert result.caption_entities == () + def test_to_dict(self, inline_query_result_cached_gif): inline_query_result_cached_gif_dict = inline_query_result_cached_gif.to_dict() diff --git a/tests/test_inlinequeryresultcachedmpeg4gif.py b/tests/test_inlinequeryresultcachedmpeg4gif.py index 4ca209d88a6..df273eff1af 100644 --- a/tests/test_inlinequeryresultcachedmpeg4gif.py +++ b/tests/test_inlinequeryresultcachedmpeg4gif.py @@ -66,7 +66,9 @@ def test_expected_values(self, inline_query_result_cached_mpeg4_gif): assert inline_query_result_cached_mpeg4_gif.title == self.title assert inline_query_result_cached_mpeg4_gif.caption == self.caption assert inline_query_result_cached_mpeg4_gif.parse_mode == self.parse_mode - assert inline_query_result_cached_mpeg4_gif.caption_entities == self.caption_entities + assert inline_query_result_cached_mpeg4_gif.caption_entities == tuple( + self.caption_entities + ) assert ( inline_query_result_cached_mpeg4_gif.input_message_content.to_dict() == self.input_message_content.to_dict() @@ -76,6 +78,10 @@ def test_expected_values(self, inline_query_result_cached_mpeg4_gif): == self.reply_markup.to_dict() ) + def test_caption_entities_always_tuple(self): + result = InlineQueryResultCachedMpeg4Gif(self.id_, self.mpeg4_file_id) + assert result.caption_entities == () + def test_to_dict(self, inline_query_result_cached_mpeg4_gif): inline_query_result_cached_mpeg4_gif_dict = inline_query_result_cached_mpeg4_gif.to_dict() diff --git a/tests/test_inlinequeryresultcachedphoto.py b/tests/test_inlinequeryresultcachedphoto.py index cbd0789467e..2ef9fbe62cf 100644 --- a/tests/test_inlinequeryresultcachedphoto.py +++ b/tests/test_inlinequeryresultcachedphoto.py @@ -69,7 +69,7 @@ def test_expected_values(self, inline_query_result_cached_photo): assert inline_query_result_cached_photo.description == self.description assert inline_query_result_cached_photo.caption == self.caption assert inline_query_result_cached_photo.parse_mode == self.parse_mode - assert inline_query_result_cached_photo.caption_entities == self.caption_entities + assert inline_query_result_cached_photo.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_photo.input_message_content.to_dict() == self.input_message_content.to_dict() @@ -78,6 +78,10 @@ def test_expected_values(self, inline_query_result_cached_photo): inline_query_result_cached_photo.reply_markup.to_dict() == self.reply_markup.to_dict() ) + def test_caption_entities_always_tuple(self): + result = InlineQueryResultCachedPhoto(self.id_, self.photo_file_id) + assert result.caption_entities == () + def test_to_dict(self, inline_query_result_cached_photo): inline_query_result_cached_photo_dict = inline_query_result_cached_photo.to_dict() diff --git a/tests/test_inlinequeryresultcachedvideo.py b/tests/test_inlinequeryresultcachedvideo.py index c71fffe1298..c7861e0ce0c 100644 --- a/tests/test_inlinequeryresultcachedvideo.py +++ b/tests/test_inlinequeryresultcachedvideo.py @@ -69,7 +69,7 @@ def test_expected_values(self, inline_query_result_cached_video): assert inline_query_result_cached_video.description == self.description assert inline_query_result_cached_video.caption == self.caption assert inline_query_result_cached_video.parse_mode == self.parse_mode - assert inline_query_result_cached_video.caption_entities == self.caption_entities + assert inline_query_result_cached_video.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_video.input_message_content.to_dict() == self.input_message_content.to_dict() @@ -78,6 +78,11 @@ def test_expected_values(self, inline_query_result_cached_video): inline_query_result_cached_video.reply_markup.to_dict() == self.reply_markup.to_dict() ) + def test_caption_entities_always_tuple(self): + video = InlineQueryResultCachedVideo(self.id_, self.video_file_id, self.title) + + assert video.caption_entities == () + def test_to_dict(self, inline_query_result_cached_video): inline_query_result_cached_video_dict = inline_query_result_cached_video.to_dict() diff --git a/tests/test_inlinequeryresultcachedvoice.py b/tests/test_inlinequeryresultcachedvoice.py index 400cf8e82c0..c4033edc386 100644 --- a/tests/test_inlinequeryresultcachedvoice.py +++ b/tests/test_inlinequeryresultcachedvoice.py @@ -66,7 +66,7 @@ def test_expected_values(self, inline_query_result_cached_voice): assert inline_query_result_cached_voice.title == self.title assert inline_query_result_cached_voice.caption == self.caption assert inline_query_result_cached_voice.parse_mode == self.parse_mode - assert inline_query_result_cached_voice.caption_entities == self.caption_entities + assert inline_query_result_cached_voice.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_voice.input_message_content.to_dict() == self.input_message_content.to_dict() @@ -75,6 +75,10 @@ def test_expected_values(self, inline_query_result_cached_voice): inline_query_result_cached_voice.reply_markup.to_dict() == self.reply_markup.to_dict() ) + def test_caption_entities_always_tuple(self): + result = InlineQueryResultCachedVoice(self.id_, self.voice_file_id, self.title) + assert result.caption_entities == () + def test_to_dict(self, inline_query_result_cached_voice): inline_query_result_cached_voice_dict = inline_query_result_cached_voice.to_dict() diff --git a/tests/test_inlinequeryresultdocument.py b/tests/test_inlinequeryresultdocument.py index 168a98de115..2915e7451e6 100644 --- a/tests/test_inlinequeryresultdocument.py +++ b/tests/test_inlinequeryresultdocument.py @@ -76,7 +76,7 @@ def test_expected_values(self, inline_query_result_document): assert inline_query_result_document.title == self.title assert inline_query_result_document.caption == self.caption assert inline_query_result_document.parse_mode == self.parse_mode - assert inline_query_result_document.caption_entities == self.caption_entities + assert inline_query_result_document.caption_entities == tuple(self.caption_entities) assert inline_query_result_document.mime_type == self.mime_type assert inline_query_result_document.description == self.description assert inline_query_result_document.thumb_url == self.thumb_url @@ -88,6 +88,10 @@ def test_expected_values(self, inline_query_result_document): ) assert inline_query_result_document.reply_markup.to_dict() == self.reply_markup.to_dict() + def test_caption_entities_always_tuple(self): + result = InlineQueryResultDocument(self.id_, self.document_url, self.title, self.mime_type) + assert result.caption_entities == () + def test_to_dict(self, inline_query_result_document): inline_query_result_document_dict = inline_query_result_document.to_dict() diff --git a/tests/test_inlinequeryresultgif.py b/tests/test_inlinequeryresultgif.py index 10f08ba6fd5..f0242148155 100644 --- a/tests/test_inlinequeryresultgif.py +++ b/tests/test_inlinequeryresultgif.py @@ -69,6 +69,10 @@ def test_slot_behaviour(self, inline_query_result_gif, mro_slots): assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + def test_caption_entities_always_tuple(self): + result = InlineQueryResultGif(self.id_, self.gif_url, self.thumb_url) + assert result.caption_entities == () + def test_expected_values(self, inline_query_result_gif): assert inline_query_result_gif.type == self.type_ assert inline_query_result_gif.id == self.id_ @@ -81,7 +85,7 @@ def test_expected_values(self, inline_query_result_gif): assert inline_query_result_gif.title == self.title assert inline_query_result_gif.caption == self.caption assert inline_query_result_gif.parse_mode == self.parse_mode - assert inline_query_result_gif.caption_entities == self.caption_entities + assert inline_query_result_gif.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_gif.input_message_content.to_dict() == self.input_message_content.to_dict() diff --git a/tests/test_inlinequeryresultmpeg4gif.py b/tests/test_inlinequeryresultmpeg4gif.py index 54b72357920..2c564d94fd7 100644 --- a/tests/test_inlinequeryresultmpeg4gif.py +++ b/tests/test_inlinequeryresultmpeg4gif.py @@ -81,13 +81,17 @@ def test_expected_values(self, inline_query_result_mpeg4_gif): assert inline_query_result_mpeg4_gif.title == self.title assert inline_query_result_mpeg4_gif.caption == self.caption assert inline_query_result_mpeg4_gif.parse_mode == self.parse_mode - assert inline_query_result_mpeg4_gif.caption_entities == self.caption_entities + assert inline_query_result_mpeg4_gif.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_mpeg4_gif.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_mpeg4_gif.reply_markup.to_dict() == self.reply_markup.to_dict() + def test_caption_entities_always_tuple(self): + result = InlineQueryResultMpeg4Gif(self.id_, self.mpeg4_url, self.thumb_url) + assert result.caption_entities == () + def test_to_dict(self, inline_query_result_mpeg4_gif): inline_query_result_mpeg4_gif_dict = inline_query_result_mpeg4_gif.to_dict() diff --git a/tests/test_inlinequeryresultphoto.py b/tests/test_inlinequeryresultphoto.py index 9d594440ccd..0e40fc5a3db 100644 --- a/tests/test_inlinequeryresultphoto.py +++ b/tests/test_inlinequeryresultphoto.py @@ -79,13 +79,17 @@ def test_expected_values(self, inline_query_result_photo): assert inline_query_result_photo.description == self.description assert inline_query_result_photo.caption == self.caption assert inline_query_result_photo.parse_mode == self.parse_mode - assert inline_query_result_photo.caption_entities == self.caption_entities + assert inline_query_result_photo.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_photo.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_photo.reply_markup.to_dict() == self.reply_markup.to_dict() + def test_caption_entities_always_tuple(self): + result = InlineQueryResultPhoto(self.id_, self.photo_url, self.thumb_url) + assert result.caption_entities == () + def test_to_dict(self, inline_query_result_photo): inline_query_result_photo_dict = inline_query_result_photo.to_dict() diff --git a/tests/test_inlinequeryresultvideo.py b/tests/test_inlinequeryresultvideo.py index c7904873387..dad63f132be 100644 --- a/tests/test_inlinequeryresultvideo.py +++ b/tests/test_inlinequeryresultvideo.py @@ -84,13 +84,19 @@ def test_expected_values(self, inline_query_result_video): assert inline_query_result_video.description == self.description assert inline_query_result_video.caption == self.caption assert inline_query_result_video.parse_mode == self.parse_mode - assert inline_query_result_video.caption_entities == self.caption_entities + assert inline_query_result_video.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_video.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_video.reply_markup.to_dict() == self.reply_markup.to_dict() + def test_caption_entities_always_tuple(self): + video = InlineQueryResultVideo( + self.id_, self.video_url, self.mime_type, self.thumb_url, self.title + ) + assert video.caption_entities == () + def test_to_dict(self, inline_query_result_video): inline_query_result_video_dict = inline_query_result_video.to_dict() diff --git a/tests/test_inlinequeryresultvoice.py b/tests/test_inlinequeryresultvoice.py index 03058e37507..0a42aa86402 100644 --- a/tests/test_inlinequeryresultvoice.py +++ b/tests/test_inlinequeryresultvoice.py @@ -69,13 +69,22 @@ def test_expected_values(self, inline_query_result_voice): assert inline_query_result_voice.voice_duration == self.voice_duration assert inline_query_result_voice.caption == self.caption assert inline_query_result_voice.parse_mode == self.parse_mode - assert inline_query_result_voice.caption_entities == self.caption_entities + assert inline_query_result_voice.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_voice.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_voice.reply_markup.to_dict() == self.reply_markup.to_dict() + def test_caption_entities_always_tuple(self): + result = InlineQueryResultVoice( + self.id_, + self.voice_url, + self.title, + ) + + assert result.caption_entities == () + def test_to_dict(self, inline_query_result_voice): inline_query_result_voice_dict = inline_query_result_voice.to_dict() diff --git a/tests/test_inputinvoicemessagecontent.py b/tests/test_inputinvoicemessagecontent.py index 93a408d57ae..93b79d4cc73 100644 --- a/tests/test_inputinvoicemessagecontent.py +++ b/tests/test_inputinvoicemessagecontent.py @@ -82,11 +82,11 @@ def test_expected_values(self, input_invoice_message_content): assert input_invoice_message_content.payload == self.payload assert input_invoice_message_content.provider_token == self.provider_token assert input_invoice_message_content.currency == self.currency - assert input_invoice_message_content.prices == self.prices + assert input_invoice_message_content.prices == tuple(self.prices) assert input_invoice_message_content.max_tip_amount == self.max_tip_amount - assert input_invoice_message_content.suggested_tip_amounts == [ + assert input_invoice_message_content.suggested_tip_amounts == tuple( int(amount) for amount in self.suggested_tip_amounts - ] + ) assert input_invoice_message_content.provider_data == self.provider_data assert input_invoice_message_content.photo_url == self.photo_url assert input_invoice_message_content.photo_size == int(self.photo_size) @@ -103,6 +103,44 @@ def test_expected_values(self, input_invoice_message_content): assert input_invoice_message_content.send_email_to_provider == self.send_email_to_provider assert input_invoice_message_content.is_flexible == self.is_flexible + def test_suggested_tip_amonuts_always_tuple(self): + input_invoice_message_content = InputInvoiceMessageContent( + title=self.title, + description=self.description, + payload=self.payload, + provider_token=self.provider_token, + currency=self.currency, + prices=self.prices, + max_tip_amount=self.max_tip_amount, + suggested_tip_amounts=self.suggested_tip_amounts, + provider_data=self.provider_data, + photo_url=self.photo_url, + photo_size=self.photo_size, + photo_width=self.photo_width, + photo_height=self.photo_height, + need_name=self.need_name, + need_phone_number=self.need_phone_number, + need_email=self.need_email, + need_shipping_address=self.need_shipping_address, + send_phone_number_to_provider=self.send_phone_number_to_provider, + send_email_to_provider=self.send_email_to_provider, + is_flexible=self.is_flexible, + ) + assert isinstance(input_invoice_message_content.suggested_tip_amounts, tuple) + assert input_invoice_message_content.suggested_tip_amounts == tuple( + int(amount) for amount in self.suggested_tip_amounts + ) + + input_invoice_message_content = InputInvoiceMessageContent( + title=self.title, + description=self.description, + payload=self.payload, + provider_token=self.provider_token, + currency=self.currency, + prices=self.prices, + ) + assert input_invoice_message_content.suggested_tip_amounts == tuple() + def test_to_dict(self, input_invoice_message_content): input_invoice_message_content_dict = input_invoice_message_content.to_dict() @@ -130,9 +168,8 @@ def test_to_dict(self, input_invoice_message_content): input_invoice_message_content_dict["max_tip_amount"] == input_invoice_message_content.max_tip_amount ) - assert ( - input_invoice_message_content_dict["suggested_tip_amounts"] - == input_invoice_message_content.suggested_tip_amounts + assert input_invoice_message_content_dict["suggested_tip_amounts"] == list( + input_invoice_message_content.suggested_tip_amounts ) assert ( input_invoice_message_content_dict["provider_data"] @@ -217,11 +254,11 @@ def test_de_json(self, bot): assert input_invoice_message_content.payload == self.payload assert input_invoice_message_content.provider_token == self.provider_token assert input_invoice_message_content.currency == self.currency - assert input_invoice_message_content.prices == self.prices + assert input_invoice_message_content.prices == tuple(self.prices) assert input_invoice_message_content.max_tip_amount == self.max_tip_amount - assert input_invoice_message_content.suggested_tip_amounts == [ + assert input_invoice_message_content.suggested_tip_amounts == tuple( int(amount) for amount in self.suggested_tip_amounts - ] + ) assert input_invoice_message_content.provider_data == self.provider_data assert input_invoice_message_content.photo_url == self.photo_url assert input_invoice_message_content.photo_size == int(self.photo_size) diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index 2f261d19068..d7f0e96789d 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -145,10 +145,14 @@ def test_expected_values(self, input_media_video): assert input_media_video.height == self.height assert input_media_video.duration == self.duration assert input_media_video.parse_mode == self.parse_mode - assert input_media_video.caption_entities == self.caption_entities + assert input_media_video.caption_entities == tuple(self.caption_entities) assert input_media_video.supports_streaming == self.supports_streaming assert isinstance(input_media_video.thumb, InputFile) + def test_caption_entities_always_tuple(self): + input_media_video = InputMediaVideo(self.media) + assert input_media_video.caption_entities == () + def test_to_dict(self, input_media_video): input_media_video_dict = input_media_video.to_dict() assert input_media_video_dict["type"] == input_media_video.type @@ -206,7 +210,11 @@ def test_expected_values(self, input_media_photo): assert input_media_photo.media == self.media assert input_media_photo.caption == self.caption assert input_media_photo.parse_mode == self.parse_mode - assert input_media_photo.caption_entities == self.caption_entities + assert input_media_photo.caption_entities == tuple(self.caption_entities) + + def test_caption_entities_always_tuple(self): + input_media_photo = InputMediaPhoto(self.media) + assert input_media_photo.caption_entities == () def test_to_dict(self, input_media_photo): input_media_photo_dict = input_media_photo.to_dict() @@ -258,9 +266,13 @@ def test_expected_values(self, input_media_animation): assert input_media_animation.media == self.media assert input_media_animation.caption == self.caption assert input_media_animation.parse_mode == self.parse_mode - assert input_media_animation.caption_entities == self.caption_entities + assert input_media_animation.caption_entities == tuple(self.caption_entities) assert isinstance(input_media_animation.thumb, InputFile) + def test_caption_entities_always_tuple(self): + input_media_animation = InputMediaAnimation(self.media) + assert input_media_animation.caption_entities == () + def test_to_dict(self, input_media_animation): input_media_animation_dict = input_media_animation.to_dict() assert input_media_animation_dict["type"] == input_media_animation.type @@ -320,9 +332,13 @@ def test_expected_values(self, input_media_audio): assert input_media_audio.performer == self.performer assert input_media_audio.title == self.title assert input_media_audio.parse_mode == self.parse_mode - assert input_media_audio.caption_entities == self.caption_entities + assert input_media_audio.caption_entities == tuple(self.caption_entities) assert isinstance(input_media_audio.thumb, InputFile) + def test_caption_entities_always_tuple(self): + input_media_audio = InputMediaAudio(self.media) + assert input_media_audio.caption_entities == () + def test_to_dict(self, input_media_audio): input_media_audio_dict = input_media_audio.to_dict() assert input_media_audio_dict["type"] == input_media_audio.type @@ -380,13 +396,17 @@ def test_expected_values(self, input_media_document): assert input_media_document.media == self.media assert input_media_document.caption == self.caption assert input_media_document.parse_mode == self.parse_mode - assert input_media_document.caption_entities == self.caption_entities + assert input_media_document.caption_entities == tuple(self.caption_entities) assert ( input_media_document.disable_content_type_detection == self.disable_content_type_detection ) assert isinstance(input_media_document.thumb, InputFile) + def test_caption_entities_always_tuple(self): + input_media_document = InputMediaDocument(self.media) + assert input_media_document.caption_entities == () + def test_to_dict(self, input_media_document): input_media_document_dict = input_media_document.to_dict() assert input_media_document_dict["type"] == input_media_document.type @@ -459,13 +479,13 @@ class TestSendMediaGroup: @pytest.mark.flaky(3, 1) async def test_send_media_group_photo(self, bot, chat_id, media_group): messages = await bot.send_media_group(chat_id, media_group) - assert isinstance(messages, list) + assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) assert all(mes.caption == f"photo {idx+1}" for idx, mes in enumerate(messages)) assert all( - mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] for mes in messages + mes.caption_entities == (MessageEntity(MessageEntity.BOLD, 0, 5),) for mes in messages ) async def test_send_media_group_with_message_thread_id( @@ -476,7 +496,7 @@ async def test_send_media_group_with_message_thread_id( media_group, message_thread_id=real_topic.message_thread_id, ) - assert isinstance(messages, list) + assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(i.message_thread_id == real_topic.message_thread_id for i in messages) @@ -538,7 +558,7 @@ async def test_send_media_group_with_group_caption( assert not any(item.parse_mode for item in media_group_no_caption_args) - assert isinstance(messages, list) + assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) @@ -548,7 +568,7 @@ async def test_send_media_group_with_group_caption( # Make sure first message got the caption, which will lead # to Telegram displaying its caption as group caption assert first_message.caption - assert first_message.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] + assert first_message.caption_entities == (MessageEntity(MessageEntity.BOLD, 0, 5),) # Check that other messages have no captions assert all(mes.caption is None for mes in other_messages) @@ -578,13 +598,13 @@ async def test_send_media_group_all_args(self, bot, raw_bot, chat_id, media_grou a.parse_mode == b.parse_mode for a, b in zip(media_group, copied_media_group) ) - assert isinstance(messages, list) + assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) assert all(mes.caption == f"photo {idx+1}" for idx, mes in enumerate(messages)) assert all( - mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] + mes.caption_entities == (MessageEntity(MessageEntity.BOLD, 0, 5),) for mes in messages ) assert all(mes.has_protected_content for mes in messages) @@ -658,7 +678,7 @@ async def func(): func, "Type of file mismatch", "Telegram did not accept the file." ) - assert isinstance(messages, list) + assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) @@ -744,7 +764,7 @@ async def test_send_media_group_default_parse_mode( for mes_group in (default, overridden_markdown_v2): first_message = mes_group[0] assert first_message.caption == "photo 1" - assert first_message.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] + assert first_message.caption_entities == (MessageEntity(MessageEntity.BOLD, 0, 5),) # This check is valid for all 3 groups of messages for mes_group in (default, overridden_markdown_v2, overridden_none): @@ -854,7 +874,7 @@ def build_media(parse_mode, med_type): message.message_id, ) assert message.caption == test_caption - assert message.caption_entities == test_entities + assert message.caption_entities == tuple(test_entities) # make sure that the media was not modified assert media.parse_mode == copied_media.parse_mode @@ -869,7 +889,7 @@ def build_media(parse_mode, med_type): message.message_id, ) assert message.caption == test_caption - assert message.caption_entities == test_entities + assert message.caption_entities == tuple(test_entities) # make sure that the media was not modified assert media.parse_mode == copied_media.parse_mode @@ -884,6 +904,6 @@ def build_media(parse_mode, med_type): message.message_id, ) assert message.caption == markdown_caption - assert message.caption_entities == [] + assert message.caption_entities == () # make sure that the media was not modified assert media.parse_mode == copied_media.parse_mode diff --git a/tests/test_inputtextmessagecontent.py b/tests/test_inputtextmessagecontent.py index 9d6181ed7e4..6f288deebe2 100644 --- a/tests/test_inputtextmessagecontent.py +++ b/tests/test_inputtextmessagecontent.py @@ -48,7 +48,11 @@ def test_expected_values(self, input_text_message_content): assert input_text_message_content.parse_mode == self.parse_mode assert input_text_message_content.message_text == self.message_text assert input_text_message_content.disable_web_page_preview == self.disable_web_page_preview - assert input_text_message_content.entities == self.entities + assert input_text_message_content.entities == tuple(self.entities) + + def test_entities_always_tuple(self): + input_text_message_content = InputTextMessageContent("text") + assert input_text_message_content.entities == () def test_to_dict(self, input_text_message_content): input_text_message_content_dict = input_text_message_content.to_dict() diff --git a/tests/test_message.py b/tests/test_message.py index 97f25725a52..7891d1e43ea 100644 --- a/tests/test_message.py +++ b/tests/test_message.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/]. +from copy import copy from datetime import datetime import pytest @@ -68,10 +69,13 @@ def message(bot): message = Message( message_id=TestMessage.id_, date=TestMessage.date, - chat=TestMessage.chat, - from_user=TestMessage.from_user, + chat=copy(TestMessage.chat), + from_user=copy(TestMessage.from_user), ) message.set_bot(bot) + message._unfreeze() + message.chat._unfreeze() + message.from_user._unfreeze() return message diff --git a/tests/test_messagehandler.py b/tests/test_messagehandler.py index daf8a451588..54ee4076c87 100644 --- a/tests/test_messagehandler.py +++ b/tests/test_messagehandler.py @@ -65,6 +65,8 @@ def false_update(request): @pytest.fixture(scope="class") def message(bot): message = Message(1, None, Chat(1, ""), from_user=User(1, "", False)) + message._unfreeze() + message.chat._unfreeze() message.set_bot(bot) return message diff --git a/tests/test_passport.py b/tests/test_passport.py index 72ac4412e3f..77f9a0c91ea 100644 --- a/tests/test_passport.py +++ b/tests/test_passport.py @@ -523,6 +523,7 @@ def test_equality(self, passport_data): assert hash(a) == hash(b) assert a is not b + passport_data.credentials._unfreeze() passport_data.credentials.hash = "NOTAPROPERHASH" c = PassportData(passport_data.data, passport_data.credentials) diff --git a/tests/test_photo.py b/tests/test_photo.py index ea2b364ed3f..4d7baeeec48 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -186,7 +186,7 @@ async def test_send_photo_caption_entities(self, bot, chat_id, photo_file, thumb ) assert message.caption == test_string - assert message.caption_entities == entities + assert message.caption_entities == tuple(entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) diff --git a/tests/test_poll.py b/tests/test_poll.py index 0d15a0207f5..425c4eadf1a 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -26,7 +26,9 @@ @pytest.fixture(scope="class") def poll_option(): - return PollOption(text=TestPollOption.text, voter_count=TestPollOption.voter_count) + out = PollOption(text=TestPollOption.text, voter_count=TestPollOption.voter_count) + out._unfreeze() + return out class TestPollOption: @@ -96,7 +98,7 @@ def test_de_json(self): assert poll_answer.poll_id == self.poll_id assert poll_answer.user == self.user - assert poll_answer.option_ids == self.option_ids + assert poll_answer.option_ids == tuple(self.option_ids) def test_to_dict(self, poll_answer): poll_answer_dict = poll_answer.to_dict() @@ -104,7 +106,7 @@ def test_to_dict(self, poll_answer): assert isinstance(poll_answer_dict, dict) assert poll_answer_dict["poll_id"] == poll_answer.poll_id assert poll_answer_dict["user"] == poll_answer.user.to_dict() - assert poll_answer_dict["option_ids"] == poll_answer.option_ids + assert poll_answer_dict["option_ids"] == list(poll_answer.option_ids) def test_equality(self): a = PollAnswer(123, self.user, [2]) @@ -128,7 +130,7 @@ def test_equality(self): @pytest.fixture(scope="class") def poll(): - return Poll( + poll = Poll( TestPoll.id_, TestPoll.question, TestPoll.options, @@ -142,6 +144,8 @@ def poll(): open_period=TestPoll.open_period, close_date=TestPoll.close_date, ) + poll._unfreeze() + return poll class TestPoll: @@ -181,7 +185,7 @@ def test_de_json(self, bot): assert poll.id == self.id_ assert poll.question == self.question - assert poll.options == self.options + assert poll.options == tuple(self.options) assert poll.options[0].text == self.options[0].text assert poll.options[0].voter_count == self.options[0].voter_count assert poll.options[1].text == self.options[1].text @@ -192,7 +196,7 @@ def test_de_json(self, bot): assert poll.type == self.type assert poll.allows_multiple_answers == self.allows_multiple_answers assert poll.explanation == self.explanation - assert poll.explanation_entities == self.explanation_entities + assert poll.explanation_entities == tuple(self.explanation_entities) assert poll.open_period == self.open_period assert abs(poll.close_date - self.close_date) < timedelta(seconds=1) assert to_timestamp(poll.close_date) == to_timestamp(self.close_date) diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py index b33046fed4a..56b1366e016 100644 --- a/tests/test_replykeyboardmarkup.py +++ b/tests/test_replykeyboardmarkup.py @@ -96,7 +96,8 @@ def test_from_column(self): assert len(reply_keyboard_markup[1]) == 1 def test_expected_values(self, reply_keyboard_markup): - assert isinstance(reply_keyboard_markup.keyboard, list) + assert isinstance(reply_keyboard_markup.keyboard, tuple) + assert all(isinstance(row, tuple) for row in reply_keyboard_markup.keyboard) assert isinstance(reply_keyboard_markup.keyboard[0][0], KeyboardButton) assert isinstance(reply_keyboard_markup.keyboard[0][1], KeyboardButton) assert reply_keyboard_markup.resize_keyboard == self.resize_keyboard @@ -105,9 +106,13 @@ def test_expected_values(self, reply_keyboard_markup): def test_wrong_keyboard_inputs(self): with pytest.raises(ValueError): - ReplyKeyboardMarkup([["button1"], "Button2"]) + ReplyKeyboardMarkup([["button1"], 1]) with pytest.raises(ValueError): - ReplyKeyboardMarkup("button") + ReplyKeyboardMarkup("strings_are_not_allowed") + with pytest.raises(ValueError): + ReplyKeyboardMarkup(["strings_are_not_allowed_in_the_rows_either"]) + with pytest.raises(ValueError): + ReplyKeyboardMarkup(KeyboardButton("button1")) def test_to_dict(self, reply_keyboard_markup): reply_keyboard_markup_dict = reply_keyboard_markup.to_dict() diff --git a/tests/test_shippingoption.py b/tests/test_shippingoption.py index 910d275433a..663a5ad2712 100644 --- a/tests/test_shippingoption.py +++ b/tests/test_shippingoption.py @@ -42,7 +42,7 @@ def test_slot_behaviour(self, shipping_option, mro_slots): def test_expected_values(self, shipping_option): assert shipping_option.id == self.id_ assert shipping_option.title == self.title - assert shipping_option.prices == self.prices + assert shipping_option.prices == tuple(self.prices) def test_to_dict(self, shipping_option): shipping_option_dict = shipping_option.to_dict() diff --git a/tests/test_sticker.py b/tests/test_sticker.py index 14b006680d6..fe96bd6ac55 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -505,7 +505,7 @@ def test_de_json(self, bot, sticker): assert sticker_set.title == self.title assert sticker_set.is_animated == self.is_animated assert sticker_set.is_video == self.is_video - assert sticker_set.stickers == self.stickers + assert sticker_set.stickers == tuple(self.stickers) assert sticker_set.thumb == sticker.thumb assert sticker_set.sticker_type == self.sticker_type assert sticker_set.api_kwargs == {"contains_masks": self.contains_masks} @@ -824,7 +824,7 @@ def test_equality(self): self.is_video, self.sticker_type, ) - c = StickerSet(self.name, None, None, None, None, Sticker.CUSTOM_EMOJI) + c = StickerSet(self.name, "title", False, [], True, Sticker.CUSTOM_EMOJI) d = StickerSet( "blah", self.title, diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 02549b06c2d..132f70770f1 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -19,8 +19,10 @@ import datetime import inspect import pickle +import re from copy import deepcopy from pathlib import Path +from types import MappingProxyType import pytest @@ -57,17 +59,20 @@ class ChangingTO(TelegramObject): pass def test_to_json(self, monkeypatch): - # to_json simply takes whatever comes from to_dict, therefore we only need to test it once - telegram_object = TelegramObject() + class Subclass(TelegramObject): + def __init__(self): + super().__init__() + self.arg = "arg" + self.arg2 = ["arg2", "arg2"] + self.arg3 = {"arg3": "arg3"} + self.empty_tuple = () - # Test that it works with a dict with str keys as well as dicts as lists as values - d = {"str": "str", "str2": ["str", "str"], "str3": {"str": "str"}} - monkeypatch.setattr("telegram.TelegramObject.to_dict", lambda _: d) - json = telegram_object.to_json() + json = Subclass().to_json() # Order isn't guarantied - assert '"str": "str"' in json - assert '"str2": ["str", "str"]' in json - assert '"str3": {"str": "str"}' in json + assert '"arg": "arg"' in json + assert '"arg2": ["arg2", "arg2"]' in json + assert '"arg3": {"arg3": "arg3"}' in json + assert "empty_tuple" not in json # Now make sure that it doesn't work with not json stuff and that it fails loudly # Tuples aren't allowed as keys in json @@ -75,13 +80,35 @@ def test_to_json(self, monkeypatch): monkeypatch.setattr("telegram.TelegramObject.to_dict", lambda _: d) with pytest.raises(TypeError): - telegram_object.to_json() + TelegramObject().to_json() def test_de_json_api_kwargs(self, bot): to = TelegramObject.de_json(data={"foo": "bar"}, bot=bot) assert to.api_kwargs == {"foo": "bar"} assert to.get_bot() is bot + def test_de_list(self, bot): + class SubClass(TelegramObject): + def __init__(self, arg: int, **kwargs): + super().__init__(**kwargs) + self.arg = arg + + self._id_attrs = (self.arg,) + + assert SubClass.de_list([{"arg": 1}, None, {"arg": 2}, None], bot) == ( + SubClass(1), + SubClass(2), + ) + + def test_api_kwargs_read_only(self): + tg_object = TelegramObject(api_kwargs={"foo": "bar"}) + tg_object._freeze() + assert isinstance(tg_object.api_kwargs, MappingProxyType) + with pytest.raises(TypeError): + tg_object.api_kwargs["foo"] = "baz" + with pytest.raises(AttributeError, match="can't be set"): + tg_object.api_kwargs = {"foo": "baz"} + @pytest.mark.parametrize("cls", TO_SUBCLASSES, ids=[cls.__name__ for cls in TO_SUBCLASSES]) def test_subclasses_have_api_kwargs(self, cls): """Checks that all subclasses of TelegramObject have an api_kwargs argument that is @@ -144,6 +171,7 @@ def test_to_dict_missing_attribute(self): message = Message( 1, datetime.datetime.now(), Chat(1, "private"), from_user=User(1, "", False) ) + message._unfreeze() del message.chat message_dict = message.to_dict() @@ -242,7 +270,15 @@ def test_pickle(self, bot): date = datetime.datetime.now() photo = PhotoSize("file_id", "unique", 21, 21) photo.set_bot(bot) - msg = Message(1, date, chat, from_user=user, text="foobar", photo=[photo]) + msg = Message( + 1, + date, + chat, + from_user=user, + text="foobar", + photo=[photo], + api_kwargs={"api": "kwargs"}, + ) msg.set_bot(bot) # Test pickling of TGObjects, we choose Message since it's contains the most subclasses. @@ -256,6 +292,8 @@ def test_pickle(self, bot): assert unpickled.from_user == user assert unpickled.date == date, f"{unpickled.date} != {date}" assert unpickled.photo[0] == photo + assert isinstance(unpickled.api_kwargs, MappingProxyType) + assert unpickled.api_kwargs == {"api": "kwargs"} def test_pickle_apply_api_kwargs(self): """Makes sure that when a class gets new attributes, the api_kwargs are moved to the @@ -269,7 +307,7 @@ def test_pickle_apply_api_kwargs(self): assert obj.foo == "bar" assert obj.api_kwargs == {} - async def test_pickle_removed_and_added_attribute(self): + async def test_pickle_backwards_compatibility(self): """Test when newer versions of the library remove or add attributes from classes (which the old pickled versions still/don't have). """ @@ -295,13 +333,20 @@ async def test_pickle_removed_and_added_attribute(self): # New attribute should not be available either as is always the case for pickle chat.is_forum + # Ensure that loading objects that were pickled before attributes were made immutable + # are still mutable + chat.id = 7 + assert chat.id == 7 + def test_deepcopy_telegram_obj(self, bot): chat = Chat(2, Chat.PRIVATE) user = User(3, "first_name", False) date = datetime.datetime.now() photo = PhotoSize("file_id", "unique", 21, 21) photo.set_bot(bot) - msg = Message(1, date, chat, from_user=user, text="foobar", photo=[photo]) + msg = Message( + 1, date, chat, from_user=user, text="foobar", photo=[photo], api_kwargs={"foo": "bar"} + ) msg.set_bot(bot) new_msg = deepcopy(msg) @@ -316,6 +361,18 @@ def test_deepcopy_telegram_obj(self, bot): assert new_msg.chat == chat and new_msg.chat is not chat assert new_msg.from_user == user and new_msg.from_user is not user assert new_msg.photo[0] == photo and new_msg.photo[0] is not photo + assert new_msg.api_kwargs == {"foo": "bar"} and new_msg.api_kwargs is not msg.api_kwargs + + # check that deepcopy preserves the freezing status + with pytest.raises( + AttributeError, match="Attribute `text` of class `Message` can't be set!" + ): + new_msg.text = "new text" + + msg._unfreeze() + new_message = deepcopy(msg) + new_message.text = "new text" + assert new_message.text == "new text" def test_deepcopy_subclass_telegram_obj(self, bot): s = self.Sub("private", "normal", bot) @@ -368,3 +425,61 @@ def __init__(self, api_kwargs=None): ) assert str(TGO(api_kwargs={"foo": "bar"})) == expected_with_api_kwargs assert repr(TGO(api_kwargs={"foo": "bar"})) == expected_with_api_kwargs + + @pytest.mark.parametrize("cls", TO_SUBCLASSES, ids=[cls.__name__ for cls in TO_SUBCLASSES]) + def test_subclasses_are_frozen(self, cls): + if cls.__name__.startswith("_"): + return + + # instantiating each subclass would be tedious as some attributes require special init + # args. So we inspect the code instead. + + source_file = inspect.getsourcefile(cls.__init__) + parents = Path(source_file).parents + is_test_file = Path(__file__).parent.resolve() in parents + if is_test_file or source_file.endswith("telegramobject.py"): + # classes without their own `__init__` can be ignored + return + + source_lines, first_line = inspect.getsourcelines(cls.__init__) + + # We use regex matching since a simple "if self._freeze() in source_lines[-1]" would also + # allo commented lines. + last_line_freezes = re.match(r"\s*self\.\_freeze\(\)", source_lines[-1]) + uses_with_unfrozen = re.search( + r"\n\s*with self\.\_unfrozen\(\)\:", inspect.getsource(cls.__init__) + ) + + assert last_line_freezes or uses_with_unfrozen, f"{cls.__name__} is not frozen correctly" + + def test_freeze_unfreeze(self): + class TestSub(TelegramObject): + def __init__(self): + super().__init__() + self._protected = True + self.public = True + self._freeze() + + foo = TestSub() + foo._protected = False + assert foo._protected is False + + with pytest.raises( + AttributeError, match="Attribute `public` of class `TestSub` can't be set!" + ): + foo.public = False + + with pytest.raises( + AttributeError, match="Attribute `public` of class `TestSub` can't be deleted!" + ): + del foo.public + + foo._unfreeze() + foo._protected = True + assert foo._protected is True + foo.public = False + assert foo.public is False + del foo.public + del foo._protected + assert not hasattr(foo, "public") + assert not hasattr(foo, "_protected") diff --git a/tests/test_user.py b/tests/test_user.py index 3ee9f1fd6b3..56c3c6eadf4 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -60,6 +60,7 @@ def user(bot): added_to_attachment_menu=TestUser.added_to_attachment_menu, ) user.set_bot(bot) + user._unfreeze() return user diff --git a/tests/test_userprofilephotos.py b/tests/test_userprofilephotos.py index bd13c223b82..c8edb0daebe 100644 --- a/tests/test_userprofilephotos.py +++ b/tests/test_userprofilephotos.py @@ -43,7 +43,7 @@ def test_de_json(self, bot): user_profile_photos = UserProfilePhotos.de_json(json_dict, bot) assert user_profile_photos.api_kwargs == {} assert user_profile_photos.total_count == self.total_count - assert user_profile_photos.photos == self.photos + assert user_profile_photos.photos == tuple(tuple(p) for p in self.photos) def test_to_dict(self): user_profile_photos = UserProfilePhotos(self.total_count, self.photos) diff --git a/tests/test_video.py b/tests/test_video.py index 876462dd636..80f68cd57bd 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -190,7 +190,7 @@ async def test_send_video_caption_entities(self, bot, chat_id, video): ) assert message.caption == test_string - assert message.caption_entities == entities + assert message.caption_entities == tuple(entities) @pytest.mark.flaky(3, 1) async def test_resend(self, bot, chat_id, video): diff --git a/tests/test_videochat.py b/tests/test_videochat.py index d4aed477441..81b36f9b852 100644 --- a/tests/test_videochat.py +++ b/tests/test_videochat.py @@ -109,7 +109,7 @@ def test_de_json(self, user1, user2, bot): video_chat_participants = VideoChatParticipantsInvited.de_json(json_data, bot) assert video_chat_participants.api_kwargs == {} - assert isinstance(video_chat_participants.users, list) + assert isinstance(video_chat_participants.users, tuple) assert video_chat_participants.users[0] == user1 assert video_chat_participants.users[1] == user2 assert video_chat_participants.users[0].id == user1.id @@ -117,9 +117,7 @@ def test_de_json(self, user1, user2, bot): @pytest.mark.parametrize("use_users", (True, False)) def test_to_dict(self, user1, user2, use_users): - video_chat_participants = VideoChatParticipantsInvited( - [user1, user2] if use_users else None - ) + video_chat_participants = VideoChatParticipantsInvited([user1, user2] if use_users else ()) video_chat_dict = video_chat_participants.to_dict() assert isinstance(video_chat_dict, dict) @@ -134,7 +132,7 @@ def test_equality(self, user1, user2): a = VideoChatParticipantsInvited([user1]) b = VideoChatParticipantsInvited([user1]) c = VideoChatParticipantsInvited([user1, user2]) - d = VideoChatParticipantsInvited(None) + d = VideoChatParticipantsInvited([]) e = VideoChatStarted() assert a == b diff --git a/tests/test_voice.py b/tests/test_voice.py index a30145a74ce..722fdb17593 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -164,7 +164,7 @@ async def test_send_voice_caption_entities(self, bot, chat_id, voice_file): ) assert message.caption == test_string - assert message.caption_entities == entities + assert message.caption_entities == tuple(entities) @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) diff --git a/tests/test_webhookinfo.py b/tests/test_webhookinfo.py index 55d94e33e7f..575f11abfb9 100644 --- a/tests/test_webhookinfo.py +++ b/tests/test_webhookinfo.py @@ -89,7 +89,7 @@ def test_de_json(self, bot): assert isinstance(webhook_info.last_error_date, datetime) assert webhook_info.last_error_date == from_timestamp(self.last_error_date) assert webhook_info.max_connections == self.max_connections - assert webhook_info.allowed_updates == self.allowed_updates + assert webhook_info.allowed_updates == tuple(self.allowed_updates) assert webhook_info.ip_address == self.ip_address assert isinstance(webhook_info.last_synchronization_error_date, datetime) assert webhook_info.last_synchronization_error_date == from_timestamp( @@ -99,6 +99,12 @@ def test_de_json(self, bot): none = WebhookInfo.de_json(None, bot) assert none is None + def test_always_tuple_allowed_updates(self): + webhook_info = WebhookInfo( + self.url, self.has_custom_certificate, self.pending_update_count + ) + assert webhook_info.allowed_updates == () + def test_equality(self): a = WebhookInfo( url=self.url,