diff --git a/docs/source/conf.py b/docs/source/conf.py index 3227dcc54..0d1049ba7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -64,6 +64,9 @@ source_suffix = ".rst" # The master toctree document. master_doc = "index" +# Global substitutions +rst_prolog = (Path.cwd() / "../substitutions/global.rst").read_text(encoding="utf-8") + # -- Extension settings ------------------------------------------------ napoleon_use_admonition_for_examples = True diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 3be887306..605194f3f 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -304,6 +304,8 @@ - The first name of the bot * - :attr:`~telegram.Bot.last_name` - The last name of the bot + * - :attr:`~telegram.Bot.local_mode` + - Whether the bot is running in local mode * - :attr:`~telegram.Bot.username` - The username of the bot, without leading ``@`` * - :attr:`~telegram.Bot.link` diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst new file mode 100644 index 000000000..b48d5b2d3 --- /dev/null +++ b/docs/substitutions/global.rst @@ -0,0 +1,17 @@ +.. |uploadinput| replace:: To upload a file, you can either pass a :term:`file object` (e.g. ``open("filename", "rb")``), the file contents as bytes or the path of the file (as string or :class:`pathlib.Path` object). In the latter case, the file contents will either be read as bytes or the file path will be passed to Telegram, depending on the :paramref:`~telegram.Bot.local_mode` setting. + +.. |uploadinputnopath| replace:: To upload a file, you can either pass a :term:`file object` (e.g. ``open("filename", "rb")``) or the file contents as bytes. If the bot is running in :paramref:`~telegram.Bot.local_mode`, passing the path of the file (as string or :class:`pathlib.Path` object) is supported as well. + +.. |fileinputbase| replace:: Pass a ``file_id`` as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one. + +.. |fileinput| replace:: |fileinputbase| |uploadinput| + +.. |fileinputnopath| replace:: |fileinputbase| |uploadinputnopath| + +.. |thumbdocstringbase| replace:: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file. + +.. |thumbdocstring| replace:: |thumbdocstringbase| |uploadinput| + +.. |thumbdocstringnopath| replace:: |thumbdocstringbase| |uploadinputnopath| + +.. |editreplymarkup| replace:: It is currently only possible to edit messages without :attr:`telegram.Message.reply_markup` or with inline keyboards. diff --git a/telegram/_bot.py b/telegram/_bot.py index 26d0712af..d2c922f55 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -99,6 +99,7 @@ from telegram.request._requestparameter import RequestParameter if TYPE_CHECKING: from telegram import ( InlineQueryResult, + InputFile, InputMediaAudio, InputMediaDocument, InputMediaPhoto, @@ -160,6 +161,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager): ``location``, ``filename``, ``venue``, ``contact``, ``{read, write, connect, pool}_timeout``, ``api_kwargs``. Use a named argument for those, and notice that some positional arguments changed position as a result. + * For uploading files, file paths are now always accepted. If :paramref:`local_mode` is + :obj:`False`, the file contents will be read in binary mode and uploaded. Otherwise, + the file path will be passed in the + `file URI scheme `_. Args: token (:obj:`str`): Bot's unique authentication token. @@ -175,6 +180,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :class:`telegram.request.HTTPXRequest` will be used. private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data. private_key_password (:obj:`bytes`, optional): Password for above private key. + local_mode (:obj:`bool`, optional): Set to :obj:`True`, if the :paramref:`base_url` is + the URI of a `Local Bot API Server `_ that runs with the ``--local`` flag. Currently, the only effect of + this is that files are uploaded using their local path in the + `file URI scheme `_. + Defaults to :obj:`False`. + + .. versionadded:: 20.0. .. include:: inclusions/bot_methods.rst @@ -189,6 +202,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): "_request", "_logger", "_initialized", + "_local_mode", ) def __init__( @@ -200,6 +214,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): get_updates_request: BaseRequest = None, private_key: bytes = None, private_key_password: bytes = None, + local_mode: bool = False, ): if not token: raise InvalidToken("You must pass the token you received from https://t.me/Botfather!") @@ -207,6 +222,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): self._base_url = base_url + self._token self._base_file_url = base_file_url + self._token + self._local_mode = local_mode self._bot_user: Optional[User] = None self._private_key = None self._logger = logging.getLogger(__name__) @@ -253,6 +269,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ return self._base_file_url + @property + def local_mode(self) -> bool: + """:obj:`bool`: Whether this bot is running in local mode. + + .. versionadded:: 20.0 + """ + return self._local_mode + # Proper type hints are difficult because: # 1. cryptography doesn't have a nice base class, so it would get lengthy # 2. we can't import cryptography if it's not installed @@ -283,6 +307,21 @@ class Bot(TelegramObject, AbstractAsyncContextManager): return decorator + def _parse_file_input( + self, + file_input: Union[FileInput, "TelegramObject"], + tg_type: Type["TelegramObject"] = None, + filename: str = None, + attach: bool = False, + ) -> Union[str, "InputFile", Any]: + return parse_file_input( + file_input=file_input, + tg_type=tg_type, + filename=filename, + attach=attach, + local_mode=self._local_mode, + ) + def _insert_defaults(self, data: Dict[str, object]) -> None: # skipcq: PYL-R0201 """This method is here to make ext.Defaults work. Because we need to be able to tell e.g. `send_message(chat_id, text)` from `send_message(chat_id, text, parse_mode=None)`, the @@ -888,10 +927,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager): ) -> Message: """Use this method to send photos. - Note: - The photo argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` - .. seealso:: :attr:`telegram.Message.reply_photo`, :attr:`telegram.Chat.send_photo`, :attr:`telegram.User.send_photo` @@ -900,13 +935,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager): of the target channel (in the format ``@channelusername``). photo (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.PhotoSize`): Photo to send. - Pass a file_id as String to send a photo that exists on the Telegram servers - (recommended), pass an HTTP URL as a String for Telegram to get a photo from the - Internet, or upload a new photo using multipart/form-data. Lastly you can pass - an existing :class:`telegram.PhotoSize` object to send. + |fileinput| + Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. caption (:obj:`str`, optional): Photo caption (may also be used when resending photos by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -961,7 +998,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ data: JSONDict = { "chat_id": chat_id, - "photo": parse_file_input(photo, PhotoSize, filename=filename), + "photo": self._parse_file_input(photo, PhotoSize, filename=filename), "parse_mode": parse_mode, } @@ -1021,10 +1058,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager): For sending voice messages, use the :meth:`send_voice` method instead. - Note: - The audio argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` - .. seealso:: :attr:`telegram.Message.reply_audio`, :attr:`telegram.Chat.send_audio`, :attr:`telegram.User.send_audio` @@ -1033,13 +1066,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager): of the target channel (in the format ``@channelusername``). audio (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Audio`): Audio file to send. - Pass a file_id as String to send an audio file that exists on the Telegram servers - (recommended), pass an HTTP URL as a String for Telegram to get an audio file from - the Internet, or upload a new one using multipart/form-data. Lastly you can pass - an existing :class:`telegram.Audio` object to send. + |fileinput| + Lastly you can pass an existing :class:`telegram.Audio` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. caption (:obj:`str`, optional): Audio caption, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -1067,16 +1102,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail - of the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than 200 kB in size. A thumbnail's width and height should - not exceed 320. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + Keyword Args: filename (:obj:`str`, optional): Custom file name for the audio, when uploading a new file. Convenience parameter, useful e.g. when sending files generated by the @@ -1106,7 +1141,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ data: JSONDict = { "chat_id": chat_id, - "audio": parse_file_input(audio, Audio, filename=filename), + "audio": self._parse_file_input(audio, Audio, filename=filename), "parse_mode": parse_mode, } @@ -1122,7 +1157,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): if caption_entities: data["caption_entities"] = caption_entities if thumb: - data["thumb"] = parse_file_input(thumb, attach=True) + data["thumb"] = self._parse_file_input(thumb, attach=True) return await self._send_message( # type: ignore[return-value] "sendAudio", @@ -1169,12 +1204,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size, this limit may be changed in the future. - Note: - * The document argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')``. - - * Sending by URL will currently only work ``GIF``, ``PDF`` & ``ZIP`` files. - .. seealso:: :attr:`telegram.Message.reply_document`, :attr:`telegram.Chat.send_document`, :attr:`telegram.User.send_document` @@ -1183,13 +1212,18 @@ class Bot(TelegramObject, AbstractAsyncContextManager): of the target channel (in the format ``@channelusername``). document (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Document`): File to send. - Pass a file_id as String to send a file that exists on the Telegram servers - (recommended), pass an HTTP URL as a String for Telegram to get a file from the - Internet, or upload a new one using multipart/form-data. Lastly you can pass - an existing :class:`telegram.Document` object to send. + |fileinput| + Lastly you can pass an existing :class:`telegram.Document` object to send. + + Note: + Sending by URL will currently only work ``GIF``, ``PDF`` & ``ZIP`` files. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. caption (:obj:`str`, optional): Document caption (may also be used when resending documents by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -1216,16 +1250,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail - of the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than 200 kB in size. A thumbnail's width and height should - not exceed 320. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + Keyword Args: filename (:obj:`str`, optional): Custom file name for the document, when uploading a new file. Convenience parameter, useful e.g. when sending files generated by the @@ -1253,7 +1287,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ data: JSONDict = { "chat_id": chat_id, - "document": parse_file_input(document, Document, filename=filename), + "document": self._parse_file_input(document, Document, filename=filename), "parse_mode": parse_mode, } @@ -1265,7 +1299,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): if disable_content_type_detection is not None: data["disable_content_type_detection"] = disable_content_type_detection if thumb: - data["thumb"] = parse_file_input(thumb, attach=True) + data["thumb"] = self._parse_file_input(thumb, attach=True) return await self._send_message( # type: ignore[return-value] "sendDocument", @@ -1302,10 +1336,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ Use this method to send static ``.WEBP``, animated ``.TGS``, or video ``.WEBM`` stickers. - Note: - The :paramref:`sticker` argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` - .. seealso:: :attr:`telegram.Message.reply_sticker`, :attr:`telegram.Chat.send_sticker`, :attr:`telegram.User.send_sticker` @@ -1314,13 +1344,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager): of the target channel (in the format ``@channelusername``). sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Sticker`): Sticker to send. - Pass a file_id as String to send a file that exists on the Telegram servers - (recommended), pass an HTTP URL as a String for Telegram to get a .webp file from - the Internet, or upload a new one using multipart/form-data. Lastly you can pass - an existing :class:`telegram.Sticker` object to send. + |fileinput| + Lastly you can pass an existing :class:`telegram.Sticker` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from @@ -1359,7 +1391,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :class:`telegram.error.TelegramError` """ - data: JSONDict = {"chat_id": chat_id, "sticker": parse_file_input(sticker, Sticker)} + data: JSONDict = {"chat_id": chat_id, "sticker": self._parse_file_input(sticker, Sticker)} return await self._send_message( # type: ignore[return-value] "sendSticker", data, @@ -1410,11 +1442,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager): changed in the future. Note: - * The :paramref:`video` argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` - * :paramref:`thumb` will be ignored for small video files, for which Telegram can - easily generate thumbnails. However, this behaviour is undocumented and might be - changed by Telegram. + :paramref:`thumb` will be ignored for small video files, for which Telegram can + easily generate thumbnails. However, this behaviour is undocumented and might be + changed by Telegram. .. seealso:: :attr:`telegram.Message.reply_video`, :attr:`telegram.Chat.send_video`, :attr:`telegram.User.send_video` @@ -1424,13 +1454,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager): of the target channel (in the format ``@channelusername``). video (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Video`): Video file to send. - Pass a file_id as String to send an video file that exists on the Telegram servers - (recommended), pass an HTTP URL as a String for Telegram to get an video file from - the Internet, or upload a new one using multipart/form-data. Lastly you can pass - an existing :class:`telegram.Video` object to send. + |fileinput| + Lastly you can pass an existing :class:`telegram.Video` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. duration (:obj:`int`, optional): Duration of sent video in seconds. width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. @@ -1460,16 +1492,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail - of the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than 200 kB in size. A thumbnail's width and height should - not exceed 320. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + Keyword Args: filename (:obj:`str`, optional): Custom file name for the video, when uploading a new file. Convenience parameter, useful e.g. when sending files generated by the @@ -1499,7 +1531,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ data: JSONDict = { "chat_id": chat_id, - "video": parse_file_input(video, Video, filename=filename), + "video": self._parse_file_input(video, Video, filename=filename), "parse_mode": parse_mode, } @@ -1516,7 +1548,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): if height: data["height"] = height if thumb: - data["thumb"] = parse_file_input(thumb, attach=True) + data["thumb"] = self._parse_file_input(thumb, attach=True) return await self._send_message( # type: ignore[return-value] "sendVideo", @@ -1559,11 +1591,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager): Use this method to send video messages. Note: - * The :paramref:`video_note` argument can be either a file_id or a file from disk - ``open(filename, 'rb')`` - * :paramref:`thumb` will be ignored for small video files, for which Telegram can - easily generate thumbnails. However, this behaviour is undocumented and might be - changed by Telegram. + :paramref:`thumb` will be ignored for small video files, for which Telegram can + easily generate thumbnails. However, this behaviour is undocumented and might be + changed by Telegram. .. seealso:: :attr:`telegram.Message.reply_video_note`, :attr:`telegram.Chat.send_video_note`, @@ -1573,14 +1603,19 @@ class Bot(TelegramObject, AbstractAsyncContextManager): chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). video_note (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.VideoNote`): Video note - to send. Pass a file_id as String to send a video note that exists on the Telegram - servers (recommended) or upload a new video using multipart/form-data. Or you can - pass an existing :class:`telegram.VideoNote` object to send. Sending video notes by - a URL is currently unsupported. + :class:`telegram.VideoNote`): Video note to send. + Pass a file_id as String to send a video note that exists on the Telegram + servers (recommended) or upload a new video using multipart/form-data. + |uploadinput| + Lastly you can pass an existing :class:`telegram.VideoNote` object to send. + Sending video notes by a URL is currently unsupported. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. duration (:obj:`int`, optional): Duration of sent video in seconds. length (:obj:`int`, optional): Video width and height, i.e. diameter of the video message. @@ -1599,16 +1634,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail - of the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than 200 kB in size. A thumbnail's width and height should - not exceed 320. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + Keyword Args: filename (:obj:`str`, optional): Custom file name for the video note, when uploading a new file. Convenience parameter, useful e.g. when sending files generated by the @@ -1638,7 +1673,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ data: JSONDict = { "chat_id": chat_id, - "video_note": parse_file_input(video_note, VideoNote, filename=filename), + "video_note": self._parse_file_input(video_note, VideoNote, filename=filename), } if duration is not None: @@ -1646,7 +1681,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): if length is not None: data["length"] = length if thumb: - data["thumb"] = parse_file_input(thumb, attach=True) + data["thumb"] = self._parse_file_input(thumb, attach=True) return await self._send_message( # type: ignore[return-value] "sendVideoNote", @@ -1696,7 +1731,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): Note: :paramref:`thumb` will be ignored for small files, for which Telegram can easily - generate thumb nails. However, this behaviour is undocumented and might be changed + generate thumbnails. However, this behaviour is undocumented and might be changed by Telegram. .. seealso:: :attr:`telegram.Message.reply_animation`, @@ -1707,10 +1742,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager): chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). animation (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Animation`): Animation to - send. Pass a file_id as String to send an animation that exists on the Telegram - servers (recommended), pass an HTTP URL as a String for Telegram to get an - animation from the Internet, or upload a new animation using multipart/form-data. + :class:`telegram.Animation`): Animation to send. + |fileinput| Lastly you can pass an existing :class:`telegram.Animation` object to send. .. versionchanged:: 13.2 @@ -1718,15 +1751,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager): duration (:obj:`int`, optional): Duration of sent animation in seconds. width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail - of the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than 200 kB in size. A thumbnail's width and height should - not exceed 320. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + caption (:obj:`str`, optional): Animation caption (may also be used when resending animations by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after @@ -1782,7 +1816,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ data: JSONDict = { "chat_id": chat_id, - "animation": parse_file_input(animation, Animation, filename=filename), + "animation": self._parse_file_input(animation, Animation, filename=filename), "parse_mode": parse_mode, } @@ -1793,7 +1827,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): if height: data["height"] = height if thumb: - data["thumb"] = parse_file_input(thumb, attach=True) + data["thumb"] = self._parse_file_input(thumb, attach=True) if caption: data["caption"] = caption if caption_entities: @@ -1844,11 +1878,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager): in size, this limit may be changed in the future. Note: - * The :paramref:`voice` argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')``. - - * To use this method, the file must have the type :mimetype:`audio/ogg` and be no more - than ``1MB`` in size. ``1-20MB`` voice notes will be sent as files. + To use this method, the file must have the type :mimetype:`audio/ogg` and be no more + than ``1MB`` in size. ``1-20MB`` voice notes will be sent as files. .. seealso:: :attr:`telegram.Message.reply_voice`, :attr:`telegram.Chat.send_voice`, :attr:`telegram.User.send_voice` @@ -1858,13 +1889,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager): of the target channel (in the format ``@channelusername``). voice (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Voice`): Voice file to send. - Pass a file_id as String to send an voice file that exists on the Telegram servers - (recommended), pass an HTTP URL as a String for Telegram to get an voice file from - the Internet, or upload a new one using multipart/form-data. Lastly you can pass - an existing :class:`telegram.Voice` object to send. + |fileinput| + Lastly you can pass an existing :class:`telegram.Voice` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. caption (:obj:`str`, optional): Voice message caption, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -1920,7 +1953,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ data: JSONDict = { "chat_id": chat_id, - "voice": parse_file_input(voice, Voice, filename=filename), + "voice": self._parse_file_input(voice, Voice, filename=filename), "parse_mode": parse_mode, } @@ -3502,8 +3535,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): Use this method to edit text and game messages. Note: - It is currently only possible to edit messages without - :attr:`telegram.Message.reply_markup` or with inline keyboards. + |editreplymarkup|. .. seealso:: :attr:`telegram.Message.edit_text`, :attr:`telegram.CallbackQuery.edit_message_text` @@ -3601,8 +3633,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): Use this method to edit captions of messages. Note: - It is currently only possible to edit messages without - :attr:`telegram.Message.reply_markup` or with inline keyboards + |editreplymarkup| .. seealso:: :attr:`telegram.Message.edit_caption`, :attr:`telegram.CallbackQuery.edit_message_caption` @@ -3698,8 +3729,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :attr:`~telegram.File.file_id` or specify a URL. Note: - It is currently only possible to edit messages without - :attr:`telegram.Message.reply_markup` or with inline keyboards + |editreplymarkup| .. seealso:: :attr:`telegram.Message.edit_media`, :attr:`telegram.CallbackQuery.edit_message_media` @@ -3779,8 +3809,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): (for inline bots). Note: - It is currently only possible to edit messages without - :attr:`telegram.Message.reply_markup` or with inline keyboards + |editreplymarkup| .. seealso:: :attr:`telegram.Message.edit_reply_markup`, :attr:`telegram.CallbackQuery.edit_message_reply_markup` @@ -3856,6 +3885,12 @@ class Bot(TelegramObject, AbstractAsyncContextManager): ) -> List[Update]: """Use this method to receive incoming updates using long polling. + 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 + server response. + 3. To take full advantage of this library take a look at :class:`telegram.ext.Updater` + Args: offset (:obj:`int`, optional): Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received @@ -3895,12 +3930,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager): api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. - 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 - server response. - 3. To take full advantage of this library take a look at :class:`telegram.ext.Updater` - Returns: List[:class:`telegram.Update`] @@ -3970,15 +3999,25 @@ class Bot(TelegramObject, AbstractAsyncContextManager): ``X-Telegram-Bot-Api-Secret-Token`` with the secret token as content. Note: - The certificate argument should be a file from disk ``open(filename, 'rb')``. + 1. You will not be able to receive updates using :meth:`get_updates` for long as an + outgoing webhook is set up. + 2. To use a self-signed certificate, you need to upload your public key certificate + using :paramref:`certificate` parameter. Please upload as + :class:`~telegram.InputFile`, sending a String will not work. + 3. Ports currently supported for Webhooks: + :attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`. + + If you're having any trouble setting up webhooks, please check out this `guide to + Webhooks`_. Args: url (:obj:`str`): HTTPS url to send updates to. Use an empty string to remove webhook integration. - certificate (:term:`file object`): Upload your public key certificate so that the root - certificate in use can be checked. See our self-signed guide for details. - (https://github.com/python-telegram-bot/python-telegram-bot/wiki/Webhooks#\ - creating-a-self-signed-certificate-using-openssl) + certificate (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`): + Upload your public key certificate so that the root + certificate in use can be checked. See our `self-signed guide `_ for details. |uploadinputnopath| ip_address (:obj:`str`, optional): The fixed IP address which will be used to send webhook requests instead of the IP address resolved through DNS. max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS @@ -4021,18 +4060,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager): api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. - Note: - 1. You will not be able to receive updates using :meth:`get_updates` for long as an - outgoing webhook is set up. - 2. To use a self-signed certificate, you need to upload your public key certificate - using certificate parameter. Please upload as InputFile, sending a String will not - work. - 3. Ports currently supported for Webhooks: - :attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`. - - If you're having any trouble setting up webhooks, please check out this `guide to - Webhooks`_. - Returns: :obj:`bool` On success, :obj:`True` is returned. @@ -4045,7 +4072,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): data: JSONDict = {"url": url} if certificate: - data["certificate"] = parse_file_input(certificate) + data["certificate"] = self._parse_file_input(certificate) if max_connections is not None: data["max_connections"] = max_connections if allowed_updates is not None: @@ -5492,6 +5519,13 @@ class Bot(TelegramObject, AbstractAsyncContextManager): link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + Note: + Each administrator in a chat generates their own invite links. Bots can't use invite + links generated by other administrators. If you want your bot to work with invite + links, it will need to generate its own link using :meth:`export_chat_invite_link` or + by calling the :meth:`get_chat` method. If your bot needs to generate a new primary + invite link replacing its previous one, use :attr:`export_chat_invite_link` again. + .. seealso:: :attr:`telegram.Chat.export_invite_link` Args: @@ -5514,13 +5548,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager): api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. - Note: - Each administrator in a chat generates their own invite links. Bots can't use invite - links generated by other administrators. If you want your bot to work with invite - links, it will need to generate its own link using :meth:`export_chat_invite_link` or - by calling the :meth:`get_chat` method. If your bot needs to generate a new primary - invite link replacing its previous one, use :attr:`export_chat_invite_link` again. - Returns: :obj:`str`: New invite link on success. @@ -5955,10 +5982,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager): chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). photo (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): New chat photo. + |uploadinput| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to @@ -5982,7 +6014,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager): :class:`telegram.error.TelegramError` """ - data: JSONDict = {"chat_id": chat_id, "photo": parse_file_input(photo)} + data: JSONDict = {"chat_id": chat_id, "photo": self._parse_file_input(photo)} result = await self._post( "setChatPhoto", data, @@ -6488,19 +6520,20 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. :meth:`create_new_sticker_set` and :meth:`add_sticker_to_set` methods (can be used multiple times). - Note: - The :paramref:`png_sticker` argument can be either a file_id, an URL or a file from - disk ``open(filename, 'rb')`` - Args: user_id (:obj:`int`): User identifier of sticker file owner. png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. + |uploadinput| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to @@ -6524,7 +6557,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. :class:`telegram.error.TelegramError` """ - data: JSONDict = {"user_id": user_id, "png_sticker": parse_file_input(png_sticker)} + data: JSONDict = {"user_id": user_id, "png_sticker": self._parse_file_input(png_sticker)} result = await self._post( "uploadStickerFile", data, @@ -6566,10 +6599,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. of the arguments had to be changed. Use keyword arguments to make sure that the arguments are passed correctly. - Note: - The :paramref:`png_sticker` and :paramref:`tgs_sticker` argument can be either a - file_id, an URL or a file from disk ``open(filename, 'rb')`` - .. versionchanged:: 20.0 The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead. @@ -6585,27 +6614,37 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ optional): **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, - and either width or height must be exactly 512px. Pass a file_id as a String to - send a file that already exists on the Telegram servers, pass an HTTP URL as a - String for Telegram to get a file from the Internet, or upload a new one - using multipart/form-data. + and either width or height must be exactly 512px. + |fileinput| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. tgs_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ - optional): **TGS** animation with the sticker, uploaded using multipart/form-data. + optional): **TGS** animation with the sticker. |uploadinput| See https://core.telegram.org/stickers#animated-sticker-requirements for technical requirements. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\ - optional): **WEBM** video with the sticker, uploaded using multipart/form-data. + optional): **WEBM** video with the sticker. |uploadinput| See https://core.telegram.org/stickers#video-sticker-requirements for technical requirements. .. versionadded:: 13.11 + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + emojis (:obj:`str`): One or more emoji corresponding to the sticker. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should be placed on faces. @@ -6642,11 +6681,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. data: JSONDict = {"user_id": user_id, "name": name, "title": title, "emojis": emojis} if png_sticker is not None: - data["png_sticker"] = parse_file_input(png_sticker) + data["png_sticker"] = self._parse_file_input(png_sticker) if tgs_sticker is not None: - data["tgs_sticker"] = parse_file_input(tgs_sticker) + data["tgs_sticker"] = self._parse_file_input(tgs_sticker) if webm_sticker is not None: - data["webm_sticker"] = parse_file_input(webm_sticker) + data["webm_sticker"] = self._parse_file_input(webm_sticker) if mask_position is not None: data["mask_position"] = mask_position if sticker_type is not None: @@ -6693,10 +6732,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. of the arguments had to be changed. Use keyword arguments to make sure that the arguments are passed correctly. - Note: - The :paramref:`png_sticker` and :paramref:`tgs_sticker` argument can be either a - file_id, an URL or a file from disk ``open(filename, 'rb')`` - Args: user_id (:obj:`int`): User identifier of created sticker set owner. @@ -6704,26 +6739,36 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ optional): **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, - and either width or height must be exactly 512px. Pass a file_id as a String to - send a file that already exists on the Telegram servers, pass an HTTP URL as a - String for Telegram to get a file from the Internet, or upload a new one - using multipart/form-data. + and either width or height must be exactly 512px. + |fileinput| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. tgs_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ - optional): **TGS** animation with the sticker, uploaded using multipart/form-data. + optional): **TGS** animation with the sticker. |uploadinput| See https://core.telegram.org/stickers#animated-sticker-requirements for technical requirements. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\ - optional): **WEBM** video with the sticker, uploaded using multipart/form-data. + optional): **WEBM** video with the sticker. |uploadinput| See https://core.telegram.org/stickers#video-sticker-requirements for technical requirements. .. versionadded:: 13.11 + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. emojis (:obj:`str`): One or more emoji corresponding to the sticker. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should be placed on faces. @@ -6754,11 +6799,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. data: JSONDict = {"user_id": user_id, "name": name, "emojis": emojis} if png_sticker is not None: - data["png_sticker"] = parse_file_input(png_sticker) + data["png_sticker"] = self._parse_file_input(png_sticker) if tgs_sticker is not None: - data["tgs_sticker"] = parse_file_input(tgs_sticker) + data["tgs_sticker"] = self._parse_file_input(tgs_sticker) if webm_sticker is not None: - data["webm_sticker"] = parse_file_input(webm_sticker) + data["webm_sticker"] = self._parse_file_input(webm_sticker) if mask_position is not None: data["mask_position"] = mask_position @@ -6895,10 +6940,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. for animated sticker sets only. Video thumbnails can be set only for video sticker sets only. - Note: - The :paramref:`thumb` can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` - Args: name (:obj:`str`): Sticker set name user_id (:obj:`int`): User identifier of created sticker set owner. @@ -6910,9 +6951,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. sticker technical requirements, or a **WEBM** video with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/stickers#video-sticker-requirements for video sticker - technical requirements. Pass a file_id as a String to send a file that - already exists on the Telegram servers, pass an HTTP URL as a String for Telegram - to get a file from the Internet, or upload a new one using multipart/form-data. + technical requirements. + |fileinput| Animated sticker set thumbnails can't be uploaded via HTTP URL. .. versionchanged:: 13.2 @@ -6943,7 +6983,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. """ data: JSONDict = {"name": name, "user_id": user_id} if thumb is not None: - data["thumb"] = parse_file_input(thumb) + data["thumb"] = self._parse_file_input(thumb) result = await self._post( "setStickerSetThumb", diff --git a/telegram/_files/inputfile.py b/telegram/_files/inputfile.py index 34aa3b273..b4bcabc4c 100644 --- a/telegram/_files/inputfile.py +++ b/telegram/_files/inputfile.py @@ -20,10 +20,10 @@ import logging import mimetypes -from pathlib import Path from typing import IO, Optional, Union from uuid import uuid4 +from telegram._utils.files import load_file from telegram._utils.types import FieldTuple _DEFAULT_MIME_TYPE = "application/octet-stream" @@ -75,15 +75,10 @@ class InputFile: elif isinstance(obj, str): self.input_file_content = obj.encode("utf-8") else: - self.input_file_content = obj.read() - self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None + reported_filename, self.input_file_content = load_file(obj) + filename = filename or reported_filename - if ( - not filename - and hasattr(obj, "name") - and not isinstance(obj.name, int) # type: ignore[union-attr] - ): - filename = Path(obj.name).name # type: ignore[union-attr] + self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None if filename: self.mimetype = mimetypes.guess_type(filename, strict=False)[0] or _DEFAULT_MIME_TYPE diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 68445c882..f21337edc 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -48,9 +48,8 @@ class InputMedia(TelegramObject): media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Animation` | :class:`telegram.Audio` | \ :class:`telegram.Document` | :class:`telegram.PhotoSize` | \ - :class:`telegram.Video`): - File to send. Pass a file_id to send a file that exists on the Telegram servers - (recommended), pass an HTTP URL for Telegram to get a file from the Internet. + :class:`telegram.Video`): File to send. + |fileinputnopath| Lastly you can pass an existing telegram media object of the corresponding type to send. caption (:obj:`str`, optional): Caption of the media to be sent, @@ -99,7 +98,11 @@ class InputMedia(TelegramObject): @staticmethod def _parse_thumb_input(thumb: Optional[FileInput]) -> Optional[Union[str, InputFile]]: - return parse_file_input(thumb, attach=True) if thumb is not None else thumb + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + return ( + parse_file_input(thumb, attach=True, local_mode=True) if thumb is not None else thumb + ) class InputMediaAnimation(InputMedia): @@ -112,10 +115,8 @@ class InputMediaAnimation(InputMedia): Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Animation`): File to send. Pass a - file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP - URL for Telegram to get a file from the Internet. Lastly you can pass an existing - :class:`telegram.Animation` object to send. + :class:`telegram.Animation`): File to send. |fileinputnopath| + Lastly you can pass an existing :class:`telegram.Animation` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -123,13 +124,9 @@ class InputMediaAnimation(InputMedia): new file. Convenience parameter, useful e.g. when sending files generated by the :obj:`tempfile` module. - .. versionadded:: 13.1 - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of - the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than ``200`` kB in size. A thumbnail's width and height should - not exceed ``320``. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + .. versionadded:: 13.1 + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstringnopath| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -180,7 +177,9 @@ class InputMediaAnimation(InputMedia): duration = media.duration if duration is None else duration media = media.file_id else: - media = parse_file_input(media, filename=filename, attach=True) + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + media = parse_file_input(media, filename=filename, attach=True, local_mode=True) super().__init__(InputMediaType.ANIMATION, media, caption, caption_entities, parse_mode) self.thumb = self._parse_thumb_input(thumb) @@ -194,10 +193,8 @@ class InputMediaPhoto(InputMedia): Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.PhotoSize`): File to send. Pass a - file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP - URL for Telegram to get a file from the Internet. Lastly you can pass an existing - :class:`telegram.PhotoSize` object to send. + :class:`telegram.PhotoSize`): File to send. |fileinputnopath| + Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -205,7 +202,7 @@ class InputMediaPhoto(InputMedia): new file. Convenience parameter, useful e.g. when sending files generated by the :obj:`tempfile` module. - .. versionadded:: 13.1 + .. versionadded:: 13.1 caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -236,7 +233,9 @@ class InputMediaPhoto(InputMedia): caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, filename: str = None, ): - media = parse_file_input(media, PhotoSize, filename=filename, attach=True) + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + media = parse_file_input(media, PhotoSize, filename=filename, attach=True, local_mode=True) super().__init__(InputMediaType.PHOTO, media, caption, caption_entities, parse_mode) @@ -253,10 +252,8 @@ class InputMediaVideo(InputMedia): Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Video`): File to send. Pass a - file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP - URL for Telegram to get a file from the Internet. Lastly you can pass an existing - :class:`telegram.Video` object to send. + :class:`telegram.Video`): File to send. |fileinputnopath| + Lastly you can pass an existing :class:`telegram.Video` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -264,7 +261,7 @@ class InputMediaVideo(InputMedia): new file. Convenience parameter, useful e.g. when sending files generated by the :obj:`tempfile` module. - .. versionadded:: 13.1 + .. versionadded:: 13.1 caption (:obj:`str`, optional): Caption of the video to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -279,12 +276,8 @@ class InputMediaVideo(InputMedia): duration (:obj:`int`, optional): Video duration in seconds. supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is suitable for streaming. - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of - the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than ``200`` kB in size. A thumbnail's width and height should - not exceed ``320``. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstringnopath| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -327,7 +320,9 @@ class InputMediaVideo(InputMedia): duration = duration if duration is not None else media.duration media = media.file_id else: - media = parse_file_input(media, filename=filename, attach=True) + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + media = parse_file_input(media, filename=filename, attach=True, local_mode=True) super().__init__(InputMediaType.VIDEO, media, caption, caption_entities, parse_mode) self.width = width @@ -347,11 +342,8 @@ class InputMediaAudio(InputMedia): Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Audio`): - File to send. Pass a - file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP - URL for Telegram to get a file from the Internet. Lastly you can pass an existing - :class:`telegram.Audio` object to send. + :class:`telegram.Audio`): File to send. |fileinputnopath| + Lastly you can pass an existing :class:`telegram.Audio` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -359,7 +351,7 @@ class InputMediaAudio(InputMedia): new file. Convenience parameter, useful e.g. when sending files generated by the :obj:`tempfile` module. - .. versionadded:: 13.1 + .. versionadded:: 13.1 caption (:obj:`str`, optional): Caption of the audio to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -373,12 +365,8 @@ class InputMediaAudio(InputMedia): performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio tags. title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of - the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than ``200`` kB in size. A thumbnail's width and height should - not exceed ``320``. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstringnopath| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -418,7 +406,9 @@ class InputMediaAudio(InputMedia): title = media.title if title is None else title media = media.file_id else: - media = parse_file_input(media, filename=filename, attach=True) + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + media = parse_file_input(media, filename=filename, attach=True, local_mode=True) super().__init__(InputMediaType.AUDIO, media, caption, caption_entities, parse_mode) self.thumb = self._parse_thumb_input(thumb) @@ -432,10 +422,8 @@ class InputMediaDocument(InputMedia): Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ - :class:`telegram.Document`): File to send. Pass a - file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP - URL for Telegram to get a file from the Internet. Lastly you can pass an existing - :class:`telegram.Document` object to send. + :class:`telegram.Document`): File to send. |fileinputnopath| + Lastly you can pass an existing :class:`telegram.Document` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -443,7 +431,7 @@ class InputMediaDocument(InputMedia): new file. Convenience parameter, useful e.g. when sending files generated by the :obj:`tempfile` module. - .. versionadded:: 13.1 + .. versionadded:: 13.1 caption (:obj:`str`, optional): Caption of the document to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. @@ -453,12 +441,8 @@ class InputMediaDocument(InputMedia): caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :paramref:`parse_mode`. - thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of - the file sent; can be ignored if - thumbnail generation for the file is supported server-side. The thumbnail should be - in JPEG format and less than ``200`` kB in size. A thumbnail's width and height should - not exceed ``320``. Ignored if the file is not uploaded using multipart/form-data. - Thumbnails can't be reused and can be only uploaded as a new file. + thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstringnopath| .. versionchanged:: 13.2 Accept :obj:`bytes` as input. @@ -492,7 +476,9 @@ class InputMediaDocument(InputMedia): caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, filename: str = None, ): - media = parse_file_input(media, Document, filename=filename, attach=True) + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + media = parse_file_input(media, Document, filename=filename, attach=True, local_mode=True) super().__init__(InputMediaType.DOCUMENT, media, caption, caption_entities, parse_mode) self.thumb = self._parse_thumb_input(thumb) self.disable_content_type_detection = disable_content_type_detection diff --git a/telegram/_utils/files.py b/telegram/_utils/files.py index 045973d69..3e13c3832 100644 --- a/telegram/_utils/files.py +++ b/telegram/_utils/files.py @@ -29,13 +29,47 @@ Warning: """ from pathlib import Path -from typing import IO, TYPE_CHECKING, Any, Optional, Type, Union, cast +from typing import IO, TYPE_CHECKING, Any, Optional, Tuple, Type, TypeVar, Union, cast, overload from telegram._utils.types import FileInput, FilePathInput if TYPE_CHECKING: from telegram import InputFile, TelegramObject +_T = TypeVar("_T", bound=Union[bytes, "InputFile", str, Path, None]) + + +@overload +def load_file(obj: IO[bytes]) -> Tuple[Optional[str], bytes]: + ... + + +@overload +def load_file(obj: _T) -> Tuple[None, _T]: + ... + + +def load_file( + obj: Optional[FileInput], +) -> Tuple[Optional[str], Union[bytes, "InputFile", str, Path, None]]: + """If the input is a file handle, read the data and name and return it. Otherwise, return + the input unchanged. + """ + if obj is None: + return None, None + + try: + contents = obj.read() # type: ignore[union-attr] + except AttributeError: + return None, cast(Union[bytes, "InputFile", str, Path], obj) + + if hasattr(obj, "name") and not isinstance(obj.name, int): # type: ignore[union-attr] + filename = Path(obj.name).name # type: ignore[union-attr] + else: + filename = None + + return filename, contents + def is_local_file(obj: Optional[FilePathInput]) -> bool: """ @@ -54,18 +88,24 @@ def is_local_file(obj: Optional[FilePathInput]) -> bool: return False -def parse_file_input( +def parse_file_input( # pylint: disable=too-many-return-statements file_input: Union[FileInput, "TelegramObject"], tg_type: Type["TelegramObject"] = None, filename: str = None, attach: bool = False, + local_mode: bool = False, ) -> Union[str, "InputFile", Any]: """ Parses input for sending files: - * For string input, if the input is an absolute path of a local file, - adds the ``file://`` prefix. If the input is a relative path of a local file, computes the - absolute path and adds the ``file://`` prefix. Returns the input unchanged, otherwise. + * For string input, if the input is an absolute path of a local file: + + * if ``local_mode`` is ``True``, adds the ``file://`` prefix. If the input is a relative + path of a local file, computes the absolute path and adds the ``file://`` prefix. + * if ``local_mode`` is ``False``, loads the file as binary data and builds an + :class:`InputFile` from that + + Returns the input unchanged, otherwise. * :class:`pathlib.Path` objects are treated the same way as strings. * For IO and bytes input, returns an :class:`telegram.InputFile`. * If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id`` @@ -81,6 +121,8 @@ def parse_file_input( attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in the request to Telegram should point to the multipart data via an ``attach://`` URI. Defaults to `False`. Only relevant if an :class:`telegram.InputFile` is returned. + local_mode (:obj:`bool`, optional): Pass :obj:`True` if the bot is running an api server + in ``--local`` mode. Returns: :obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched @@ -90,13 +132,17 @@ def parse_file_input( from telegram import InputFile # pylint: disable=import-outside-toplevel if isinstance(file_input, str) and file_input.startswith("file://"): + if not local_mode: + raise ValueError("Specified file input is a file URI, but local mode is not enabled.") return file_input if isinstance(file_input, (str, Path)): if is_local_file(file_input): - out = Path(file_input).absolute().as_uri() - else: - out = file_input # type: ignore[assignment] - return out + path = Path(file_input) + if local_mode: + return path.absolute().as_uri() + return InputFile(path.open(mode="rb"), filename=filename, attach=attach) + + return file_input if isinstance(file_input, bytes): return InputFile(file_input, filename=filename, attach=attach) if hasattr(file_input, "read"): diff --git a/telegram/_utils/types.py b/telegram/_utils/types.py index ebf91ee8b..884e592e3 100644 --- a/telegram/_utils/types.py +++ b/telegram/_utils/types.py @@ -44,8 +44,7 @@ FilePathInput = Union[str, Path] FileInput = Union[FilePathInput, FileLike, bytes, str] """Valid input for passing files to Telegram. Either a file id as string, a file like object, -a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes` or -:obj:`str`.""" +a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`.""" JSONDict = Dict[str, Any] """Dictionary containing response from Telegram or data to send to the API.""" diff --git a/telegram/ext/_applicationbuilder.py b/telegram/ext/_applicationbuilder.py index 403e53cdd..b3e804251 100644 --- a/telegram/ext/_applicationbuilder.py +++ b/telegram/ext/_applicationbuilder.py @@ -83,6 +83,7 @@ _BOT_CHECKS = [ ("arbitrary_callback_data", "arbitrary_callback_data"), ("private_key", "private_key"), ("rate_limiter", "rate_limiter instance"), + ("local_mode", "local_mode setting"), ] _TWO_ARGS_REQ = "The parameter `{}` may only be set, if no {} was set." @@ -148,6 +149,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): "_update_queue", "_updater", "_write_timeout", + "_local_mode", ) def __init__(self: "InitApplicationBuilder"): @@ -172,6 +174,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): self._private_key_password: ODVInput[bytes] = DEFAULT_NONE self._defaults: ODVInput["Defaults"] = DEFAULT_NONE self._arbitrary_callback_data: DVInput[Union[bool, int]] = DEFAULT_FALSE + self._local_mode: DVInput[bool] = DEFAULT_FALSE self._bot: DVInput[Bot] = DEFAULT_NONE self._update_queue: DVInput[Queue] = DefaultValue(Queue()) self._job_queue: ODVInput["JobQueue"] = DefaultValue(JobQueue()) @@ -232,8 +235,17 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): request=self._build_request(get_updates=False), get_updates_request=self._build_request(get_updates=True), rate_limiter=DefaultValue.get_value(self._rate_limiter), + local_mode=DefaultValue.get_value(self._local_mode), ) + def _bot_check(self, name: str) -> None: + if self._bot is not DEFAULT_NONE: + raise RuntimeError(_TWO_ARGS_REQ.format(name, "bot instance")) + + def _updater_check(self, name: str) -> None: + if self._updater not in (DEFAULT_NONE, None): + raise RuntimeError(_TWO_ARGS_REQ.format(name, "updater")) + def build( self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", ) -> Application[BT, CCT, UD, CD, BD, JQ]: @@ -326,10 +338,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - if self._bot is not DEFAULT_NONE: - raise RuntimeError(_TWO_ARGS_REQ.format("token", "bot instance")) - if self._updater not in (DEFAULT_NONE, None): - raise RuntimeError(_TWO_ARGS_REQ.format("token", "updater")) + self._bot_check("token") + self._updater_check("token") self._token = token return self @@ -347,10 +357,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - if self._bot is not DEFAULT_NONE: - raise RuntimeError(_TWO_ARGS_REQ.format("base_url", "bot instance")) - if self._updater not in (DEFAULT_NONE, None): - raise RuntimeError(_TWO_ARGS_REQ.format("base_url", "updater")) + self._bot_check("base_url") + self._updater_check("base_url") self._base_url = base_url return self @@ -368,10 +376,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - if self._bot is not DEFAULT_NONE: - raise RuntimeError(_TWO_ARGS_REQ.format("base_file_url", "bot instance")) - if self._updater not in (DEFAULT_NONE, None): - raise RuntimeError(_TWO_ARGS_REQ.format("base_file_url", "updater")) + self._bot_check("base_file_url") + self._updater_check("base_file_url") self._base_file_url = base_file_url return self @@ -388,8 +394,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): raise RuntimeError(_TWO_ARGS_REQ.format(name, "connection_pool_size")) if not isinstance(getattr(self, f"_{prefix}proxy_url"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format(name, "proxy_url")) - if self._bot is not DEFAULT_NONE: - raise RuntimeError(_TWO_ARGS_REQ.format(name, "bot instance")) + self._bot_check(name) if self._updater not in (DEFAULT_NONE, None): raise RuntimeError(_TWO_ARGS_REQ.format(name, "updater instance")) @@ -670,10 +675,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - if self._bot is not DEFAULT_NONE: - raise RuntimeError(_TWO_ARGS_REQ.format("private_key", "bot instance")) - if self._updater not in (DEFAULT_NONE, None): - raise RuntimeError(_TWO_ARGS_REQ.format("private_key", "updater")) + self._bot_check("private_key") + self._updater_check("private_key") self._private_key = ( private_key if isinstance(private_key, bytes) else Path(private_key).read_bytes() @@ -698,10 +701,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - if self._bot is not DEFAULT_NONE: - raise RuntimeError(_TWO_ARGS_REQ.format("defaults", "bot instance")) - if self._updater not in (DEFAULT_NONE, None): - raise RuntimeError(_TWO_ARGS_REQ.format("defaults", "updater")) + self._bot_check("defaults") + self._updater_check("defaults") self._defaults = defaults return self @@ -725,13 +726,30 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - if self._bot is not DEFAULT_NONE: - raise RuntimeError(_TWO_ARGS_REQ.format("arbitrary_callback_data", "bot instance")) - if self._updater not in (DEFAULT_NONE, None): - raise RuntimeError(_TWO_ARGS_REQ.format("arbitrary_callback_data", "updater")) + self._bot_check("arbitrary_callback_data") + self._updater_check("arbitrary_callback_data") self._arbitrary_callback_data = arbitrary_callback_data return self + def local_mode(self: BuilderType, local_mode: bool) -> BuilderType: + """Specifies the value for :paramref:`~telegram.Bot.local_mode` for the + :attr:`telegram.ext.Application.bot`. + If not called, will default to :obj:`False`. + + .. seealso:: `Local Bot API Server `_, + + Args: + local_mode (:obj:`bool`): Whether the bot should run in local mode. + + Returns: + :class:`ApplicationBuilder`: The same builder with the updated argument. + """ + self._bot_check("local_mode") + self._updater_check("local_mode") + self._local_mode = local_mode + return self + def bot( self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", bot: InBT, @@ -746,8 +764,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - if self._updater not in (DEFAULT_NONE, None): - raise RuntimeError(_TWO_ARGS_REQ.format("bot", "updater")) + self._updater_check("bot") for attr, error in _BOT_CHECKS: if not isinstance(getattr(self, f"_{attr}"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format("bot", error)) @@ -992,10 +1009,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ - if self._bot is not DEFAULT_NONE: - raise RuntimeError(_TWO_ARGS_REQ.format("rate_limiter", "bot instance")) - if self._updater not in (DEFAULT_NONE, None): - raise RuntimeError(_TWO_ARGS_REQ.format("rate_limiter", "updater")) + self._bot_check("rate_limiter") + self._updater_check("rate_limiter") self._rate_limiter = rate_limiter return self # type: ignore[return-value] diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 785d4be39..37ea55025 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -163,6 +163,7 @@ class ExtBot(Bot, Generic[RLARGS]): private_key_password: bytes = None, defaults: "Defaults" = None, arbitrary_callback_data: Union[bool, int] = False, + local_mode: bool = False, ): ... @@ -178,6 +179,7 @@ class ExtBot(Bot, Generic[RLARGS]): private_key_password: bytes = None, defaults: "Defaults" = None, arbitrary_callback_data: Union[bool, int] = False, + local_mode: bool = False, rate_limiter: "BaseRateLimiter[RLARGS]" = None, ): ... @@ -193,6 +195,7 @@ class ExtBot(Bot, Generic[RLARGS]): private_key_password: bytes = None, defaults: "Defaults" = None, arbitrary_callback_data: Union[bool, int] = False, + local_mode: bool = False, rate_limiter: "BaseRateLimiter" = None, ): super().__init__( @@ -203,6 +206,7 @@ class ExtBot(Bot, Generic[RLARGS]): get_updates_request=get_updates_request, private_key=private_key, private_key_password=private_key_password, + local_mode=local_mode, ) self._defaults = defaults self._rate_limiter = rate_limiter diff --git a/tests/test_animation.py b/tests/test_animation.py index dfc3a265d..a1c804b2f 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -22,7 +22,7 @@ from pathlib import Path import pytest from flaky import flaky -from telegram import Animation, Bot, MessageEntity, PhotoSize, Voice +from telegram import Animation, Bot, InputFile, MessageEntity, PhotoSize, Voice from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData @@ -202,20 +202,30 @@ class TestAnimation: assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) - async def test_send_animation_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_send_animation_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("animation") == expected and data.get("thumb") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("animation") == expected and data.get("thumb") == expected + else: + test_flag = isinstance(data.get("animation"), InputFile) and isinstance( + data.get("thumb"), InputFile + ) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.send_animation(chat_id, file, thumb=file) - assert test_flag - monkeypatch.delattr(bot, "_post") + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.send_animation(chat_id, file, thumb=file) + assert test_flag + monkeypatch.delattr(bot, "_post") + finally: + bot._local_mode = False @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_applicationbuilder.py b/tests/test_applicationbuilder.py index 5d48e7805..c4930e39d 100644 --- a/tests/test_applicationbuilder.py +++ b/tests/test_applicationbuilder.py @@ -84,6 +84,7 @@ class TestApplicationBuilder: assert app.bot.arbitrary_callback_data is False assert app.bot.defaults is None assert app.bot.rate_limiter is None + assert app.bot.local_mode is False get_updates_client = app.bot._request[0]._client assert get_updates_client.limits == httpx.Limits( @@ -257,6 +258,8 @@ class TestApplicationBuilder: get_updates_request ).rate_limiter( rate_limiter + ).local_mode( + True ) built_bot = builder.build().bot @@ -273,6 +276,7 @@ class TestApplicationBuilder: assert built_bot.callback_data_cache.maxsize == 42 assert built_bot.private_key assert built_bot.rate_limiter is rate_limiter + assert built_bot.local_mode is True @dataclass class Client: diff --git a/tests/test_audio.py b/tests/test_audio.py index b10398cd3..36dc2335a 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -22,7 +22,7 @@ from pathlib import Path import pytest from flaky import flaky -from telegram import Audio, Bot, MessageEntity, Voice +from telegram import Audio, Bot, InputFile, MessageEntity, Voice from telegram.error import TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData @@ -234,20 +234,30 @@ class TestAudio: unprotected = await default_bot.send_audio(chat_id, audio, protect_content=False) assert not unprotected.has_protected_content - async def test_send_audio_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_send_audio_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("audio") == expected and data.get("thumb") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("audio") == expected and data.get("thumb") == expected + else: + test_flag = isinstance(data.get("audio"), InputFile) and isinstance( + data.get("thumb"), InputFile + ) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.send_audio(chat_id, file, thumb=file) - assert test_flag - monkeypatch.delattr(bot, "_post") + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.send_audio(chat_id, file, thumb=file) + assert test_flag + monkeypatch.delattr(bot, "_post") + finally: + bot._local_mode = False def test_de_json(self, bot, audio): json_dict = { diff --git a/tests/test_bot.py b/tests/test_bot.py index 4ee91e2c9..ad5c6dfd2 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -44,6 +44,7 @@ from telegram import ( InlineQueryResultArticle, InlineQueryResultDocument, InlineQueryResultVoice, + InputFile, InputMedia, InputTextMessageContent, LabeledPrice, @@ -1578,18 +1579,25 @@ class TestBot: @flaky(3, 1) @pytest.mark.parametrize("use_ip", [True, False]) - async def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot, use_ip): + # local file path as file_input is tested below in test_set_webhook_params + @pytest.mark.parametrize("file_input", ["bytes", "file_handle"]) + async def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot, use_ip, file_input): url = "https://python-telegram-bot.org/test/webhook" # Get the ip address of the website - dynamically just in case it ever changes ip = socket.gethostbyname("python-telegram-bot.org") max_connections = 7 allowed_updates = ["message"] + file_input = ( + data_file("sslcert.pem").read_bytes() + if file_input == "bytes" + else data_file("sslcert.pem").open("rb") + ) await bot.set_webhook( url, max_connections=max_connections, allowed_updates=allowed_updates, ip_address=ip if use_ip else None, - certificate=data_file("sslcert.pem").read_bytes() if use_ip else None, + certificate=file_input if use_ip else None, ) await asyncio.sleep(1) @@ -1620,16 +1628,25 @@ class TestBot: assert await bot.set_webhook("", drop_pending_updates=drop_pending_updates) assert await bot.delete_webhook(drop_pending_updates=drop_pending_updates) - async def test_set_webhook_params(self, bot, monkeypatch): + @pytest.mark.parametrize("local_file", ["str", "Path", False]) + async def test_set_webhook_params(self, bot, monkeypatch, local_file): # actually making calls to TG is done in # test_set_webhook_get_webhook_info_and_delete_webhook. Sadly secret_token can't be tested # there so we have this function \o/ async def make_assertion(*args, **_): kwargs = args[1] + + if local_file is False: + cert_assertion = ( + kwargs["certificate"].input_file_content + == data_file("sslcert.pem").read_bytes() + ) + else: + cert_assertion = data_file("sslcert.pem").as_uri() + return ( kwargs["url"] == "example.com" - and kwargs["certificate"].input_file_content - == data_file("sslcert.pem").read_bytes() + and cert_assertion and kwargs["max_connections"] == 7 and kwargs["allowed_updates"] == ["messages"] and kwargs["ip_address"] == "127.0.0.1" @@ -1639,9 +1656,17 @@ class TestBot: monkeypatch.setattr(bot, "_post", make_assertion) + cert_path = data_file("sslcert.pem") + if local_file == "str": + certificate = str(cert_path) + elif local_file == "Path": + certificate = cert_path + else: + certificate = cert_path.read_bytes() + assert await bot.set_webhook( "example.com", - data_file("sslcert.pem").read_bytes(), + certificate, 7, ["messages"], "127.0.0.1", @@ -2163,19 +2188,27 @@ class TestBot: func, "Type of file mismatch", "Telegram did not accept the file." ) - async def test_set_chat_photo_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_set_chat_photo_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("photo") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("photo") == expected + else: + test_flag = isinstance(data.get("photo"), InputFile) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.set_chat_photo(chat_id, file) - assert test_flag + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.set_chat_photo(chat_id, file) + assert test_flag + finally: + bot._local_mode = False @flaky(3, 1) async def test_delete_chat_photo(self, bot, channel_id): diff --git a/tests/test_document.py b/tests/test_document.py index 9c8c3c5e2..54bd0a7e9 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -22,7 +22,7 @@ from pathlib import Path import pytest from flaky import flaky -from telegram import Bot, Document, MessageEntity, PhotoSize, Voice +from telegram import Bot, Document, InputFile, MessageEntity, PhotoSize, Voice from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData @@ -255,19 +255,29 @@ class TestDocument: unprotected = await default_bot.send_document(chat_id, document, protect_content=False) assert not unprotected.has_protected_content - async def test_send_document_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_send_document_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("document") == expected and data.get("thumb") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("document") == expected and data.get("thumb") == expected + else: + test_flag = isinstance(data.get("document"), InputFile) and isinstance( + data.get("thumb"), InputFile + ) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.send_document(chat_id, file, thumb=file) - assert test_flag + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.send_document(chat_id, file, thumb=file) + assert test_flag + finally: + bot._local_mode = False def test_de_json(self, bot, document): json_dict = { diff --git a/tests/test_files.py b/tests/test_files.py index 62d7be9bc..8ed816bb4 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -16,6 +16,9 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +import subprocess +import sys +from pathlib import Path import pytest @@ -43,23 +46,43 @@ class TestFiles: assert telegram._utils.files.is_local_file(string) == expected @pytest.mark.parametrize( - "string,expected", + "string,expected_local,expected_non_local", [ - (data_file("game.gif"), data_file("game.gif").as_uri()), - (TEST_DATA_PATH, TEST_DATA_PATH), - ("file://foobar", "file://foobar"), - (str(data_file("game.gif")), data_file("game.gif").as_uri()), - (str(TEST_DATA_PATH), str(TEST_DATA_PATH)), - (data_file("game.gif"), data_file("game.gif").as_uri()), - (TEST_DATA_PATH, TEST_DATA_PATH), + (data_file("game.gif"), data_file("game.gif").as_uri(), InputFile), + (TEST_DATA_PATH, TEST_DATA_PATH, TEST_DATA_PATH), + ("file://foobar", "file://foobar", ValueError), + (str(data_file("game.gif")), data_file("game.gif").as_uri(), InputFile), + (str(TEST_DATA_PATH), str(TEST_DATA_PATH), str(TEST_DATA_PATH)), ( "https:/api.org/file/botTOKEN/document/file_3", "https:/api.org/file/botTOKEN/document/file_3", + "https:/api.org/file/botTOKEN/document/file_3", ), ], + ids=[ + "Path(local_file)", + "Path(directory)", + "file_uri", + "str-path local file", + "str-path directory", + "URL", + ], ) - def test_parse_file_input_string(self, string, expected): - assert telegram._utils.files.parse_file_input(string) == expected + def test_parse_file_input_string(self, string, expected_local, expected_non_local): + assert telegram._utils.files.parse_file_input(string, local_mode=True) == expected_local + + if expected_non_local is InputFile: + assert isinstance( + telegram._utils.files.parse_file_input(string, local_mode=False), InputFile + ) + elif expected_non_local is ValueError: + with pytest.raises(ValueError): + telegram._utils.files.parse_file_input(string, local_mode=False) + else: + assert ( + telegram._utils.files.parse_file_input(string, local_mode=False) + == expected_non_local + ) def test_parse_file_input_file_like(self): source_file = data_file("game.gif") @@ -105,3 +128,34 @@ class TestFiles: assert isinstance(parsed, InputFile) assert bool(parsed.attach_name) is attach + + def test_load_file_none(self): + assert telegram._utils.files.load_file(None) == (None, None) + + @pytest.mark.parametrize("arg", [b"bytes", "string", InputFile(b"content"), Path("file/path")]) + def test_load_file_no_file(self, arg): + out = telegram._utils.files.load_file(arg) + assert out[0] is None + assert out[1] is arg + + def test_load_file_file_handle(self): + out = telegram._utils.files.load_file(data_file("telegram.gif").open("rb")) + assert out[0] == "telegram.gif" + assert out[1] == data_file("telegram.gif").read_bytes() + + def test_load_file_subprocess_pipe(self): + png_file = data_file("telegram.png") + cmd_str = "type" if sys.platform == "win32" else "cat" + cmd = [cmd_str, str(png_file)] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=(sys.platform == "win32")) + out = telegram._utils.files.load_file(proc.stdout) + + assert out[0] is None + assert out[1] == png_file.read_bytes() + + try: + proc.kill() + except ProcessLookupError: + # This exception may be thrown if the process has finished before we had the chance + # to kill it. + pass diff --git a/tests/test_photo.py b/tests/test_photo.py index 7098bd09a..a22fba0f7 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -236,19 +236,27 @@ class TestPhoto: unprotected = await default_bot.send_photo(chat_id, photo, protect_content=False) assert not unprotected.has_protected_content - async def test_send_photo_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_send_photo_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("photo") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("photo") == expected + else: + test_flag = isinstance(data.get("photo"), InputFile) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.send_photo(chat_id, file) - assert test_flag + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.send_photo(chat_id, file) + assert test_flag + finally: + bot._local_mode = False @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_sticker.py b/tests/test_sticker.py index 80fc47f26..06a2dcdce 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -23,7 +23,7 @@ from pathlib import Path import pytest from flaky import flaky -from telegram import Audio, Bot, File, MaskPosition, PhotoSize, Sticker, StickerSet +from telegram import Audio, Bot, File, InputFile, MaskPosition, PhotoSize, Sticker, StickerSet from telegram.error import BadRequest, TelegramError from telegram.request import RequestData from tests.conftest import ( @@ -250,20 +250,28 @@ class TestSticker: message = await bot.send_sticker(sticker=sticker, chat_id=chat_id) assert message - async def test_send_sticker_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_send_sticker_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("sticker") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("sticker") == expected + else: + test_flag = isinstance(data.get("sticker"), InputFile) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.send_sticker(chat_id, file) - assert test_flag - monkeypatch.delattr(bot, "_post") + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.send_sticker(chat_id, file) + assert test_flag + monkeypatch.delattr(bot, "_post") + finally: + bot._local_mode = False @flaky(3, 1) @pytest.mark.parametrize( @@ -645,47 +653,67 @@ class TestStickerSet: file_id = video_sticker_set.stickers[-1].file_id assert await bot.delete_sticker_from_set(file_id) - async def test_upload_sticker_file_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_upload_sticker_file_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("png_sticker") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("png_sticker") == expected + else: + test_flag = isinstance(data.get("png_sticker"), InputFile) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.upload_sticker_file(chat_id, file) - assert test_flag - monkeypatch.delattr(bot, "_post") + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.upload_sticker_file(chat_id, file) + assert test_flag + monkeypatch.delattr(bot, "_post") + finally: + bot._local_mode = False - async def test_create_new_sticker_set_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_create_new_sticker_set_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = ( - data.get("png_sticker") == expected - and data.get("tgs_sticker") == expected - and data.get("webm_sticker") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = ( + data.get("png_sticker") == expected + and data.get("tgs_sticker") == expected + and data.get("webm_sticker") == expected + ) + else: + test_flag = ( + isinstance(data.get("png_sticker"), InputFile) + and isinstance(data.get("tgs_sticker"), InputFile) + and isinstance(data.get("webm_sticker"), InputFile) + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.create_new_sticker_set( + chat_id, + "name", + "title", + "emoji", + png_sticker=file, + tgs_sticker=file, + webm_sticker=file, ) - - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.create_new_sticker_set( - chat_id, - "name", - "title", - "emoji", - png_sticker=file, - tgs_sticker=file, - webm_sticker=file, - ) - assert test_flag - monkeypatch.delattr(bot, "_post") + assert test_flag + monkeypatch.delattr(bot, "_post") + finally: + bot._local_mode = False async def test_create_new_sticker_all_params(self, monkeypatch, bot, chat_id, mask_position): async def make_assertion(_, data, *args, **kwargs): @@ -713,35 +741,57 @@ class TestStickerSet: ) monkeypatch.delattr(bot, "_post") - async def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("png_sticker") == expected and data.get("tgs_sticker") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = ( + data.get("png_sticker") == expected and data.get("tgs_sticker") == expected + ) + else: + test_flag = isinstance(data.get("png_sticker"), InputFile) and isinstance( + data.get("tgs_sticker"), InputFile + ) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.add_sticker_to_set(chat_id, "name", "emoji", png_sticker=file, tgs_sticker=file) - assert test_flag - monkeypatch.delattr(bot, "_post") + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.add_sticker_to_set( + chat_id, "name", "emoji", png_sticker=file, tgs_sticker=file + ) + assert test_flag + monkeypatch.delattr(bot, "_post") + finally: + bot._local_mode = False - async def test_set_sticker_set_thumb_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_set_sticker_set_thumb_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("thumb") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("thumb") == expected + else: + test_flag = isinstance(data.get("thumb"), InputFile) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.set_sticker_set_thumb("name", chat_id, thumb=file) - assert test_flag - monkeypatch.delattr(bot, "_post") + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.set_sticker_set_thumb("name", chat_id, thumb=file) + assert test_flag + monkeypatch.delattr(bot, "_post") + finally: + bot._local_mode = False async def test_get_file_instance_method(self, monkeypatch, sticker): async def make_assertion(*_, **kwargs): diff --git a/tests/test_video.py b/tests/test_video.py index a133f0dd0..9476fb235 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -22,7 +22,7 @@ from pathlib import Path import pytest from flaky import flaky -from telegram import Bot, MessageEntity, PhotoSize, Video, Voice +from telegram import Bot, InputFile, MessageEntity, PhotoSize, Video, Voice from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData @@ -247,19 +247,29 @@ class TestVideo: unprotected = await default_bot.send_video(chat_id, video, protect_content=False) assert not unprotected.has_protected_content - async def test_send_video_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_send_video_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("video") == expected and data.get("thumb") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("video") == expected and data.get("thumb") == expected + else: + test_flag = isinstance(data.get("video"), InputFile) and isinstance( + data.get("thumb"), InputFile + ) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.send_video(chat_id, file, thumb=file) - assert test_flag + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.send_video(chat_id, file, thumb=file) + assert test_flag + finally: + bot._local_mode = False @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_videonote.py b/tests/test_videonote.py index 1b96b4b22..3691d5e2f 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -22,7 +22,7 @@ from pathlib import Path import pytest from flaky import flaky -from telegram import Bot, PhotoSize, VideoNote, Voice +from telegram import Bot, InputFile, PhotoSize, VideoNote, Voice from telegram.error import BadRequest, TelegramError from telegram.request import RequestData from tests.conftest import ( @@ -177,19 +177,31 @@ class TestVideoNote: assert video_note_dict["duration"] == video_note.duration assert video_note_dict["file_size"] == video_note.file_size - async def test_send_video_note_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_send_video_note_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("video_note") == expected and data.get("thumb") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = ( + data.get("video_note") == expected and data.get("thumb") == expected + ) + else: + test_flag = isinstance(data.get("video_note"), InputFile) and isinstance( + data.get("thumb"), InputFile + ) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.send_video_note(chat_id, file, thumb=file) - assert test_flag + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.send_video_note(chat_id, file, thumb=file) + assert test_flag + finally: + bot._local_mode = False @flaky(3, 1) @pytest.mark.parametrize( diff --git a/tests/test_voice.py b/tests/test_voice.py index ed6c83ce2..11ba57f56 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -22,7 +22,7 @@ from pathlib import Path import pytest from flaky import flaky -from telegram import Audio, Bot, MessageEntity, Voice +from telegram import Audio, Bot, InputFile, MessageEntity, Voice from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData @@ -207,19 +207,27 @@ class TestVoice: unprotected = await default_bot.send_voice(chat_id, voice, protect_content=False) assert not unprotected.has_protected_content - async def test_send_voice_local_files(self, monkeypatch, bot, chat_id): - # For just test that the correct paths are passed as we have no local bot API set up - test_flag = False - file = data_file("telegram.jpg") - expected = file.as_uri() + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_send_voice_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() - async def make_assertion(_, data, *args, **kwargs): - nonlocal test_flag - test_flag = data.get("voice") == expected + async def make_assertion(_, data, *args, **kwargs): + nonlocal test_flag + if local_mode: + test_flag = data.get("voice") == expected + else: + test_flag = isinstance(data.get("voice"), InputFile) - monkeypatch.setattr(bot, "_post", make_assertion) - await bot.send_voice(chat_id, file) - assert test_flag + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.send_voice(chat_id, file) + assert test_flag + finally: + bot._local_mode = False @flaky(3, 1) @pytest.mark.parametrize(