From 786762bb733156c622685a077a143fb3275a2655 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi Date: Wed, 16 Dec 2020 14:28:53 +0100 Subject: [PATCH] Allow Passing Custom Filename For All Media (#2249) * Add filename arg to send_media methods and InputMedia* * Tests --- telegram/bot.py | 56 ++++++++++++++++++++++++++++++------ telegram/files/inputmedia.py | 30 +++++++++++++++---- tests/test_animation.py | 10 +++++++ tests/test_audio.py | 10 +++++++ tests/test_inputmedia.py | 28 ++++++++++++++++++ tests/test_official.py | 19 +++++++++--- tests/test_photo.py | 10 +++++++ tests/test_video.py | 10 +++++++ tests/test_videonote.py | 10 +++++++ tests/test_voice.py | 10 +++++++ 10 files changed, 176 insertions(+), 17 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 01205ee7d..ac3cf53ef 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -575,6 +575,7 @@ class Bot(TelegramObject): api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ) -> Optional[Message]: """Use this method to send photos. @@ -591,6 +592,9 @@ class Bot(TelegramObject): (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. + filename (:obj:`str`, optional): Custom file name for the photo, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. caption (:obj:`str`, optional): Photo caption (may also be used when resending photos by file_id), 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to @@ -619,7 +623,10 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data: JSONDict = {'chat_id': chat_id, 'photo': parse_file_input(photo, PhotoSize)} + data: JSONDict = { + 'chat_id': chat_id, + 'photo': parse_file_input(photo, PhotoSize, filename=filename), + } if caption: data['caption'] = caption @@ -657,6 +664,7 @@ class Bot(TelegramObject): api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ) -> Optional[Message]: """ Use this method to send audio files, if you want Telegram clients to display them in the @@ -680,6 +688,9 @@ class Bot(TelegramObject): (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. + 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 + :obj:`tempfile` module. caption (:obj:`str`, optional): Audio caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to @@ -717,7 +728,10 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data: JSONDict = {'chat_id': chat_id, 'audio': parse_file_input(audio, Audio)} + data: JSONDict = { + 'chat_id': chat_id, + 'audio': parse_file_input(audio, Audio, filename=filename), + } if duration: data['duration'] = duration @@ -782,8 +796,9 @@ class Bot(TelegramObject): (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. - filename (:obj:`str`, optional): File name that shows in telegram message (it is useful - when you send file generated by temp module, for example). Undocumented. + 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 + :obj:`tempfile` module. caption (:obj:`str`, optional): Document caption (may also be used when resending documents by file_id), 0-1024 characters after entities parsing. disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side @@ -927,6 +942,7 @@ class Bot(TelegramObject): api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ) -> Optional[Message]: """ Use this method to send video files, Telegram clients support mp4 videos @@ -951,6 +967,9 @@ class Bot(TelegramObject): (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. + 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 + :obj:`tempfile` module. duration (:obj:`int`, optional): Duration of sent video in seconds. width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. @@ -990,7 +1009,10 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data: JSONDict = {'chat_id': chat_id, 'video': parse_file_input(video, Video)} + data: JSONDict = { + 'chat_id': chat_id, + 'video': parse_file_input(video, Video, filename=filename), + } if duration: data['duration'] = duration @@ -1034,6 +1056,7 @@ class Bot(TelegramObject): thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, + filename: str = None, ) -> Optional[Message]: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. @@ -1055,6 +1078,9 @@ class Bot(TelegramObject): 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. + 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 + :obj:`tempfile` module. 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. @@ -1086,7 +1112,7 @@ class Bot(TelegramObject): """ data: JSONDict = { 'chat_id': chat_id, - 'video_note': parse_file_input(video_note, VideoNote), + 'video_note': parse_file_input(video_note, VideoNote, filename=filename), } if duration is not None: @@ -1125,6 +1151,7 @@ class Bot(TelegramObject): api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ) -> Optional[Message]: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -1145,6 +1172,9 @@ class Bot(TelegramObject): 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. Lastly you can pass an existing :class:`telegram.Animation` object to send. + filename (:obj:`str`, optional): Custom file name for the animation, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. duration (:obj:`int`, optional): Duration of sent animation in seconds. width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. @@ -1182,7 +1212,10 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data: JSONDict = {'chat_id': chat_id, 'animation': parse_file_input(animation, Animation)} + data: JSONDict = { + 'chat_id': chat_id, + 'animation': parse_file_input(animation, Animation, filename=filename), + } if duration: data['duration'] = duration @@ -1225,6 +1258,7 @@ class Bot(TelegramObject): api_kwargs: JSONDict = None, allow_sending_without_reply: bool = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ) -> Optional[Message]: """ Use this method to send audio files, if you want Telegram clients to display the file @@ -1245,6 +1279,9 @@ class Bot(TelegramObject): (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. + filename (:obj:`str`, optional): Custom file name for the voice, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. caption (:obj:`str`, optional): Voice message caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to @@ -1274,7 +1311,10 @@ class Bot(TelegramObject): :class:`telegram.TelegramError` """ - data: JSONDict = {'chat_id': chat_id, 'voice': parse_file_input(voice, Voice)} + data: JSONDict = { + 'chat_id': chat_id, + 'voice': parse_file_input(voice, Voice, filename=filename), + } if duration: data['duration'] = duration diff --git a/telegram/files/inputmedia.py b/telegram/files/inputmedia.py index 50091b853..1c150f8fa 100644 --- a/telegram/files/inputmedia.py +++ b/telegram/files/inputmedia.py @@ -78,6 +78,9 @@ class InputMediaAnimation(InputMedia): 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. + filename (:obj:`str`, optional): Custom file name for the animation, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. thumb (`filelike object` | :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 @@ -111,6 +114,7 @@ class InputMediaAnimation(InputMedia): height: int = None, duration: int = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ): self.type = 'animation' @@ -120,7 +124,7 @@ class InputMediaAnimation(InputMedia): self.height = media.height self.duration = media.duration else: - self.media = parse_file_input(media, attach=True) + self.media = parse_file_input(media, attach=True, filename=filename) if thumb: self.thumb = parse_file_input(thumb, attach=True) @@ -154,6 +158,9 @@ class InputMediaPhoto(InputMedia): 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. + filename (:obj:`str`, optional): Custom file name for the photo, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show @@ -169,9 +176,10 @@ class InputMediaPhoto(InputMedia): caption: str = None, parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ): self.type = 'photo' - self.media = parse_file_input(media, PhotoSize, attach=True) + self.media = parse_file_input(media, PhotoSize, attach=True, filename=filename) if caption: self.caption = caption @@ -202,6 +210,9 @@ class InputMediaVideo(InputMedia): 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. + 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 + :obj:`tempfile` module. caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show @@ -241,6 +252,7 @@ class InputMediaVideo(InputMedia): parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, thumb: FileInput = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ): self.type = 'video' @@ -250,7 +262,7 @@ class InputMediaVideo(InputMedia): self.height = media.height self.duration = media.duration else: - self.media = parse_file_input(media, attach=True) + self.media = parse_file_input(media, attach=True, filename=filename) if thumb: self.thumb = parse_file_input(thumb, attach=True) @@ -291,6 +303,9 @@ class InputMediaAudio(InputMedia): 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. + 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 + :obj:`tempfile` module. caption (:obj:`str`, optional): Caption of the audio to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show @@ -325,6 +340,7 @@ class InputMediaAudio(InputMedia): performer: str = None, title: str = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ): self.type = 'audio' @@ -334,7 +350,7 @@ class InputMediaAudio(InputMedia): self.performer = media.performer self.title = media.title else: - self.media = parse_file_input(media, attach=True) + self.media = parse_file_input(media, attach=True, filename=filename) if thumb: self.thumb = parse_file_input(thumb, attach=True) @@ -372,6 +388,9 @@ class InputMediaDocument(InputMedia): 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. + 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 + :obj:`tempfile` module. caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show @@ -398,9 +417,10 @@ class InputMediaDocument(InputMedia): parse_mode: Union[str, DefaultValue] = DEFAULT_NONE, disable_content_type_detection: bool = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, + filename: str = None, ): self.type = 'document' - self.media = parse_file_input(media, Document, attach=True) + self.media = parse_file_input(media, Document, attach=True, filename=filename) if thumb: self.thumb = parse_file_input(thumb, attach=True) diff --git a/tests/test_animation.py b/tests/test_animation.py index 91b86153a..55b7d996f 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -96,6 +96,16 @@ class TestAnimation: assert message.animation.thumb.width == self.width assert message.animation.thumb.height == self.height + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_animation_custom_filename(self, bot, chat_id, animation_file, monkeypatch): + def make_assertion(url, data, **kwargs): + return data['animation'].filename == 'custom_filename' + + monkeypatch.setattr(bot.request, 'post', make_assertion) + + assert bot.send_animation(chat_id, animation_file, filename='custom_filename') + @flaky(3, 1) @pytest.mark.timeout(10) def test_get_and_download(self, bot, animation): diff --git a/tests/test_audio.py b/tests/test_audio.py index 340dc631d..b1883fc78 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -108,6 +108,16 @@ class TestAudio: assert message.audio.thumb.width == self.thumb_width assert message.audio.thumb.height == self.thumb_height + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_audio_custom_filename(self, bot, chat_id, audio_file, monkeypatch): + def make_assertion(url, data, **kwargs): + return data['audio'].filename == 'custom_filename' + + monkeypatch.setattr(bot.request, 'post', make_assertion) + + assert bot.send_audio(chat_id, audio_file, filename='custom_filename') + @flaky(3, 1) @pytest.mark.timeout(10) def test_get_and_download(self, bot, audio): diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index 5b535f76d..b693f39d7 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -437,6 +437,34 @@ class TestSendMediaGroup: mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] for mes in messages ) + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_media_group_custom_filename( + self, + bot, + chat_id, + photo_file, # noqa: F811 + animation_file, # noqa: F811 + audio_file, # noqa: F811 + video_file, # noqa: F811 + monkeypatch, + ): + def make_assertion(url, data, **kwargs): + result = all(im.media.filename == 'custom_filename' for im in data['media']) + # We are a bit hacky here b/c Bot.send_media_group expects a list of Message-dicts + return [Message(0, None, None, text=result).to_dict()] + + monkeypatch.setattr(bot.request, 'post', make_assertion) + + media = [ + InputMediaAnimation(animation_file, filename='custom_filename'), + InputMediaAudio(audio_file, filename='custom_filename'), + InputMediaPhoto(photo_file, filename='custom_filename'), + InputMediaVideo(video_file, filename='custom_filename'), + ] + + assert bot.send_media_group(chat_id, media)[0].text is True + def test_send_media_group_with_thumbs( self, bot, chat_id, video_file, photo_file, monkeypatch # noqa: F811 ): diff --git a/tests/test_official.py b/tests/test_official.py index 995806151..56f830bb7 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -76,8 +76,19 @@ def check_method(h4): ignored = IGNORED_PARAMETERS.copy() if name == 'getUpdates': ignored -= {'timeout'} # Has it's own timeout parameter that we do wanna check for - elif name == 'sendDocument': - ignored |= {'filename'} # Undocumented + elif name in ( + f'send{media_type}' + for media_type in [ + 'Animation', + 'Audio', + 'Document', + 'Photo', + 'Video', + 'VideoNote', + 'Voice', + ] + ): + ignored |= {'filename'} # Convenience parameter elif name == 'setGameScore': ignored |= {'edit_message'} # TODO: Now deprecated, so no longer in telegrams docs elif name == 'sendContact': @@ -131,8 +142,8 @@ def check_object(h4): ignored |= {'credentials'} elif name == 'PassportElementError': ignored |= {'message', 'type', 'source'} - elif name == 'Message': - ignored |= {'default_quote'} + elif name.startswith('InputMedia'): + ignored |= {'filename'} # Convenience parameter assert (sig.parameters.keys() ^ checked) - ignored == set() diff --git a/tests/test_photo.py b/tests/test_photo.py index 4d8cc1798..79eb41cd4 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -115,6 +115,16 @@ class TestPhoto: assert message.caption == TestPhoto.caption.replace('*', '') + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_photo_custom_filename(self, bot, chat_id, photo_file, monkeypatch): + def make_assertion(url, data, **kwargs): + return data['photo'].filename == 'custom_filename' + + monkeypatch.setattr(bot.request, 'post', make_assertion) + + assert bot.send_photo(chat_id, photo_file, filename='custom_filename') + @flaky(3, 1) @pytest.mark.timeout(10) def test_send_photo_parse_mode_markdown(self, bot, chat_id, photo_file, thumb, photo): diff --git a/tests/test_video.py b/tests/test_video.py index fe9f5108b..d11e7fedd 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -114,6 +114,16 @@ class TestVideo: assert message.video.file_name == self.file_name + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_video_custom_filename(self, bot, chat_id, video_file, monkeypatch): + def make_assertion(url, data, **kwargs): + return data['video'].filename == 'custom_filename' + + monkeypatch.setattr(bot.request, 'post', make_assertion) + + assert bot.send_video(chat_id, video_file, filename='custom_filename') + @flaky(3, 1) @pytest.mark.timeout(10) def test_get_and_download(self, bot, video): diff --git a/tests/test_videonote.py b/tests/test_videonote.py index 517a07d0b..ff486080b 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -96,6 +96,16 @@ class TestVideoNote: assert message.video_note.thumb.width == self.thumb_width assert message.video_note.thumb.height == self.thumb_height + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_video_note_custom_filename(self, bot, chat_id, video_note_file, monkeypatch): + def make_assertion(url, data, **kwargs): + return data['video_note'].filename == 'custom_filename' + + monkeypatch.setattr(bot.request, 'post', make_assertion) + + assert bot.send_video_note(chat_id, video_note_file, filename='custom_filename') + @flaky(3, 1) @pytest.mark.timeout(10) def test_get_and_download(self, bot, video_note): diff --git a/tests/test_voice.py b/tests/test_voice.py index 4ebed9f35..30d48ce03 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -86,6 +86,16 @@ class TestVoice: assert message.voice.file_size == voice.file_size assert message.caption == self.caption.replace('*', '') + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_voice_custom_filename(self, bot, chat_id, voice_file, monkeypatch): + def make_assertion(url, data, **kwargs): + return data['voice'].filename == 'custom_filename' + + monkeypatch.setattr(bot.request, 'post', make_assertion) + + assert bot.send_voice(chat_id, voice_file, filename='custom_filename') + @flaky(3, 1) @pytest.mark.timeout(10) def test_get_and_download(self, bot, voice):